TikTok Ad Radar — New Creative Monitor avatar

TikTok Ad Radar — New Creative Monitor

Pricing

Pay per event

Go to Apify Store
TikTok Ad Radar — New Creative Monitor

TikTok Ad Radar — New Creative Monitor

Every TikTok ads actor is a one-shot dump: 100 creatives today, the same 100 tomorrow, billed again. This one keeps a durable memory of the Creative Center board you watch and charges only for creatives it has never shown you. Never twice for the same ad, never for a blocked check.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Watch a slice of the TikTok Creative Center Top Ads board (country + period, optionally industry, objective and ad language) and get told when a creative shows up in it that you have not been shown before. You pay for the check and for each newly observed ad — never twice for the same ad, and never for a failed check.

What "new" means here, precisely: newly observed in this monitored Top Ads window. The monitor uses a deterministic ranking (ctr by default, or impression) rather than the board's personalised "For You" feed, so the tracked window is stable between runs. An older creative can still enter the window when its ranking rises, and it is reported then. TikTok does not publish an ad's real first-appearance date, and this actor does not claim one.

This is not another bulk scraper. The TikTok ads actors reviewed while building this one are all one-shot pulls ("extract up to N ads per run"): they do not remember what they already showed you, so on a schedule you re-download — and re-pay for — the same creatives every single run. This one keeps durable state per watch.

How it works

  1. No third-party account needed. TikTok Creative Center renders its board client-side and its data endpoint answers no permission outside a real browser session, so a real anti-detect browser is required — this Actor uses a built-in stealth browser by default. You can still point it at your own remote browser (cdpUrl, e.g. a Bright Data Scraping Browser) if you prefer; the backend is never switched automatically and is reported in the run's coverage. Either way, this Actor only reads what the page loads for an ordinary visitor.
  2. Each watch is a filter slice: countryCode (+ optional period, industryId, objectiveId, adLanguage). Its watchId owns an independent, durable history.
  3. Every run opens the board for that slice, collects up to 100 ads (the source's own hard ceiling: 5 pages × 20), and diffs them against the ads already seen for that watch.
  4. Ads not previously observed for that watch go to the dataset and are billed once each. A quiet check — nothing newly observed — costs only the check event.

The first run of a new watchId is a free baseline: it records what is currently on the board and bills no new-ad-detected events at all. "New" starts from your second run.

Input

{
"monitorId": "my-tiktok-radar",
"watches": [
{ "watchId": "us-30d", "countryCode": "US", "period": 30 },
{ "watchId": "gb-beauty-7d", "countryCode": "GB", "period": 7, "industryId": "label_25000000000" }
],
"notifyOn": "new_ads",
"webhookUrl": "https://example.com/webhook"
}
FieldMeaning
monitorIdName of the durable history. Keep it stable across scheduled runs.
cdpUrlOptional. Leave empty to use the built-in stealth browser. If set, stored as a secret and never logged.
watches[].watchIdIndependent history key for one filter slice.
watches[].countryCodeTwo-letter country, e.g. US. Required.
watches[].period7, 30 or 180 days. Default 30.
watches[].industryId / objectiveId / adLanguageOptional narrowing filters.
watches[].orderByctr (default), impression (Reach), or for_you. Prefer the first two: they are deterministic.
watches[].maxPages15. Default 5 (up to 100 ads — the source's ceiling).
notifyOnnew_ads (default), always, never.
webhookUrlOptional HTTPS endpoint receiving a JSON digest of the paid new ads.

Add more watches later under the same monitorId — each keeps its own history. Changing a filter of an existing watchId is rejected (watch_config_mismatch, before any network request): a different filter is a different board, and silently mixing the two histories would hide real results from you. Use a new watchId instead.

Output

One dataset row per new ad:

watchId, adId, title, brand, ctr, cost, likes, industryKey, objectiveKey, videoId, durationSec, coverUrl, videoUrl, countryCode, period, orderBy, monitorId, runId, discoveredAt, eventId, billed.

The key-value store also holds coverage (requested / attempted / delivered / billed) and digest.

Billing

Two events:

  • target-check-confirmed — once per successful check of a watch, including the free baseline. It is charged only after the board actually answered with a valid, recognised payload; a timeout, a block, or a changed page structure charges nothing. This event covers the real cost of driving a browser.
  • new-ad-detected — once per ad newly observed in that watch's window since its previous check.

Delivery guarantee for new-ad-detected: at-most-once (not exactly-once)

You will never be charged twice for the same ad. The right to write an ad row and to charge for it is granted by a single atomic primitive — one addRequest(uniqueKey) into a dedicated, named claim-journal Request Queue (<prefix>-<monitorId>-claims). Exactly one run ever wins that key. Claim requests are never deleted and never handled: the queue is a permanent journal of irreversible attempts, not a work list.

Stated honestly, including the part that changed:

  • It is not exactly-once, and a crash can now lose an ad. If a run wins the claim and then dies before the row reaches the dataset (or before the charge completes), that ad is lost: it closes as dataset_unknown / charge_unknown and is never re-delivered. Earlier builds replayed such an ad from a payload stored inside the claim; that replay has been removed on purpose, because it could not be distinguished from a second live run holding the same claim — and the only safe resolution of that ambiguity is to refuse. We deliberately prefer losing a delivery over double-charging you.
  • target-check-confirmed is deliberately outside this gate. It is not deduplicated between runs: a repeated successful check is real work actually performed (a real browser session), not a duplicate.
  • Boundary of the guarantee: it holds for as long as the named claim-journal queue exists. Anyone with account access can delete or re-create that queue through the Apify Console/API; a fresh journal starts empty, and previously delivered ads could then be delivered and billed again. That is an inherent limit of any durable storage, not a defect of the protocol.
  • Migration boundary: the guarantee applies from the build that introduced the claim gate onward. Older builds of this actor must not keep running against the same monitorId — they predate the journal and would not see the claims it holds.
  • coverage.claimJournalSize reports the journal's size each run (best-effort; null if the queue's metadata could not be read, and the value lags by a few seconds because Apify's totalRequestCount is eventually consistent). Use it to watch growth, not to make decisions.
  • No storage rename here. Unlike the other actors in this wave, this one's storage names already fit Apify's 63-character limit exactly, so nothing was renamed: your existing history, checkpoints and dedup carry over untouched and there is no re-baseline.

Price vs. the alternatives

target-check-confirmed $0.010 · new-ad-detected $0.006.

Honest comparison for the same job — monitoring one US slice daily for a month, with ~5 newly observed creatives a day (30 checks, 150 new ads; a bulk scraper would re-download all 100 board ads on every one of those 30 runs):

cost / monthkeeps state?tells you what's new?
Bulk scraper at $3.00 / 1,000≈ $9.00nono
Bulk scraper at $1.50 / 1,000≈ $4.50nono
Bulk scraper at $0.49 / 1,000≈ $1.47nono
TikTok Ad Radar (30 checks + 150 new ads)≈ $1.20yesyes
Cheapest bulk dumper at $0.01 / 1,000≈ $0.03nono

Being straight about the last row: one bulk actor is cheaper in absolute dollars than anything short of free. What it gives you is 100 rows a day, all of them again every day, with no idea which ones you have already seen — you still have to build and host the diffing, the history and the alerting yourself. This actor sells that part, not the rows.

Limits, honestly

  • The source caps a filter slice at 100 ads (5 pages × 20). This actor monitors the top-100 window of your slice, not "every TikTok ad ever". If your run hits the ceiling with more pages still available, the run is reported as partial with reason window_truncated.
  • Ranking. By default the monitor sorts by ctr (or impression), which is deterministic — unlike the board's personalised for_you feed, which rotates its window between runs. If you deliberately pick for_you, expect ads to drop out and come back; they are reported again as present, but never billed a second time — a durable per-ad receipt, not the recent-IDs index, decides what has already been paid for. Either way, an older creative entering your window for the first time is reported (and billed) as newly observed: that is the honest meaning of the signal.
  • Only what the public board shows an ordinary visitor is read; no internal API is called directly.
  • One run at a time per monitorId (durable lease). A second concurrent run exits with lease_busy instead of corrupting the history. Every read-modify-write of the monitor's state happens inside a short atomic lock, so a stalled run that wakes up late cannot overwrite state another run has already closed.

Author: OmniCoder (https://t.me/OmniCoder)