Why we built our own ad-attribution stack
The Telegram Mini App ecosystem grew fast over the last 18 months — but the marketing infrastructure for paid acquisition inside Telegram channels did not. When we started running our first paid shoutouts in @fuckingenglish (322k subscribers learning English through real-world slang), the standard playbook was: drop a t.me/wall/app deep-link, count signups in your DB the next day, hope for the best.
That works for a single campaign. It collapses the moment you're running 5+ campaigns across multiple channels with different creative variants. We needed: per-channel attribution, per-creative breakdown (button vs in-text hyperlink vs link-preview), orphan-safe logging for typo'd URLs, and dual URL formats for in-Telegram vs off-Telegram surfaces. Off-the-shelf trackers don't fit the Telegram-Mini-App model — they're built for browser-only journeys.
So we built it ourselves at the bot level. Wall Ad Network Phase 0 went live in May 2026. Public architecture docs at wall.support/ad-network.
How the URL anatomy works
Every paid-campaign URL on Wall encodes channel, creative, and placement in a single hyphen-separated token after ref_:
https://t.me/wall/app?startapp=ref_<root>-<utm1>-<utm2>-<utm3>
- root — a registered
ReferralLink.slugowned by the buyer (e.g.,adfor the Wall Ads namespace) - utm1 — source channel code (e.g.,
fe= @fuckingenglish,tgl= @tglive) - utm2 — creative / post identifier (e.g.,
tagged,replyguy) - utm3 — placement (
btn= inline button,body= in-text hyperlink,qr= QR code,dm= direct-message share)
The bot's /start handler and the Mini App's /api/init both parse this token, look up the root in the database, and store the full UTM breakdown in ReferralEvent.metadata. SQL filtering on metadata->>'utm1' = 'fe' (for example) gives instant per-channel cohort analysis without a separate analytics warehouse.
Two URL formats — for inside and outside Telegram
The architecture exposes two URL formats per slug, used in different contexts:
- 📱 In-Telegram deeplink —
t.me/wall/app?startapp=ref_<token>. Telegram opens the Mini App in 1 tap with no browser flash. Used for inline buttons in channel posts, in-text hyperlinks, DM shares, forwarded messages. - 🌐 Universal redirector —
wall.tg/r/<token>?c=&s=. Logs the click event before 302-redirecting to Telegram. Used for off-Telegram surfaces (Twitter, blogs, QR codes, email campaigns) where we need to capture click events even when /start never fires.
The redirector also filters known link-preview-bot User-Agents (TelegramBot, TwitterBot, facebookexternalhit, LinkedInBot, Slackbot, WhatsApp, Discordbot, Googlebot, bingbot, YandexBot, Pinterest, redditbot, Applebot, DuckDuckBot, Mastodon) so preview hits don't pollute the click metric.
Orphan-safe attribution
Typos happen. A campaign URL can be hand-edited by a channel admin and a hyphen accidentally turned into an underscore. Without orphan logging, those clicks would silently disappear and we'd lose paid-campaign ROI to misattribution.
Wall's parser logs every unrecognised referral token as a ReferralEvent { type: 'unknown_ref' } with the raw URL preserved in metadata.rawParam. Buyers can periodically scan the orphan log via SQL:
SELECT metadata->>'rawParam' AS bad_url, COUNT(*) AS hits
FROM referral_events
WHERE type = 'unknown_ref'
AND created_at > NOW() - INTERVAL '7 days'
GROUP BY bad_url ORDER BY hits DESC;
After spotting common typos (e.g., fe_tagged_bttn instead of fe-tagged-btn), admins manually re-bind those events to the correct slug — preserving attribution that would otherwise be lost ROI on a paid campaign.
What's in production today vs Phase 1+ roadmap
Phase 0 (live): parser in lib/referral-token.ts, redirector at app/r/[slug]/route.ts, UTM-aware /start handler in bot/index.ts, UTM-aware /api/init in the Mini App. Per-slug analytics via direct SQL on referral_events.metadata already powers our internal dashboards.
Phase 1 (in development): dedicated AdCampaign + AdClick database tables for persistent campaign aggregation. Bridge from off-Telegram clicks (logged in the redirector) to /start events (when the user lands in the Mini App) via Redis. Off-Telegram clicks that never reach /start get an AdClick row with resultedInStart = false — full top-of-funnel visibility.
Phase 2: /promo cabinet UI for advertisers. List of campaigns, per-campaign A/B charts, per-channel cohort LTV, ROI calculator. Access gated by Profile.role IN ('admin', 'ad_buyer').
Phase 3: publisher cabinet at /publisher for channel admins selling ad slots. Marketplace for buyers + escrow flow.
Phase 4: public REST API for third-party advertisers. Automated bid management.
First campaign — @fuckingenglish (322k subs)
The first ad buy launched in May 2026: a two-post arc on the popular Russian-language English-learning channel @fuckingenglish, covering the slang words "tagged" and "reply guy". URLs used:
ref_ad-fe-tagged-btn ← POST 1 inline button
ref_ad-fe-tagged-body ← POST 1 in-text hyperlink
ref_ad-fe-replyguy-btn ← POST 2 inline button
ref_ad-fe-replyguy-body ← POST 2 in-text hyperlink
All four URLs share the same root (ad) — one ReferralLink row owns the entire campaign. Per-creative + per-placement breakdown comes from SQL on metadata.utm2 and metadata.utm3. The button-vs-in-text A/B is visible from the first hour after publication; per-user cohort analysis (which subscribers later become Premium) drives the re-buy decision for the next campaign in the same channel.
Why publish the architecture instead of treating it as a moat
The "competitive advantage" of hidden architecture decisions is overrated. Public documentation forces internal clarity (you can't write what you don't understand), attracts collaborator-grade users (developers + journalists + power users), and creates a credibility moat that paid PR can't replicate.
For Wall specifically, the ad-network is one of the few infrastructure surfaces most Mini App platforms outsource to third-party trackers. Publishing the full mechanism — with SQL examples, code references, and a 4-phase roadmap — positions Wall as a platform that ships its own marketing primitives end-to-end. That signal matters more than secrecy.
Read more
- wall.support/ad-network — full architecture, FAQ, SQL examples
- wall.support/security — how the bot-level auth + rate limits work
- wall.support/api — public
/api/productJSON endpoint - wall.foundation/transparency — operating principles + decision log
- github.com/gmediaorg/wall-public — public mirror (docs, canonical HTML, daily-stats; live app source stays private)