TEASEDocs
Guides

Telegram attribution

See where each paying Telegram fan came from — exact deep-link matches, country-and-time click matching, and an honest precision badge on every row.

Your funnel doesn't only end on OnlyFans — part of the traffic is routed straight into a Telegram sales bot, and that bot produces its own revenue. Telegram attribution answers the same question Attribution answers for OnlyFans, but for the bot: which of your domains sent each paying Telegram fan, and how much did they pay?

The catch: the bot is reached through a plain redirect, so most fans arrive without a tracking payload. TEASE therefore resolves each fan with a layered model — an exact match where one exists, a time-and-country match for the bulk, and an honest organic residual — and stamps every row with a precision badge so a calculation is never passed off as a fact.

The mental model

Every Telegram fan resolves to exactly one origin kind:

kindPrecisionMeaning
cidexactThe fan started the bot through a tracked deep link that carried a click id, and that click resolves to one of your domains. Bulletproof — but rare, because the everyday redirect into the bot carries no payload.
tg_matchapproxThe nearest preceding Telegram-bound click from the same country within a 5-minute window. This tier carries the bulk of the attribution.
organicapproxNeither signal — the honest residual bucket.

How the time-and-country match works

When a fan first writes to the bot, TEASE looks back 5 minutes from that first message at the human clicks that headed toward Telegram — the direct Telegram geo-redirects and the landing pages' Telegram buttons — from the same country. The nearest click preceding the first message wins, and its domain becomes the fan's origin. No qualifying click in the window (or no country on the fan) → the fan stays organic.

Why five minutes

Someone you just sent to Telegram writes immediately. Measured on live data (2130 fans with a country and a first message), the gap between the nearest same-country click and the first message falls out like this: under 10 s — 29, 10–60 s — 665, 1–5 min — 170, 5–30 min — 31, 30–60 min — 26, 1–6 h — 67, 6–24 h — 45. Past five minutes the count collapses to a handful and then rises again at 1–6 h and 6–24 h — that is no longer a behavioural tail but coincidence: the wider the window, the likelier someone in the same country clicked for unrelated reasons. The old 24-hour window credited those coincidences to a domain.

The width is tunable without a deploy — TG_MATCH_WINDOW_SEC (seconds) in the API environment. Default 300; garbage and non-positive values fall back to it. Widening the window buys more attribution at the cost of truthfulness.

The precision badge never lies

precision is exact only for a real cid first touch. A time-and-country match is a calculation, so it is always badged approx — and a match that cannot name a domain degrades to organic instead of inventing a source. Render the badge next to the money; never present an approx row as a measured fact.

Two honest lenses

The report deliberately mixes two windows, mirroring how the bot itself reports:

  • Revenue (money) — every sale inside the window, dated by the sale, attributed to the paying fan's resolved origin. An old fan (first message long before the window) who pays today is counted today. This drives summary.payers, summary.revenue_net_cents, the leaderboard money, countries, and the trend's revenue/payers.
  • Acquisition (funnel) — the arrival cohort: fans whose first message falls inside the window, walked through arrived → wrote 5+ messages → paid in-period. This drives summary.fans, funnel, per-source fans / msg5plus / nonpayers, and the trend's arrivals.

Why fans and payers don't reconcile against each other

summary.fans counts new arrivals while summary.payers counts everyone who paid in the window — old fans included. A source can therefore show real money with zero new arrivals (its old fans keep paying), and funnel.payers (payers among the arrivals) is always ≤ summary.payers.

Read the report

GET /api/admin/tg-attribution returns the whole report in one owner-scoped read. All money values are integer net cents; responses are briefly cached server-side.

curl "https://app.tease.link/api/admin/tg-attribution?period_from=1751328000&period_to=0" \
  -H "Authorization: Bearer $TEASE_API_KEY"
const res = await fetch(
  'https://app.tease.link/api/admin/tg-attribution?period_from=1751328000&period_to=0',
  { headers: { Authorization: `Bearer ${process.env.TEASE_API_KEY}` } },
);
const report = await res.json();
import os, requests

res = requests.get(
    "https://app.tease.link/api/admin/tg-attribution",
    headers={"Authorization": f"Bearer {os.environ['TEASE_API_KEY']}"},
    params={"period_from": 1751328000, "period_to": 0},
)
report = res.json()

Query parameters

FieldTypeRequiredDescription
period_fromintegernoWindow start, unix seconds. 0 = from the beginning.
period_tointegernoWindow end, unix seconds. 0 = up to now.

Response

{
  "summary": { "fans": 214, "payers": 57, "revenue_net_cents": 331900, "msg5plus": 122 },
  "funnel": { "arrived": 214, "msg5plus": 122, "payers": 31 },
  "leaderboard": [
    {
      "domain_source": "yourbrand.link",
      "source_label": "Main IG",
      "source_icon": "instagram",
      "kind": "tg_match",
      "fans": 96,
      "spenders": 24,
      "msg5plus": 61,
      "revenue_net_cents": 182400,
      "pct": 55.0,
      "countries": [
        { "country": "DE", "revenue_net_cents": 96100 },
        { "country": "AE", "revenue_net_cents": 51200 }
      ],
      "users": [
        { "fan_id": "7093412885", "username": "fan_handle", "country": "DE",
          "revenue_net_cents": 44900, "first_ts": 1752537600 }
      ],
      "nonpayers": [
        { "fan_id": "7093412901", "username": "quiet_fan", "country": "DE",
          "message_count": 18, "first_ts": 1752882000 }
      ]
    }
  ],
  "organic": {
    "domain_source": "", "source_label": "Органика (TG)", "source_icon": "",
    "kind": "organic", "fans": 84, "spenders": 21, "msg5plus": 40,
    "revenue_net_cents": 101800, "pct": 30.7,
    "countries": [], "users": [], "nonpayers": []
  },
  "countries": [
    { "country": "DE", "fans": 41, "spenders": 12,
      "revenue_net_cents": 121300, "msg10plus": 9 }
  ],
  "trend": [
    { "date": "2026-07-18", "arrivals": 24, "payers": 6, "revenue_net_cents": 21500 }
  ],
  "trend_unit": "day"
}

Top-level fields

FieldTypeDescription
summaryobjectThe headline: fans (arrivals in-window), payers (distinct fans with positive net in-window, old fans included), revenue_net_cents (net of all in-window sales), msg5plus (arrivals who wrote more than 5 messages).
funnelobjectThe arrival cohort's funnel: arrivedmsg5pluspayers (payers among the arrivals).
leaderboardarrayOne row per resolved source domain, sorted by revenue_net_cents descending. Only cid / tg_match fans with a domain make a row — everything else lands in organic.
organicobjectThe residual bucket, same row shape. Always present.
countriesarrayPer-country rollup across all sources — see below.
trendarrayTime series of arrivals, payers, revenue_net_cents per bucket.
trend_unitstringhour for windows up to 48 hours, else day. Buckets are UTC and range-filled (empty buckets are zeros) on bounded windows, so charts aren't sparse.

Source row fields

FieldTypeDescription
domain_sourcestringThe origin domain. Empty on the organic row.
source_labelstringFriendly source name — the same display name the revenue leaderboard uses.
source_iconstringBrand-icon key for the source, matching the revenue leaderboard; may be empty.
kindstringcid, tg_match, or organic — the row's precision class.
fansintegerNew arrivals resolved to this source in-window.
spendersintegerDistinct fans of this source with positive net in-window (old fans included).
msg5plusintegerArrivals of this source who wrote more than 5 messages.
revenue_net_centsintegerNet of this source's in-window sales.
pctfloatShare of the window's total Telegram net, 0–100.
countriesarrayThis source's money split by fan country: { country, revenue_net_cents }, sorted by money.
usersarrayThe paying fans behind the row (top 50 by in-window net): fan_id, username, country, revenue_net_cents, first_ts. Feed fan_id to the fan card.
nonpayersarrayArrivals of this source who didn't pay in-period (top 60, most-engaged first): fan_id, username, country, message_count, first_ts. Your best creative/targeting research list.

Country row fields

FieldTypeDescription
countrystringISO-3166 alpha-2 code (empty when unknown).
fansintegerArrivals from this country in-window.
spendersintegerDistinct payers from this country in-window.
revenue_net_centsintegerNet revenue from this country in-window.
msg10plusintegerArrivals from this country who wrote more than 10 messages.

A payer is positive net

A fan counts as a payer only when their in-window net is above zero — a lone refund doesn't make a payer. Sales are windowed by their sale timestamp, in UTC.

One fan's card

GET /api/admin/tg-fan/{user_id} opens a single Telegram fan — identity, resolved origin with the precision badge, lifetime totals, a per-day net series, the transaction list, and the purchased content. Returns 404 when the fan is unknown (or not yours — the read is tenant-scoped).

curl "https://app.tease.link/api/admin/tg-fan/7093412885" \
  -H "Authorization: Bearer $TEASE_API_KEY"
{
  "fan_id": "7093412885",
  "username": "fan_handle",
  "name": "Alex",
  "country": "DE",
  "origin": {
    "kind": "tg_match",
    "label": "TG-трафик yourbrand.link",
    "source": "yourbrand.link",
    "domain": "yourbrand.link",
    "country": "DE",
    "precision": "approx"
  },
  "total_net_cents": 44900,
  "purchases": 6,
  "message_count": 132,
  "closeness_level": "warm",
  "closeness_score": 62,
  "first_ts": 1752537600,
  "last_ts": 1753142400,
  "last_purchase_ts": 1753056000,
  "blocked": false,
  "summary_text": "…",
  "preferred_name": "Alex",
  "conversion": 3,
  "paywall_triggers": 2,
  "active_hours": [20, 21, 22],
  "by_day": [ { "day": "2026-07-18", "net_cents": 9900 } ],
  "transactions": [
    { "ts": 1753056000, "net_cents": 9900, "gross_cents": 12900, "kind": "content" }
  ],
  "purchased": [
    {
      "content_id": "c_412", "net_cents": 19800, "count": 2, "last_ts": 1753056000,
      "sale_type": "content", "description": "…", "tags": ["…"],
      "level": 2, "level_label": "…", "media_count": 4,
      "media_types": { "video": 1, "photo": 3 }, "in_catalog": true
    }
  ]
}

Card fields

FieldTypeDescription
fan_id / username / name / countrystringsIdentity as known to the bot.
originobjectThe resolved origin: kind, human label, source, domain, country, and the precision badge (exact / approx) — the contract above.
total_net_cents / purchasesintegersLifetime net and purchase count.
message_countintegerLifetime messages from the fan.
closeness_level / closeness_scorestring / integerThe bot's relationship gauge for the fan.
first_ts / last_ts / last_purchase_tsintegersFirst message, last message, last content purchase (unix seconds).
blockedbooleanWhether the fan blocked the bot.
summary_text / preferred_name / conversion / paywall_triggers / active_hoursmixedLive enrichment from the bot's own fan profile (chat summary, the name the fan prefers, funnel step, paywall hits, active hours 0–23). Fails safe: when the bot API is unreachable these come back empty and the card still renders from local data.
by_dayarrayPer-day net series: { day, net_cents }.
transactionsarrayNewest-first: { ts, net_cents, gross_cents, kind }.
purchasedarrayGallery of bought content (content purchases only — tips make no tile): totals per item plus catalog metadata (description, tags, level, level_label, media_count, media_types, in_catalog).

In the panel

The same report renders as the dashboard's «Telegram» mode — the funnel, the arrivals-and-payments trend, the source leaderboard (every fan chip opens the fan card above), and the country map. Each source row and each fan card carries the precision badge, so «точно» vs «расчёт» is always visible next to the money.

FAQ

Next steps

  • Attribution — the OnlyFans-side revenue leaderboard this report mirrors.
  • Landing pages — the geo-segments and direct redirects that route fans toward Telegram.
  • Smart Links — the trackable links behind every click this report matches against.

On this page