Multi-platform ad attribution redirect for Telegram Mini-Apps
https://tmetlink.com/{botUsername}/{appName}?agency_id=AGENCY&utm_source=X&utm_medium=Y&...
→ deep link redirect (skips t.me web page):
tg://resolve?domain={botUsername}&appname={appName}&startapp={base64url_encoded_params}
→ CTA fallback (if Telegram not installed):
https://t.me/{botUsername}/{appName}?startapp={base64url_encoded_params}
startapp limit: 512 chars, base64url-safe only [A-Za-z0-9_-]. (For reference: the non-mini-app ?start= bot deep link is capped at 64 chars — too tight to fit click_id + agency + UTMs, which is why this service uses mini-app links exclusively.)
Gift Fest (ru): https://tmetlink.com/giftfest2_bot/app?agency_id=mn&lang=ru&utm_source=test1&utm_medium=test2&utm_campaign=test3&utm_content=test4&utm_term=test5&ad_id=test6&fbclid=IwY2xjawRDnSVleHRuA2FlbQIxMABicmlkETF2eHFpTVFJV3BpNXhieTU0c3J0YwZhcHBfaWQQMjIyMDM5MTc4ODIwMDg5MgABHsACAKT7QmVRW9oM7c5e37lczqpDlRMl09-Y6Xmpo71TIV7sAllQ9ov3btFb_aem_ha4IyVoN-RtWmXNpvOgvhg → tg://resolve?domain=giftfest2_bot&appname=app&startapp=VD1WMVN0R1hSOF9aNWpkSGk2Qm15VHgmQUc9bW4mTE49cnUmVVM9dGVzdDEmVU09dGVzdDImVUNQPXRlc3QzJlVDPXRlc3Q0JlVUPXRlc3Q1 Gift Fest (en): https://tmetlink.com/giftfest2_bot/app?agency_id=mn&lang=en&utm_source=test1&utm_medium=test2&utm_campaign=test3&utm_content=test4&utm_term=test5&ad_id=test6&fbclid=IwY2xjawRDnSVleHRuA2FlbQIxMABicmlkETF2eHFpTVFJV3BpNXhieTU0c3J0YwZhcHBfaWQQMjIyMDM5MTc4ODIwMDg5MgABHsACAKT7QmVRW9oM7c5e37lczqpDlRMl09-Y6Xmpo71TIV7sAllQ9ov3btFb_aem_ha4IyVoN-RtWmXNpvOgvhg → tg://resolve?domain=giftfest2_bot&appname=app&startapp=VD1LMllwTFJxM1Z4Qm44TWVDcFhnWWomQUc9bW4mTE49ZW4mVVM9dGVzdDEmVU09dGVzdDImVUNQPXRlc3QzJlVDPXRlc3Q0JlVUPXRlc3Q1 Mira: https://tmetlink.com/mira_ibot/app?agency_id=ts&lang=en&utm_source=meta&utm_medium=cpc&utm_campaign=launch → tg://resolve?domain=mira_ibot&appname=app&startapp=VD1tMVJhWHl6OV9BYkMyZEVmR2hJajAmQUc9dHMmTE49ZW4mVVM9bWV0YSZVTT1jcGMmVUNQPWxhdW5jaA Tonstakers: https://tmetlink.com/tonstakers_bot/app?agency_id=mn&lang=en&utm_source=google&utm_medium=cpc&utm_campaign=staking → tg://resolve?domain=tonstakers_bot&appname=app&startapp=VD10Uy1LOXBRNFdtWDJyTDd2WWJOMWMmQUc9bW4mTE49ZW4mVVM9Z29vZ2xlJlVNPWNwYyZVQ1A9c3Rha2luZw Tribute: https://tmetlink.com/subscribeappbot/app?agency_id=ts&lang=ru&utm_source=tiktok&utm_medium=video&utm_campaign=growth → tg://resolve?domain=subscribeappbot&appname=app&startapp=VD1UcjFiVXQzLVg5elAyX0tqUTh3QXYmQUc9dHMmTE49cnUmVVM9dGlrdG9rJlVNPXZpZGVvJlVDUD1ncm93dGg StonFi: https://tmetlink.com/ston_app_bot/app?agency_id=mn&lang=en&utm_source=yandex&utm_medium=cpc&utm_campaign=dex → tg://resolve?domain=ston_app_bot&appname=app&startapp=VD1TdDBuRjEtRDN4UXBfWjhtQWJWNHkmQUc9bW4mTE49ZW4mVVM9eWFuZGV4JlVNPWNwYyZVQ1A9ZGV4
startapp encoded length: Gift Fest example = 81 raw → 108 / 512. Typical (T + AG + LN + source/medium): 54 raw → 72 / 512. fbclid (~185 chars) is NOT in startapp — it goes to BQ via sGTM.
T (click_id) is the primary join key: sGTM logs the landing page hit to BigQuery with T, the TMA backend decodes T from startapp and stores it with the conversion event. Airflow joins on click_id to match ad click → conversion for offline postbacks.
URL params with a constrained set of values:
| Param | Slugs | Default |
|---|---|---|
lang | en, ru, es, pt, fr, de, ar, zh | en |
theme | light, dark | OS preference (prefers-color-scheme) |
How lang works: the landing calls bot-info (Cloud Function), which opens a Telegram MTProto connection with lang_code=<lang> and runs users.GetFullUser(@bot). Telegram returns the bot name and about-text in that language — whatever the bot owner registered via BotFather. Static UI strings (CTA, "DOWNLOAD FOR" prefix, monthly-users suffix) are baked into custom.js for the 8 listed slugs.
To enable a new language (or improve an existing one):
1. Bot owner in BotFather: /setname and /setabouttext → pick the bot → choose the target language → submit the localized name and bio. Without this, Telegram falls back to the default (usually English) for that lang.
2. Landing UI strings: extend the STRINGS map in static/custom.js with cta, title, mau, dl for the new lang and redeploy. Adding a slug here also widens the lang table above.
The original URL contains many params (fbclid, ad_id, etc.), but only the minimal set is encoded into startapp. Everything else goes to sGTM → BigQuery and is joined later by click_id.
T=V1StGXR8_Z5jdHi6BmyTx&AG=mn&LN=ru&US=test1&UM=test2&UCP=test3&UC=test4&UT=test5
Which parses to:
| Key | Value | Source |
|---|---|---|
| T | V1StGXR8_Z5jdHi6BmyTx | click_id (generated by JS, nanoid 21 chars) |
| AG | mn | agency_id (URL) |
| LN | ru | lang (URL, default: en) |
| US | test1 | utm_source (URL) |
| UM | test2 | utm_medium (URL) |
| UCP | test3 | utm_campaign (URL) |
| UC | test4 | utm_content (URL) |
| UT | test5 | utm_term (URL) |
fbclid, ad_id, gclid, and all other params from the URL are not in startapp — they are logged to BigQuery via sGTM with the same click_id and joined later by Airflow.
~80 raw chars → ~110 base64url / 512 limit. Well within bounds.
Minimal set — everything the TMA needs for analytics and conversion matching:
| Key | Input | Source | Purpose |
|---|---|---|---|
| T | click_id | JS (nanoid) | Primary join key — links this ad click to the TMA conversion event. Airflow joins landing_hits.click_id = conversions.click_id for offline postbacks. |
| AG | agency_id | URL | Agency/pixel routing: ts → 1112919053742027, mn → 1921519221768719 |
| LN | lang | URL | Language code (en, ru, es, etc.). Controls landing page i18n. |
| US | utm_source | URL | Traffic source |
| UM | utm_medium | URL | Marketing medium |
| UCP | utm_campaign | URL | Campaign name |
| UC | utm_content | URL | Ad content variant |
| UT | utm_term | URL | Search term |
| PC | partner_code | URL | Partner code |
| RC | ref_code | URL | Referral code |
All URL params + synthesized values are sent to sGTM via sendBeacon and logged to BigQuery. The TMA doesn't need these — Airflow retrieves them by joining on click_id.
| Input | Source | Purpose |
|---|---|---|
| Platform Click IDs | ||
| fbclid | URL | Meta click ID |
| gclid | URL | Google Ads click ID |
| gbraid | URL | Google privacy-compliant aggregate click ID |
| wbraid | URL | Google privacy-compliant web-to-app click ID |
| ttclid | URL | TikTok Ads click ID |
| yclid | URL | Yandex Direct click ID |
| ysclid | URL | Yandex Search click ID |
| tgclid | URL | Telegram Ads click ID |
| Meta Ads Parameters | ||
| campaign_id | URL | Campaign ID ({{campaign.id}}) |
| adset_id | URL | Ad set ID ({{adset.id}}) |
| ad_id | URL | Ad ID ({{ad.id}}) |
| campaign_name | URL | Campaign name ({{campaign.name}}) |
| adset_name | URL | Ad set name ({{adset.name}}) |
| ad_name | URL | Ad name ({{ad.name}}) |
| placement | URL | Ad placement ({{placement}}) |
| site_source_name | URL | Site source ({{site_source_name}}) |
| Synthesized by JS (Meta attribution) | ||
| fbc | JS | Meta CAPI click ID: fb.1.{timestamp}.{fbclid} |
| fbp | JS | Meta CAPI browser ID: fb.1.{timestamp}.{random} |
| Custom | ||
| partner_code | URL | Partner code |
| ref_code | URL | Referral code |
| Server-side (captured automatically) | ||
| client_ip_address | sGTM | From request headers |
| client_user_agent | sGTM | From request headers |
All params above are logged to BigQuery with the same click_id (T). When the TMA reports a conversion, Airflow joins on click_id to retrieve the full attribution data for offline postbacks to Meta CAPI, Google Ads, TikTok, Yandex.
Ad Platforms (Meta/Google/TikTok/Yandex/Telegram)
↓ ad click (fbclid, gclid, ttclid, yclid, agency_id, UTMs)
tma-ad-landing (Cloud Run)
generate click_id → synthesize fbc/fbp
sendBeacon → sGTM redirect → TMA
(all params) (click_id + AG + UTMs)
↓ ↓
sGTM (Cloud Run) Telegram TMA (per product)
→ BQ streaming On conversion:
→ Meta CAPI* write to BQ with click_id
→ Google Ads*
→ TikTok/Yandex* *=TODO
↓ ↓
BigQuery
landing_sgtm_events conversions (per product)
(shared, all clicks) (per product TMA backend)
←— JOIN on click_id —→
↓
Airflow DAG (per product)
→ Meta CAPI offline conversions
→ Google Ads Enhanced Conversions
→ TikTok Events API
→ Yandex offline conversions