Price is Right

API for partners

A calibrated time-to-sale and price-recommendation engine for your platform

The same engine that gives every seller on this platform a price recommendation: a hedonic value model (“what it's worth”), a hierarchical time-to-sale risk model (“how the asking price moves the time to sell”) and a decision layer that turns those two distributions into a wealth-maximising recommendation — with calibrated probabilities, not just a point estimate.

Apartments — all of Estonia, live today Architecture: any class of comparable assets Cars — sketched

How we prove it

Every number comes from a replay, not a pitch deck

The model is measured by a standing evaluation harness: a temporal train–test split with administrative censoring (no future leakage into training), weekly calibration checks and a decision replay in which the deployed recommender prices held-out listings and the outcomes unfold independently. Below are the numbers from its latest full run.

0,0032
Calibration error (ECE)

Calibration error of the weekly sale probability. When the model says “7% probability of selling this week”, that is what happens. The model captures 95% of the fair skill ceiling (one that cannot see seller urgency); discrimination C-index 0,678.

0,503 vs 0,504
Ex-ante forecast vs reality

P(sold within 26 weeks), forecast from the asking price known on day one, vs the share observed later (adjusted for drift). A horizon forecast that can only be made on a listing's first day — and it held.

+3 012 €
Expected extra wealth per listing
78%
Win rate
45%
Of the oracle's edge captured

Decision replay: the model's price recommendation vs typical seller behaviour on the same held-out listings (95% confidence interval 2 240…3 712 €). 45% of the edge that only an oracle knowing the true parameters would capture. Measured in the setting occupancy=empty, stigma 0,5.

Price levels and tax rules: real sources

Price levels are calibrated against actual transaction data from Maa-amet (the Estonian Land Board) (district table + decade price index). The rule for taxing sale proceeds is checked against §15 (5)–(6) of the Estonian Income Tax Act — the recommendation handles the tax exemption correctly, not approximately.

Coverage: all of Estonia

33 regions, one yardstick, the same numbers side by side

The world was extended from 8 Tallinn districts to 10 cities and 15 rest-of-county buckets, each calibrated to real Maa-amet price levels and transaction volumes (200 county/city rows and 370 municipality rows from the official price-statistics tool). The Tallinn world was appended to, never regenerated — its rows are byte-identical — so the original evidence stays valid, and the harness re-ran on the expanded world:

Metric Tallinn, original world Tallinn, inside the expanded world 25 new regions
Weekly hazard calibration (ECE)0,00320,00350,0017
Discrimination (C-index)0,6780,6780,678
Decision replay, extra wealth per listing+3 012 € [2 240, 3 712]+2 583 € [1 796, 3 329]+1 703 € [1 264, 2 226]
P(sold within 26 weeks), forecast / observed0,460 / 0,5040,452 / 0,5040,459 / 0,496

Overlapping confidence intervals for Tallinn before and after: the expansion did not degrade the original market. Smaller absolute gains in the new regions are expected — cheaper apartments mean fewer euros at stake per decision. Stated limitation: regional liquidity (how much slower a Võru apartment sells than a Tallinn one) has no public data source, so time-to-sale dynamics outside Tallinn carry prior uncertainty; price levels and volumes are real, speed differences are honestly wide bands.

Second asset class: houses

A failure, its diagnosis, and the fix — all measured by the same yardstick

Detached houses were added as a separate asset class — own hedonic (house area, plot, era), own time-to-sale model — calibrated to real county-level Maa-amet house statistics (75 rows, 15 counties, 5 years; all 14 counties with enough transactions match their real price levels). The first fit told an uncomfortable story, so we published it, then fixed it:

Houses First fit (published as a failure) Measurement-error fit (ships)
Weekly sale probability (ECE)0,00260,0032
Fitted price-elasticity of sale speed (truth −4,4)−1,6−4,3 ± 0,8
Model asks × market (oracle: 1,04×)1,21×1,01×
Decision replay, per listing vs default seller−3 451 € [−5 484, −1 650]+1 653 € [945, 2 454]
Win rate vs default seller45%72%

Diagnosis. House values are far noisier than apartments (hedonic noise 0,18 vs 0,07 in log terms). Feeding a noisy value estimate into the time-to-sale model attenuates the fitted price-elasticity to a third of the truth — classical errors-in-variables — so overpricing looked cheap and the model asked too much. Fix. The true value is now a latent variable inside the model, with the hedonic estimate as its noisy measurement; the noise level is fitted, not assumed — and came out at 0,17, recovering the true 0,18 without being told.

One more negative result, kept: even with the correct elasticity, advice anchored on the hedonic value alone still loses for houses (−3 478 €) — a 0,18-noise anchor misplaces an individual house too far to out-price a seller who knows their own home. The advice wins only when the seller's own asking price sharpens the anchor. So the API flags house recommendations as experimental unless an asking price is supplied. Apartments were re-checked under the same treatment: no material change, the deployed apartment model stays as measured. County granularity only for houses; town-vs-countryside within a county is not modelled yet.

An honest limitation we are the first to state ourselves

All validation so far has been done against synthetic data whose structure the model shares. The held-out test rules out overfitting to noise, but not a deeper circularity: the model's form matches the generator's by construction. The evaluation harness is a fixed yardstick against which real data will be measured — not a claim that real-world numbers will match the ones above.

Measured and rejected

Negative results we publish

Every feature idea goes through the same replay before it reaches users. Two ideas failed the measurement and stayed out of production. This discipline is why the positive numbers above mean something — the same harness that approves can also reject.

Recommending a price-cut schedule
Did not ship

The idea: recommend the seller an upfront schedule of price cuts, not just an opening number. Replay result: a statistical tie — +10 € per listing, 95% confidence interval [−25, 39]. An effect indistinguishable from zero is not a feature, it is noise.

An anchor blended with comparable sales
Did not ship

The idea: blend the prices of comparable sold apartments into the recommendation's price anchor. Replay result: a monotonic loss of wealth — the larger the blending weight, the worse the outcome; the optimum turned out to be α = 0. Comparable sales stayed in the interface as an explanation, not in the decision model.

AI photo scoring (condition from photos)
Bounded to ±6%

A vision model assesses from the listing's photos what the hedonic model cannot see: finish quality, kitchen, bathroom, light, visible risk spots. The score is converted by a deterministic, documented formula into a shift of at most ±6% on the market anchor — half the spread two human assessors show on the same apartment. An honest limitation: in the synthetic world the same stock photos repeat across thousands of listings, so the photo→price relationship CANNOT be validated by replay here. The feature is in production as an informational layer; validation awaits real transaction data.

API quickstart

For your developers

JSON over HTTPS, four endpoints under /v1. Authenticate with an X-API-Key header (Authorization: Bearer is accepted for back-compat). Default rate limit 60 requests/min per key; a breach returns 429 with Retry-After.

POST /v1/market-price hedonic market value from raw listing features
curl -s "$API_BASE/v1/market-price" \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{
    "district_index": 0, "area_m2": 58.5, "renovated": true,
    "transit_minutes": 8.5, "school_score": 0.8,
    "top_floor_no_lift": false,
    "building_series": "1990s", "heating": "district"
  }'

→ {"market_price_cents": 14800000}
POST /v1/recommend the full advisory: optimal ask, indifference band, time-to-sale
curl -s "$API_BASE/v1/recommend" \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{
    "district_index": 0, "market_price_cents": 14800000,
    "seller_inputs": {
      "occupancy": "live_in", "monthly_rent": 0,
      "imputed_rent_monthly": 0, "horizon_weeks": 52,
      "deadline_week": 26, "deadline_prob": 0.9
    }
  }'

→ {
  "best_price_cents": 15340000,
  "indiff_lo_cents": 14970000, "indiff_hi_cents": 15760000,
  "expected_days": 64.3, "p_sold_within_horizon": 0.8712,
  "breakeven_carry_per_day_cents": 4120,
  "days_saved_per_1000": 1.92, "deadline_price_cents": 14760000,
  "market_price_cents": 14800000,
  "grid_boundary_hit": false, "grid_width_pct_used": 35.0,
  "curve": {"price_cents": [...], "expected_days": [...]},
  "viz": {...}   // chart-ready posterior grids; may evolve faster than the rest
}
POST /v1/comps weighted nearest sold comparables; only lat/lng/area required
curl -s "$API_BASE/v1/comps" \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{
    "lat": 59.4345, "lng": 24.809, "area_m2": 58.5, "rooms": 2,
    "building_series": "1990s", "renovated": true, "k": 25
  }'

→ {
  "anchor_price_cents": 14520000, "anchor_eur_m2_real": 2482.05,
  "n_comps": 25, "ess": 14.2, "local_median_weeks": 9.0,
  "comps": [
    {"listing": 4211, "district": 3, "lat": 59.4351, "lng": 24.8102,
     "distance_m": 214.0, "area_m2": 61.0, "rooms": 2,
     "series": "1990s", "sold_date": "2025-11-04",
     "realized_price_real_cents": 15100000,
     "realized_eur_m2_real": 2475.41, "weight": 0.0912},
    ...  // top 10
  ]
}
GET /health no auth
curl -s "$API_BASE/health"

→ {"status": "ok", "n_districts": 8, "model_version": "..."}
Versioning promise

/v1 response fields are additive-only: we add fields, we never rename, retype or remove them. A breaking change means /v2, with both versions served in parallel. The one exception is the viz block (chart-support data), which is explicitly documented as evolving faster.

Why it gets better over time

Every transaction on your platform makes the model more accurate

For every listing, the platform collects data in exactly the shape model retraining needs: attributes, opening price, price changes, final outcome and actual time to sell — with no personal data about the seller. The synthetic validation harness is a yardstick built ready for this dataset: as real outcomes accumulate, we will measure the model against them just as strictly as against synthetic data today. This is a flywheel a latecomer cannot buy their way into — with every partner the dataset grows, and every partner benefits from it.

Interested? mart@kollanekirss.ee We reply within one business day.