I run a visa agency in Bali. Scrapers are the other half of my week, and the only part of that work I trust is the part I have run more than once. So when my TikTok Shop scraper started getting a daily health check with a fixed input, I let the results pile up instead of deleting them.
Thirteen runs, 17–28 October 2026, byte-identical input every time:
{ "searchQueries": ["air fryer"], "country": "US", "maxProductsPerSource": 30, "sortBy": "relevance", "productDetails": true } 390 product rows. 154 distinct products. Zero products present in all thirteen runs. 76 of the 154 showed up exactly once and never again.
That is not a scraper fault, and it is not TikTok being broken. It is what a shopping search actually is, measured. Below is the breakdown, because every number in it changes how you should schedule a job like this.
TikTok's keyword search answered with 10 products, except when it answered with 30
Each product row carries a foundVia field — which path reached it. Grouping the runs by that field splits them cleanly in two:
run (UTC) from the keyword search itself 2026-10-17 00:45 10 2026-10-18 00:39 30 2026-10-19 00:27 10 2026-10-20 00:23 10 2026-10-21 00:29 10 2026-10-22 00:28 30 2026-10-22 04:30 10 2026-10-22 10:31 30 2026-10-23 00:29 30 2026-10-24 00:28 30 2026-10-26 00:57 10 2026-10-27 00:20 10 2026-10-28 00:33 10 Eight runs of thirteen got exactly 10 products out of air fryer and no more. Five got the full 30. Three of those runs happened on the same day, 22 October, four hours apart: 30 products at 00:28, 10 at 04:30, 30 again at 10:31.
The 10-product answer is almost perfectly deterministic. On seven of those eight runs the ten product IDs came back in the same order, down to the last position. The 30-product answer is the opposite — it is a different catalogue every time. Overlap between a 10-run and a 30-run was as low as 0 products out of 30:
10-19 10-20 10-22T00:28 10-22T10:31 10-19 (10) 30 18 0 0 10-20 (10) 18 30 0 0 Two runs, same keyword, same country, nothing in common. Take either one as "the air fryer market on TikTok Shop" and you are looking at two disjoint markets.
My working read: a popular keyword has a small cached head that any cold client gets instantly, plus a deeper list that is only sometimes served. I cannot see TikTok's side, so that stays a hypothesis — but the shape of it repeated eight times.
The rows that fill the gap come from different keywords every night
When the keyword runs dry before your limit, my Actor widens to TikTok's own related searches rather than returning 10 rows and calling it a day. That design decision is visible in foundVia, and it is also where the churn comes from:
10-17 related search: air fryers 20 rows 10-19 air fryers on sale 6 + air fryer paper liners 14 10-21 air fryers on sale 20 10-22 air fryers on sale 6 + paper liners 9 + silicone liner 5 10-27 air fryer liners 4 + airfryer 13 + air fryers recipes 3 10-28 air fryer paper liners 20 TikTok's related-search block is itself a moving target, so two thirds of a 30-row dataset can be sourced from a keyword that did not exist in yesterday's run. air fryers recipes is not the same market as air fryer.
Practical consequence: if you want comparable daily numbers, filter to foundVia == "search" and accept 10 rows, or stop using keywords for the job.
And position is not a rank you can diff. The same dual-basket air fryer came back at position 1 on nine runs and position 26 on two of them — the runs where the search returned 30 and the cached head landed at the end of the response.
Prices moved every single night, on one shared multiplier
34 of the 52 products I saw three or more times changed price between runs. Median spread between a product's cheapest and dearest reading: 14.8%. Eleven products moved more than 20%.
Here is one, seen all eleven days, with units sold frozen at 30 the whole time:
10-17 $8.62 10-22 $9.49 10-18 $9.27 10-23 $8.50 10-19 $9.46 10-24 $9.58 10-20 $8.81 10-26 $9.24 10-21 $8.93 10-27 $8.59 10-28 $9.13 No trend, no sale label, no discount field set. What makes it interesting is the variant ladder underneath. On 23 October its three variants were $17.01 / $8.50 / $8.50; on 24 October, $19.18 / $9.58 / $9.58. Every variant multiplied by 1.1271, 1.1276 — the same factor to four decimals. Across the whole eleven days I never saw one SKU move on its own; the ladder always scaled together.
So this is not a seller editing prices, and it is not my parser picking a different variant. Something upstream applies a daily multiplier to the whole product. If you are tracking competitor prices on TikTok Shop, that is the noise floor you are working against: roughly ±7% a night on a $9 item, invisible in any discount field.
Which is how "estimated revenue" tripled on a product that sold nothing
estimatedRevenue in my output is soldCount × price — lifetime units times today's price. I document it as an estimate. Eleven days of the same product shows exactly how rough it is:
soldCount price estimatedRevenue variants 10-17 1748 $21.99 $38,439 1 10-18 1748 $6.99 $12,219 9 10-19 1748 $9.09 $15,889 9 … 10-28 1749 $9.09 $15,898 9 One unit sold in eleven days, and a lifetime GMV estimate that ranged from $12,219 to $38,439 — a 3.1× spread. The 17 October reading is the odd one: the page served a single Default variant at $21.99 with 9,879 in stock, and from 18 October on it served nine named variants at $6.99–$20.79 with 9,000. Either the seller restructured the SKUs that night or the page occasionally serves a stripped version; one reading cannot tell you which, which is the whole argument for reading twice.
Two honest rules fall out of this. Never quote estimatedRevenue from a single run. And if you do use it, store price and soldCount next to it so you can tell a sales move from a price move.
What did not budge
The pessimistic half of this post is the useful half, but the stable fields deserve naming too.
-
soldCountnever went backwards. 52 of 52 recurring products, 13 runs: non-decreasing, every time. The deltas are small and believable — one fryer went 527 → 546 over eleven days. -
productIdis a real join key. Titles get rewritten mid-week (the same fryer was "9.5L Dual Basket Airfryer for Family" and later "9.5L Aluminum Airfryer with Dual Baskets"), so key on the ID, never the title. - Variant stock, shipping and shop stats were populated on every run — the fields I expected to go missing under load did not.
- The 7-day sales figure matched my own measurement. Ten products appeared in both the 21 October and the 28 October run — exactly seven days apart. For the four of them covered by the Actor's shared sales history,
sold7don the 28th was 0, 5, 0 and 7. My own observed deltas across those two runs: 0, 5, 0 and 7. Four of four, to the unit. -
sold30dwas null on all 390 rows. The shared history started on 7 October, so 30-day windows only fill from 6 November. A null there is an honest "not yet", and I would rather ship that than a number computed from 21 days.
Coverage of that history on a random keyword was 9–17 tracked products per 30-row run. The rest arrive with nulls in the 7/30-day fields and fill in from your own second run instead.
Running it
30 rows with full product pages costs $0.108 in Actor charges; the last run took 180 seconds and $0.0099 of platform usage on top. No login, no cookies, no proxy setup.
curl -X POST "https://api.apify.com/v2/acts/lergassy~tiktok-shop-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'content-type: application/json' \ -d '{"searchQueries":["air fryer"],"country":"US","maxProductsPerSource":30,"productDetails":true}' The Actor is TikTok Shop Scraper. Filter type != "notice" — the run summary arrives as a free row.
When not to use a keyword input at all: market sizing, daily price tracking, anything where yesterday's rows have to be comparable to today's. Eight of my thirteen runs could only see ten products, and the five that saw thirty saw thirty different ones. Point the same Actor at a category link, a store link or the best-seller ranking and you get a list TikTok actually maintains — and then diffs between runs mean something.
What I changed about my own habits after counting this: schedule the job daily, union the rows on productId over a week, and treat a single run as a sample rather than an inventory. Eleven days of this keyword gave me 154 products. Any one morning gave me 30, and told me nothing about the other 124.

