ActorStack.dev

Finding used cars priced below market in Spain, with one query

A concrete screen: which fields to filter on, which segments carry the valuation data, and how to avoid the two biases that make a below-market list look better than it is.

By Oswaldo Carabano8 min read

Short answer

Filter listings where `price_eur` sits meaningfully below `price_average_indicator_eur` and `price_rank_indicator` agrees. Because the valuation is present on 86.6% of dealer listings but only 38.2% of private ones, an unfiltered screen is biased toward dealers — and the biggest apparent bargains are usually high-mileage or badge-restricted cars, not mispriced ones.

Key points

  • The screen needs two conditions, not one: an absolute gap and the site's own rank, because either alone produces false positives.
  • Coverage bias is structural: dealers carry the valuation more than twice as often as private sellers, so a naive screen is a dealer screen.
  • Mileage and DGT badge explain most large gaps. A car with no badge is restricted in Madrid and Barcelona low-emission zones, which is priced in.
  • `from_cache` and `data_age_hours` matter here more than anywhere: a bargain from a week-old row may already be sold.
  • Sorting by percentage gap rather than absolute gap keeps cheap cars from dominating the list.
On this page7 sections

The screen itself is four lines of SQL. Everything difficult about this task is in the two biases you inherit for free and the three categories of gap that look like bargains and are not.

The screen, in full

below-market screen
SELECT ad_id, url, make, model, year, km, hp,
       environmental_label, province, seller_type,
       price_eur, price_average_indicator_eur AS market_eur,
       ROUND(100.0 * (price_average_indicator_eur - price_eur)
             / price_average_indicator_eur, 1) AS gap_pct
FROM listings
WHERE price_average_indicator_eur IS NOT NULL
  AND price_rank_indicator IN ('below_market', 'good_price')
  AND price_eur < price_average_indicator_eur * 0.88
  AND km < 150000
  AND environmental_label IS NOT NULL
  AND data_age_hours < 48
ORDER BY gap_pct DESC
LIMIT 100;

Six conditions, and each one removes a specific class of false positive. The rest of this article is why each is there.

Why one condition is not enough

An absolute gap on its own produces two kinds of noise. Rounding artefacts at the cheap end — a €3,000 car €400 below a €3,400 average is a 12% gap and a rounding error — and model-mix effects at the expensive end, where a base trim sits below the average of a model whose average is dominated by higher trims.

Adding price_rank_indicator filters both, because the site's own rank is computed with context you do not have. Using the two together means you are asking: is this car cheap by the numbers, and does the marketplace itself think so?

The percentage threshold does the third job. 88% of the estimate is roughly the point where the gap exceeds normal trim and condition variance; below 80% the results skew heavily toward cars with something wrong that the data does not show.

The coverage bias you inherit

The valuation is present on 86.6% of dealer listings and 38.2% of private ones. So the first condition in the screen — price_average_indicator_eur IS NOT NULL — silently makes this mostly a dealer screen.

That may be what you want: dealers are the segment that trades at volume, publishes phone numbers and can be contacted at scale. But if your thesis is that private sellers are cheaper, this screen cannot test it, and running it anyway produces a confident answer to a question it did not ask.

To compare segments, screen them separately with segment-specific reference prices — the coverage numbers are the reason.

The gaps that are not bargains

Three explanations account for most of the largest gaps:

  • Mileage. The valuation is a model-level average, so a car at 220,000 km is compared against a reference dominated by cars at half that. The gap is real and correctly priced.
  • No DGT badge. A car with no environmental badge cannot enter the low-emission zones in Madrid or Barcelona. That restriction is priced into the ask and is invisible unless you read environmental_label what the codes mean.
  • Stale rows. A cached row from six days ago may describe a car that sold on day two. data_age_hours exists so this is checkable rather than assumed.

Subtract those three and the list gets much shorter and much more interesting. What remains is mostly dealers repricing slow stock and cars in provinces where that model is oversupplied.

Freshness decides whether the car still exists

This is the one condition people leave out and then wonder why half their leads are dead. Rows can be served from a shared cache — which is why runs are cheap — and every row reports which it was through from_cache, fetched_at and data_age_hours.

For a live bargain hunt, set maxCacheAgeDays: 0 so every row is read fresh during your run, and accept the higher cost. For depreciation analysis, week-old rows are perfectly fine and the cache is a gift.

A worked example

Take a single make in a single province, fresh, with dealers included so you can actually make contact:

input.json
{
  "maxResults": 3000,
  "makes": ["bmw"],
  "provinces": ["madrid", "barcelona", "valencia"],
  "maxCacheAgeDays": 0,
  "scrapeDealers": true,
  "maxDealers": 150
}

Three thousand listings is $6, plus $0.60 for the dealer profiles. Run the screen above and you will typically be left with a few dozen rows. Join the dealer dataset on the seller to get a phone number on 100% of the dealer ones, and you have a list you can act on the same morning.

Repeat it on a schedule and the interesting signal is not the list — it is the churn. A car that appears in the below-market set and disappears within a day was correctly priced by somebody faster than you.

Scaling it to the whole country

50,000 rows per run is the cap, so a national sweep is several runs partitioned by facet, which is the better operational shape anyway: a failure costs one partition. Partition by make for analysis, by province for lead generation.

And keep the raw rows. The below-market screen is cheap to re-run; re-scraping last week to discover which listings vanished is impossible. Disappearance from the listing set is the closest proxy you have for a sale, and it only exists if you kept the earlier snapshot — turning snapshots into a time series.

Frequently asked questions

How do I find underpriced used cars in Spain?
Screen listings where the asking price is meaningfully below coches.net's own market-average estimate and its price rank agrees, then subtract the explainable gaps: high mileage, missing DGT badge, and stale rows.
Are private-seller cars cheaper?
Often, but you cannot verify it with the marketplace's own valuation, because that field is filled on only 38.2% of private listings against 86.6% of dealer ones. Any comparison across segments inherits that gap.
Why does a car look 30% below market?
Usually mileage, a missing environmental badge, or a row old enough that the car has already sold. Check `km`, `environmental_label` and `data_age_hours` before treating a gap as an opportunity.

Sources

Every URL below was requested and returned a page on the date shown.

  1. Operator claimchecked 18 Aug 2026
    Coches.net Car Listings & Dealer Scraper — Actor README and input schemaActorStack / Apify Store
  2. Law or regulatorchecked 18 Aug 2026
    Distintivo ambientalDirección General de Tráfico (Spain)
A narrow street in Valencia, Spain, lined with parked cars and apartment blocks.
coches.netGuide

Spanish market analysis

What you can and cannot conclude from marketplace listings: asking prices are not transaction prices, inventory is not demand, and the badge distribution tells you more than the fuel type does.

8 min
Paper receipts and printed documents arranged on a desk beside office stationery.
coches.netExplainer

Price rank explained

Coches.net computes its own valuation for each model and publishes it next to the asking price. Two fields, filled on 77.6% of listings, that replace a pricing model you would otherwise have to build.

7 min
Aerial view of a vehicle storage lot with cars parked in long, even rows.
coches.netGuide

Scrape coches.net

A walkthrough of extracting used-car listings from Spain's largest vehicle marketplace: how discovery works by facet rather than pagination, what the 55 fields contain, and what a thousand listings cost.

9 min