How tgadsspy Works: Technical Deep Dive into the Classifier and Ingest Pipeline
Technical documentation of the tgadsspy data pipeline — data collection, niche classification architecture (regex + weights), geo classifier 3-step pipeline, media mirror SHA256 content-addressed storage, and aggregation caching. For developers, researchers and compliance teams.
Contents
- Purpose and audience
- 1. Data source
- 2. Channel pool and tiering
- 3. Creative deduplication
- 4. Niche classification
- 5. Geo classification
- 6. Media mirror
- 7. Advertiser extraction
- 8. Aggregation caching
- 9. Discover-similar BFS spider
- 10. Data completeness and known gaps
- 11. Data licensing and citation
- Related documentation
Purpose and audience#
This document is a technical deep-dive into how Telegram Ads Spy collects, classifies, and serves Telegram ad data. It supplements the overview at /about with implementation-level detail. Primary audience: developers building on the public API, researchers who need to understand data provenance for citation, and compliance teams assessing the system's OSINT methodology.
A shorter overview methodology is published at /about. This document covers the how in depth: API integration, classification logic, storage architecture, and caching.
1. Data source#
All ad data in Telegram Ads Spy originates from a single source: our automated collection pipeline. The collection layer works on top of Telegram's API and returns structured data. Telegram Ads Spy relies on this single pipeline — no scraping.
What we collect#
- Sponsored messages in the channels we track — text, CTA button and link, media, and the advertiser's target. Media links are temporary (1-hour lifetime), so we mirror them to permanent storage.
- Channel metadata — title, username, description, member count and avatar.
- Channel search — used for discovery with 47 rotating seed queries, and by
/api/v1/submitwhen a user submits a t.me link. - Telegram's similar-channels recommendations — used to grow the channel pool.
Multi-region collection#
Telegram has multiple data-centre clusters (DC1–DC5). Sponsored messages are region-specific: a channel in DC2 may show different sponsored messages than the same channel viewed from DC4. Telegram Ads Spy performs multi-region ingest for high-tier channels: the same channel is checked from multiple regions to increase creative coverage. This is the mechanism behind "seen in multiple geos" — different DCs serve different advertiser targeting.
2. Channel pool and tiering#
The ingest pipeline operates on a pool of ~9,000+ channels. Channels are tiered by member count, which determines ingest frequency:
| Tier | Member count | Ingest interval | Rationale |
|---|---|---|---|
| S (Super) | 1M+ | 30 minutes | High ad density, fast creative turnover |
| A | 100k–1M | 2 hours | Active market, moderate turnover |
| B | 10k–100k | 8 hours | Adequate for daily coverage |
| C | 1k–10k | 72 hours | Low ad density, spot-checking sufficient |
| Placeholder | Unknown | Not ingested | Awaiting resolver to hydrate |
Every 15 minutes, a metadata job picks up placeholder channels — those submitted via seed batches, user submission, or discovery — and fills in their member count, title and avatar. Once resolved, the channel is tiered and added to the ingest queue.
3. Creative deduplication#
When the ingest cron receives sponsored messages for a channel, deduplication happens before any storage:
creative fingerprint = sha256(title + text + ctaUrl + ctaLabel + accentColor)
A creative is considered new only if its fingerprint hasn't been seen before. This means:
- The same ad running in 100 channels produces one
AdCreativerecord (not 100) - Each appearance in a channel produces one
SponsoredImpressionrecord pointing to theAdCreative - Minor variations (different
accentColorwith same text) are treated as different creatives — intentional, as colour variants are used in A/B testing
Creative lifecycle: A creative is first seen when its fingerprint first appears. It's considered "active" while it continues appearing in new ingest ticks. AdCreative.lastSeenAt is updated on each new impression. When a creative stops appearing, it transitions to inactive naturally — there is no explicit deactivation signal.
4. Niche classification#
Every AdCreative is classified into one or more niches. The classifier is a weighted keyword-plus-brand-detection system implemented in lib/niche.ts.
Architecture#
The classifier operates on the concatenated text of title + text + ctaUrl. It runs two passes:
Pass 1: Brand detection A lookup against a dictionary of ~400 known advertiser brands, mapped to their primary niche. Example entries:
binance→crypto1xbet→gamblingnordvpn→vpnexness→forexdream11→betting
Brand matches carry high weight (w=10) and dominate the classification when present. The brand dictionary is maintained in lib/niche-brands.ts and updated as new advertisers are identified.
Pass 2: Keyword scoring For each niche, a set of regex patterns is evaluated against the creative text. Each match adds a positive weight to that niche's score. Patterns are designed to avoid false positives through:
- Specificity: "USDT P2P exchange" is a crypto signal; "exchange" alone is too generic
- Negation rules: Some patterns carry negative weight to suppress false positives (e.g., "slot" appearing in a tech context)
- Language variants: Patterns include Arabic, Russian, Indonesian, Thai and other language variants for the major niches
Score threshold: A niche is assigned if its score exceeds a minimum threshold. Multiple niches can be assigned — a creative can be gambling + crypto if it's a crypto-casino (e.g., BC.Game).
Niche taxonomy#
Current top-level niches (as of April 2026):
crypto, trading, forex, fintech, gambling (casino), betting (sports), vpn, dating, news, education, gaming, retail, tech, bots, adult, signals, remittance, ai, other
Sub-niches are assigned within the niche-meta.ts taxonomy for display grouping. The classification schema is append-only — niches are never removed, only new ones added.
Classification accuracy#
We validate accuracy through:
- Spot-check sampling: periodic manual review of 50 random creatives per niche
- Brand-miss audit: if a known brand is misclassified, the brand dictionary is updated
- False positive rate: estimated at ~4% based on last sampling round (January 2026)
Known limitations:
- Short text-only creatives with no brand signal have ~12% misclassification rate
- New brands not yet in the dictionary are initially classified by keyword only
- Multilingual edge cases (mixed script creatives) occasionally confuse the keyword scorer
5. Geo classification#
Every creative receives a geo assignment (ISO 3166-1 alpha-2 country code or regional code). The geo classifier is a 3-step cascade:
Step 1: CTA URL TLD analysis The CTA URL's top-level domain is parsed:
.ru→ RU.com.br,.br→ BR.pk→ PK.sa→ SA.eualone is ambiguous (treated as EU rather than a specific country)
Country-code TLDs provide high-confidence geo signals. If Step 1 produces a non-ambiguous result, the cascade stops.
Step 2: Language detection on creative text
If Step 1 is ambiguous (e.g., .com domain), the creative text language is detected using Unicode block analysis and a fastText-family language identifier:
- Arabic script → AR (regional)
- Cyrillic → RU/CIS (default RU unless Step 3 disambiguates)
- Devanagari → HI (India likely)
- Hangul → KR
- Hiragana/Katakana → JP
- Thai → TH
- Bengali → BD
- Urdu (Arabic script + language model) → PK
Step 3: Channel-level geo aggregation
The channel in which the creative appeared has its own geo signal (from language, name, description, and prior ingest history). When a creative appears in channels with consistent geo signals, the creative inherits that geo. For example, a .com domain English creative that appears predominantly in Russian-language channels is classified as RU.
Multiple geo assignment: A creative can have multiple geo codes when it demonstrably targets multiple markets (common for Binance global campaigns). In the UI, multi-geo creatives appear in all relevant geo filter segments.
6. Media mirror#
Banner media arrives as signed URLs to Telegram's media CDN with a 1-hour TTL. These URLs expire and become inaccessible, making them unsuitable for permanent archiving.
The Telegram Ads Spy-media-mirror cron (runs every 5 minutes) processes newly ingested creatives with unmirrored media:
- Fetch: HTTP GET to the temporary signed media URL
- Hash: SHA-256 of the raw binary content
- Store: Write to
/var/www/tgadsspy-media/<prefix>/<sha256-hex>.<ext>on the serverprefix= first 2 hex characters of the SHA256 (256-bucket directory sharding)ext= inferred from Content-Type header
- Update:
AdCreative.mediaUrlis updated from the temporary media URL to/m/<prefix>/<sha256-hex>.<ext>
The nginx alias serves /m/ paths from the media storage directory with:
Cache-Control: public, immutable, max-age=31536000
One-year immutable cache — content-addressed storage guarantees the hash never changes.
Deduplication: Two different ads using the same banner image produce a single stored file (same hash). The file is stored once; both creatives reference the same /m/... URL.
Fallback: For creatives that arrive without a banner (text-only or channel-pic format), a secondary nightly cron (Telegram Ads Spy-creative-media) fetches og:image from the CTA URL domain as a fallback thumbnail. A third fallback uses the target channel's avatar URL.
7. Advertiser extraction#
Advertiser identity is derived from the CTA URL, not from any Telegram-provided field:
Domain advertiser: if ctaUrl is an external URL, the registered domain (e.g., binance.com from https://www.binance.com/en/referral?...) becomes an Advertiser record with type: domain. The full URL (including UTM parameters and referral codes) is preserved on the AdCreative record for competitive analysis.
Telegram advertiser: if ctaUrl is a t.me/<username> URL, the username becomes an Advertiser record with type: telegram. The channel is also added to the discovery queue if not already tracked.
Advertiser slug: a normalized version of the domain or username — lowercase, special characters stripped, used in /advertisers/<slug> URLs. Slugs are stable once assigned.
Alias merging: The same entity may advertise from multiple domains (e.g., binance.com and binance.cc). Manual alias merges are supported in the admin interface, consolidating creative counts under a canonical advertiser record.
8. Aggregation caching#
Two Redis keys are pre-warmed every minute by the Telegram Ads Spy-warm-cache cron:
Telegram Ads Spy:home:agg (TTL 120s)
Contains: total creative count, total advertiser count, total channel count, top niches (name + count), recent 20 creatives (thumbnail + title), today's new creative count, today's new advertiser count. Used by the home page dashboard and the /api/v1/stats endpoint. Cold miss on this key would cause the home page to hit the database directly — the warm-cache cron ensures this never happens in production.
Telegram Ads Spy:pool:stats (TTL 600s)
Contains: channel count by tier, total sponsored-eligible count, countries represented. Used by the OG image generator (the home page's dynamic Open Graph image includes live stats — must be fast to serve in the 100ms og:image timeout).
Per-entity caches: Individual channel, advertiser, and niche pages cache their aggregated stats at /api/v1/channels/<id>, etc., with TTL of 60–300s depending on update frequency expectations.
9. Discover-similar BFS spider#
In addition to the seed-based discovery cron, Telegram Ads Spy runs a continuous BFS (breadth-first search) spider using Telegram's "similar channels" graph:
- Anchor selection:
Channel.lastSimilarCheckAt IS NULL OR < NOW()-1h— channels that haven't been checked for similarities in the past hour, ordered ascending (oldest check first, new channels prioritised by default NULL value) - Batch size: 30 channels per tick (every minute in-process)
- Recommendation lookup: Telegram's similar-channels recommendations for the anchor channel → returns 10–20 similar channels
- New channel handling: similar channels not in the pool are added as placeholders → resolver picks them up in the next 15-minute cycle
- Bot filter: channels with
memberCount < 100or names matching bot-name patterns are discarded - Rate limiting: anchor cooldown of 1 hour prevents the same channel from being re-spidered more than once per hour, regardless of how many other channels reference it as similar
This recursive BFS, combined with the multi-query discovery job, is how the channel pool has grown from seed lists of a few hundred to 9,000+ channels organically.
10. Data completeness and known gaps#
What we capture well:
- Sponsored messages on the Telegram Ads Platform from both the EUR cabinet and the self-serve Gram cabinet (formerly the TON cabinet), told apart by the
can_reportflag (high coverage via multi-region ingest)
Known gaps:
- Group-level advertising: Telegram groups and supergroups are not indexed (sponsored messages only run in channels; paid posts in groups are outside our scope)
- Bot-to-user messages: Advertisers who send promotional messages directly to users via bots are not captured — we only see channel-level placements
- Inline bot results: Telegram's inline query ads (rare) are not captured
- Very new channels: Channels created recently that haven't been discovered by any pipeline path may be missed for days or weeks
- Low-tier channels (< 1k subscribers): Not eligible for sponsored messages; paid posts sold directly in very small channels are not in our scope
Coverage estimate: For EUR-cabinet sponsored messages, coverage is estimated at 65–75% of all unique creatives that ran in the period. The gap represents channels not yet in our pool that received sponsored message deliveries. This estimate is based on cross-referencing our creative counts against aggregate stats from our data source for channels we do track.
11. Data licensing and citation#
All data in Telegram Ads Spy's archive is released under CC-BY-4.0. You may use, republish, and analyse it for any purpose with attribution:
Source: tgadsspy.com · tgadsspy.com/blog/tgadsspy-classifier-pipeline-technical-deep-dive · CC-BY-4.0
For programmatic access: public API documentation · bulk CSV export.
For bug reports or data correction requests: open an issue at the GitHub repo or email [email protected].
Related documentation#
- /about — non-technical methodology overview
- Public API docs — endpoint reference for developers
- State of Telegram Ads 2026 — what the pipeline has collected
- Regulation Guide 2026 — how regulators can use this data
Also available in:
Cite this article
tgadsspy research (2026). How tgadsspy Works: Technical Deep Dive into the Classifier and Ingest Pipeline. tgadsspy.com. Retrieved from https://tgadsspy.com/blog/tgadsspy-classifier-pipeline-technical-deep-dive
Licensed CC-BY-4.0 — reuse allowed including commercial, attribution required.