DEV Community

Christian Anderson
Christian Anderson

Posted on

Where second-hand price data actually comes from: every source I tried, measured

I needed to answer one question automatically: what is this second-hand thing worth?

Not "what are people asking" — what did one actually sell for, recently, with a date attached. That turns out to be one of the most locked-down pieces of data on the consumer web, and almost everything written about getting it is out of date.

So I measured all of it. Here is what each source actually returned, in September 2026, from a UK home connection.

The negative results are the useful part. Nobody publishes the list of things that don't work, which is why everybody spends the same three weekends finding out.


The one-line conclusion

eBay sold data is licensed, not public. Every free route to it is either technically blocked, contractually prohibited, or behind an approval queue. Free retail pricing, by contrast, is widely available.

If you take one thing from this: stop trying to scrape sold prices. Anchor on retail, treat sold comps as a metered resource, and spend your effort on relevance — which is the actual hard problem, as we'll see.

The dates matter

  • eBay decommissioned its free Finding and Shopping APIs on 5 February 2025.
  • Reddit closed self-serve API access in June 2026.

Most advice older than that describes a world that no longer exists — including a lot of what's currently on page one of search results.


Tested and blocked

Source Result
ebay.co.uk/sch (sold + active) Akamai challenge. Plain curl403; headless browser → "Pardon Our Interruption"
eBay robots.txt Prohibits automated access outright, and names "LLM-driven bots" explicitly
ebay.co.uk/itm/ Fetchable (5×200 @ ~3.3s, no block) — but carries no sold date, so useless for a time-windowed rule
130point.com Cloudflare 403
watchcount.com robots.txt is User-agent: * / Disallow: / — everything banned bar named search engines
Reddit JSON 403 from a datacentre. Self-serve API closed; commercial tier ~$12k/mo
PriceSpy / Idealo / MusicMagpie 403
eBay Finding + Shopping APIs Decommissioned 2025-02-05
eBay Marketplace Insights Limited Release, partner-approved only

Why WatchCount can and you can't

They analyse "data provided directly from eBay" as a partner of ~20 years, originally built on eBay's Merchandising API.

Their access is a relationship, not a technique — which is exactly why their robots.txt forbids re-scraping it. Great tool to use by hand. Not one to automate against.

The two that fetch fine and still don't help

Gumtree and Vinted return pages without complaint.

Gumtree, for one graphics card, returned: £550, £650, £605, £280, £1, £1800. Whole PCs, chancers, and the actual item, mixed together, with no sold dates. Backmarket returned laptops containing the GPU.

Both fail on relevance. This is the lesson of the whole exercise:

Fetching was never the hard part.


What works #1: solve relevance structurally

PriceRunner publishes retail prices with the category in the URL path:

/pl/37-3204201908/Graphics-Cards/ASUS-GeForce-RTX-3060-Dual-OC-V2...
/pl/27-3200833084/Laptops/HP-Omen-16-b0009na...          ← wrong category
Enter fullscreen mode Exit fullscreen mode

That's it. That's the filter. No model, no LLM relevance pass, no prompt to tune — the URL tells you whether the match is a graphics card or a laptop that contains one.

And you don't need to touch their search. Their robots.txt disallows /results and /cl/*?q=*. But they publish a sitemap for crawlers: 1,269 child sitemaps, one per category, ~1,000 products each, refreshed daily. Fetch /pl/ product pages from that, which robots.txt permits.

Measured: category 37 → 856 graphics cards indexed locally in one fetch. Index once, query locally forever. That's the difference between a pipeline that survives and one that's blocked in a fortnight.

What works #2: pay for the one field nothing free provides

trawl.dev — 250 requests/month free, recurring, no card. $20/mo for 3,000.

Field Why it matters
sale_price The sold price
date_sold The field nothing free provides
shipping_price Postage, separately

Without a sold date you can't run a time-windowed rule, and a price with no window isn't a market price — it's an average over an unknown period during which the market may have halved.

What works #3: ask the way the page asks

This is my favourite one.

CeX (a big UK second-hand chain) is behind bot protection. uk.webuy.com returns Cloudflare 403 to a server, and their SPA defeats a headless browser too.

But open the network tab. Their own front end queries Algolia, at search.webuy.io — and that endpoint answers an unauthenticated server request, returning the same rows the website renders.

SEARCH = "https://search.webuy.io/1/indexes/*/queries"
INDEX  = "prod_cex_uk"

body = json.dumps({"requests": [{
    "indexName": INDEX,
    "params": f"query={urllib.parse.quote(query)}&hitsPerPage={limit}",
}]}).encode()
Enter fullscreen mode Exit fullscreen mode

One POST. No key, no evasion, no browser. You're making the same request the page makes.

And it gives you something better than a retail price. Two numbers that are different in kind:

Field Is Means
sellPrice an ask What CeX retails it for. Nobody paid this
cashPriceCalculated a floor What CeX pays you, today

That second one is rare and valuable. It is real money available immediately — the only number in a pricing chain that isn't somebody's opinion. If CeX will hand you £82 today, listing at £60 is giving money away, and nothing else in your pipeline notices.

Measured on a Sony WH-1000XM4: sell £145, buyPerc 57 → £82 cash, exchangePerc 69 → £100 voucher. The trade-in "formula" people go looking for is published per product.

Two traps in it

Condition is in the name, not a field. Grades read as a suffix — "…, A", "…, B" — and each grade is its own row at its own price. And it is not always preceded by a comma: "…Midnight, Unlocked B" grades as B with the comma three words earlier. Anchoring on the comma read every iPhone as ungraded.

A boxId is <product code><grade letter>. So 4548736112117A/B/C is one pair of headphones at three conditions, not three pieces of evidence. I got this wrong at first and my confidence line overstated what the market had told me. Count distinct products, not rows.


Web search as a price source: measured, and it does not work

I wanted this to work. It doesn't. Tested against four real items via a healthy self-hosted SearXNG (HTTP 200 in 1.7s, 279 engines):

Query Results Carry a price What those prices were
Buc-ee's beaver plush for sale 20 2 $21.95, $35
Pokemon Black Star Promo Trainer 20 3 $980 pricecharting, $8,900 facebook
Buc-ee's keychain 1982 fob 20 2 $39 a jewellery shop, $24 a podcast host
Garmin Forerunner 630 used 20 2 $165 reddit, £1.79 ebay (a watch strap)

Two or three results in twenty carry a number at all, and those are the same relevance failure that killed the marketplace routes — plus mixed $/£ with no normalisation.

Search snippets are not a price source.

But search is excellent for discovery. It surfaced pricecharting.com for the Pokémon card. Same shape as PriceRunner: use search to find the specialist catalogue, then read that catalogue's structured data. Never read the price out of the snippet.

Which leads to the last route: keyless specialist catalogues. api.pokemontcg.io (Cardmarket's real 30-day sold average), api.discogs.com, api.scryfall.com, tcgcsv.com, BrickLink's 6-month sold averages for LEGO. Most need no auth at all.

Two warnings there: api.pokemontcg.io's query endpoint is flaky — I measured 500, 500, 500, 200, 200 on five identical calls (fetch-by-id is reliable) — and Cardmarket's snapshot ran ~2 months stale while TCGplayer was same-day.


Three kinds of number

Most pricing mistakes are category errors. These are not interchangeable and must never be averaged together:

  • Sold — what somebody actually paid. The only fact here.
  • Ask — somebody's opinion. Useful as competition, not as value.
  • Floor — money available today. A sanity check under everything else.

A sold average and a lowest ask are different evidence. Cardmarket's 30-day average is money people paid, so price from it. A Discogs lowest price is one seller's opinion, so it joins the asking pool — shaving a few percent off the cheapest ask is a race to the bottom dressed up as a method.


The trap that cost me the most time

One evening of ordinary testing throttled all four of SearXNG's upstream engines — brave "Suspended: too many requests", duckduckgo "CAPTCHA", google cse suspended, startpage "CAPTCHA".

It then returns HTTP 200 with zero results. Which is indistinguishable from "this product does not exist."

Never build a pipeline whose failure mode is a silent empty list.

Related, and the same shape: absent must never read as zero. A failed lookup returning 0.0 becomes "this is worthless" three functions downstream. Return an empty result and make the caller handle it.

except (urllib.error.URLError, ValueError, KeyError, IndexError):
    # ⚠️ Absent, never zero. A dead lookup must not read as "CeX says £0".
    return []
Enter fullscreen mode Exit fullscreen mode

How to re-measure

All of this has a shelf life. The method matters more than my tables:

  1. Date every measurement. A result without a date is folklore within six months.
  2. Record what it returned — the 403, the challenge page title, the actual prices — not just "worked / didn't".
  3. Test from where your code will run. A home connection and a datacentre get different answers.
  4. Read robots.txt before the site. It tells you whether you have a technical problem or a permission one. Those have different solutions.
  5. Check whether the front end calls something you can call too. That's the whole CeX section in one sentence.
  6. Distinguish "unreachable" from "no such thing" in your own return values, or you'll one day price an item from a network error.

This came out of building a tool that turns photos into priced eBay drafts. The full measured research — every source, the specialist catalogue table, and the complete CeX module — is written up in more detail than fits here.

If you're building anything that needs to know what a used thing is worth: start with the "tested and blocked" table. It'll save you the weekends it cost me.

Top comments (0)