DEV Community

Cover image for I Built a Vinted Monitor That Tracks What Disappears, Not Just What Exists
KazKN
KazKN

Posted on

I Built a Vinted Monitor That Tracks What Disappears, Not Just What Exists

A normal Vinted search tells you what exists right now.

A reseller usually needs the opposite:

What changed since yesterday?

That sounds like a small difference, but it changes the whole scraper design.

One-off exports are easy. Scheduled market monitoring is not. You need stable inputs, a persistent state store, clean diffs, cost caps, and a way to avoid calling every missing item "sold" when Vinted simply changed ranking or hid a page.

I ran this live on Apify on June 4, 2026 with Vinted Smart Scraper.

Disclosure: I built this Actor. Some Apify links may use my referral code.

πŸ“Š The live baseline

Before changing the article, I pulled the real public Store and run data.

Metric Value
Actor kazkn/vinted-smart-scraper
Actor ID 4UAPrwZb1XOlkCJKK
Supported Vinted markets 26
Countries per run cap 5
Total public runs 108,367
Total users 464
Users, 7 days 31
Users, 30 days 97
Runs, 30 days 7,392
Succeeded runs, 30 days 7,166
Rating 5.0 / 5
Reviews 4

Current Free-tier event pricing at the time of the test:

$0.020 per Actor start
$0.002 per dataset result
Enter fullscreen mode Exit fullscreen mode

So the billing guardrail is simple:

estimated charge = actor start + emitted rows * result price
Enter fullscreen mode Exit fullscreen mode

❌ The wrong abstraction

My first mistake was thinking like this:

"Users need a Vinted scraper."

That is too broad.

Resellers do not only need raw listings. They need a repeatable signal.

❌ One-time scraper βœ… Market monitor
Export one CSV Run the same input every day
Count active listings Compare snapshots
Trust hidden sold pages Track missing, likely_sold, reappeared
Pull everything Cap rows with maxItems
Pay for noisy rows Emit only changes after baseline
Debug manually Send summary rows to CSV/API/webhook

That is why the most important mode is not the normal SEARCH.

It is SELL_THROUGH_TRACKER.

πŸ§ͺ Run 1: normal search

I started with a tiny real search:

{
  "mode": "SEARCH",
  "query": "nike air force 1",
  "countries": ["fr"],
  "condition": ["very_good", "good"],
  "priceMin": 20,
  "priceMax": 120,
  "sortBy": "newest_first",
  "maxItems": 8,
  "includePhotos": false,
  "includeSellerDetails": false
}
Enter fullscreen mode Exit fullscreen mode

Result:

Field Value
Run ID 7BHS0mBNeN4faoDZk
Dataset ID DQGMfYNvIQabnV2wF
Status SUCCEEDED
Duration 10s
Rows emitted 8
Total available in Vinted response 960
Free-tier estimated charge $0.036

Sample output:

{
  "id": 9089776821,
  "title": "Nike air force 1 shadow unisex numero 45",
  "url": "https://www.vinted.fr/items/9089776821-nike-air-force-1-shadow-unisex-numero-45",
  "price": 30,
  "currency": "EUR",
  "brand": "Air Force",
  "size": "45",
  "condition": "Bon Γ©tat",
  "country": "fr",
  "seller": {
    "id": 3160438769,
    "username": "eeeeeeed1"
  }
}
Enter fullscreen mode Exit fullscreen mode

That is useful, but it is still just a snapshot.

The more interesting question is:

What happens if I run the same thing again?

βœ… Run 2: build a sell-through baseline

Here is the baseline input:

{
  "mode": "SELL_THROUGH_TRACKER",
  "query": "nike air force 1",
  "countries": ["fr"],
  "condition": ["very_good", "good"],
  "priceMin": 20,
  "priceMax": 120,
  "sortBy": "newest_first",
  "maxItems": 8,
  "trackingStoreName": "devto-series-sell-through-2026-06-04",
  "trackerId": "devto-nike-air-force-fr-2026-06-04",
  "missingRunsThreshold": 2,
  "emitActiveItems": true,
  "emitRunSummary": true
}
Enter fullscreen mode Exit fullscreen mode

The important fields:

Field Why it matters
trackingStoreName Keeps state across scheduled runs
trackerId Stable ID for this monitor
missingRunsThreshold Reduces false sold signals
emitActiveItems Full baseline first, lighter output later
emitRunSummary Adds one daily summary row for automations

Live result:

Field Value
Run ID gCd8a3kk4AC9yJWpI
Dataset ID 0QbscZ6GRdhmOlSqx
Status SUCCEEDED
Duration 6s
Rows emitted 9
Item rows 8
Summary rows 1
Free-tier estimated charge $0.038

The summary row is what turns the Actor into a daily monitor:

{
  "recordType": "SELL_THROUGH_DAILY_SUMMARY",
  "trackerId": "devto-nike-air-force-fr-2026-06-04",
  "isBaseline": true,
  "scheduleReady": true,
  "observedItems": 8,
  "outputItemRecords": 8,
  "totalTrackedItems": 8,
  "newItems": 8,
  "activeItems": 8,
  "missingItems": 0,
  "likelySoldItems": 0,
  "explicitSoldItems": 0,
  "sellThroughRate": 0,
  "avgPrice": 35,
  "topBrands": [
    { "brand": "Air Force", "count": 5 },
    { "brand": "Nike", "count": 3 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The baseline does not prove anything sold yet.

It creates the memory needed for the next run.

πŸ” Run 3: same tracker, lighter scheduled output

For the second run, I kept the same trackingStoreName and trackerId, but changed one thing:

{
  "emitActiveItems": false
}
Enter fullscreen mode Exit fullscreen mode

Full input:

{
  "mode": "SELL_THROUGH_TRACKER",
  "query": "nike air force 1",
  "countries": ["fr"],
  "condition": ["very_good", "good"],
  "priceMin": 20,
  "priceMax": 120,
  "sortBy": "newest_first",
  "maxItems": 8,
  "trackingStoreName": "devto-series-sell-through-2026-06-04",
  "trackerId": "devto-nike-air-force-fr-2026-06-04",
  "missingRunsThreshold": 2,
  "emitActiveItems": false,
  "emitRunSummary": true
}
Enter fullscreen mode Exit fullscreen mode

Live result:

Field Value
Run ID sbCksMapWGd5GBG9O
Dataset ID 55gRHqbUbiqnmT6O3
Status SUCCEEDED
Duration 5s
Rows emitted 2
Item rows 1
Summary rows 1
Free-tier estimated charge $0.024

That second run found:

{
  "recordType": "SELL_THROUGH_DAILY_SUMMARY",
  "isBaseline": false,
  "observedItems": 8,
  "outputItemRecords": 1,
  "totalTrackedItems": 9,
  "newItems": 1,
  "activeItems": 8,
  "missingItems": 1,
  "likelySoldItems": 0,
  "explicitSoldItems": 0,
  "newlyMissingItems": 1,
  "dailySellThroughSignals": 0,
  "avgPrice": 34.38
}
Enter fullscreen mode Exit fullscreen mode

One item was not observed on the second run:

{
  "recordType": "SELL_THROUGH_ITEM",
  "itemId": 9089774198,
  "title": "Nike Air Pegasus Volt Green Neon - EUR42 - HQ5403-700",
  "trackingStatus": "missing",
  "changeType": "missing",
  "confidence": "low",
  "lastSeenPrice": 39.99,
  "missingRuns": 1,
  "missingRunsThreshold": 2
}
Enter fullscreen mode Exit fullscreen mode

Notice the important part:

missingRuns: 1
missingRunsThreshold: 2
confidence: low
likelySoldItems: 0
Enter fullscreen mode Exit fullscreen mode

The Actor did not call this sold after one missing run.

That is intentional.

🧠 Why I do not trust sold pages blindly

I also ran SOLD_ITEMS on the same query:

{
  "mode": "SOLD_ITEMS",
  "query": "nike air force 1",
  "countries": ["fr"],
  "maxItems": 5
}
Enter fullscreen mode Exit fullscreen mode

Live result:

Field Value
Run ID wvFbmO0HscQits0UV
Status SUCCEEDED
Rows emitted 0
Free-tier estimated charge $0.020

The log showed the reason:

Sold-items endpoint variants returned active listings only.
Returning no rows instead of false sold items.
Enter fullscreen mode Exit fullscreen mode

That is exactly the kind of boring defensive behavior I want.

Situation Bad scraper behavior Safer behavior
Vinted returns active listings from a sold endpoint ❌ Mark active items as sold βœ… Return zero rows
Item disappears for one run ❌ Claim it sold βœ… Mark missing, low confidence
Item stays missing across runs βœ… Increase confidence βœ… Use missingRunsThreshold

πŸ–ΌοΈ Snapshot diff visual

Snapshot diff workflow

The mental model is:

baseline snapshot
       ↓
scheduled snapshot
       ↓
diff engine
       ↓
daily summary row + item-level changes
Enter fullscreen mode Exit fullscreen mode

For a webhook or Telegram bot, the useful object is not every listing.

It is the summary:

{
  "telegramText": "Vinted sell-through devto-nike-air-force-fr-2026-06-04: 8 observed, 1 first seen, likely sold: 0, sold: 0, reappeared: 0."
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Έ Cost guardrails

With current Free-tier event pricing:

start = $0.020
row = $0.002
Enter fullscreen mode Exit fullscreen mode

The test charges would be:

Run Rows Estimated Free-tier charge
Search snapshot 8 $0.036
Sell-through baseline 9 $0.038
Scheduled second pass 2 $0.024
Sold explicit check 0 $0.020

The practical rule:

Workflow First run Scheduled run
Inspect all current inventory βœ… emitActiveItems: true ❌ usually too noisy
Daily changes only ❌ not enough baseline βœ… emitActiveItems: false
Webhook / Telegram βœ… keep emitRunSummary: true βœ… keep emitRunSummary: true

Start with maxItems: 100.

Only move to 300 after one run proves the query is useful.

⚠️ Limits

This is not magic.

Claim Reality
"Missing means sold" ❌ No. Missing can mean hidden, removed, reranked, moderated, or sold.
"Sold pages are always reliable" ❌ Not in my tests. Some endpoint variants returned active listings.
"More countries is always better" ❌ Current cap is 5 countries per run for stability.
"Schedule everything hourly" ❌ You pay per start and per emitted row. Start daily.
"This guarantees profit" ❌ It only gives better market data.

πŸš€ Try the exact monitor

Open Vinted Smart Scraper on Apify.

Use this first:

{
  "mode": "SELL_THROUGH_TRACKER",
  "query": "nike air force 1",
  "countries": ["fr"],
  "maxItems": 100,
  "trackingStoreName": "daily-nike-air-force-fr",
  "trackerId": "daily-nike-air-force-fr",
  "missingRunsThreshold": 2,
  "emitActiveItems": true,
  "emitRunSummary": true
}
Enter fullscreen mode Exit fullscreen mode

Run it once.

Then save it as an Apify Task, add a Schedule, and switch:

{
  "emitActiveItems": false
}
Enter fullscreen mode Exit fullscreen mode

That is the difference between scraping Vinted once and monitoring a resale market every day.

πŸ“š Series

This is Part 1 of Vinted Smart Scraper - Market Monitor.

Next:

  • Part 2: Cross-Country Vinted Search: Comparing 5 Markets Without Burning Sessions
  • Part 3: Seller Watchlists, Price Drops, and Webhooks: The Vinted Signals I Actually Schedule
  • Part 4: Best Vinted Scraper in 2026: An Honest Developer Comparison

Top comments (0)