Skip to content

Model changelog

Every output is stamped model_version. When the version bumps, active listings scored under the old model get rescored (stale_model_rows in /api/health tracks the backlog). Releases that change numbers get an entry here.

Presentation and freshness release — the deal math is unchanged, but three things now match what a consumer expects:

  • Rank-transformed scores. Every served flip/BRRRR score is now a county percentile: the best tract reads 99–100, the median reads ~50. A 90 now means top decile, literally. Raw factor math is unchanged and stays internal; only the surfaced number moves onto a 0–100 scale you can read at a glance. The deal score for each listing is built from the rank-transformed tract score too, so a great location finally shows up near the top of the range.
  • Real listing dates. Listings now capture Redfin days-on-market, so “listed X days ago” reflects the actual on-market date instead of when we first saw it. Older listings where the source hides the day count still fall back to first-seen.
  • Photo service. A Street View photo service is wired in behind GOOGLE_SV_KEY; property and listing responses expose photo_url (a /api/photo?lat=&lon= link that never leaks the key). It ships dark — returns a 404 with a plain reason — until the key is set.

What changed for your numbers: scores spread across the full 0–100 range instead of bunching in the 40–85 band, so the same tract that read 85 may now read 99. Rankings and the underlying math are identical — this is a rescale, not a re-rank. Run a rescore after deploy so stored listings pick up the new scale.

Comp-engine guards and honesty fixes, following a 2026-07-08 audit that found a live $500k Logan Square 2-flat valued at ARV $1.37M from comps spanning $125k–$1.94M:

  • Similarity floor 0.45 and a minimum of 4 comps — dissimilar sales are rejected with reasons, not averaged in.
  • Dispersion gate: comp IQR > 45% of median → “insufficient comps,” not a number.
  • Dispersion penalty in the confidence formula uncapped (was capped at 35); comps drive ARV only at confidence ≥ 55 (was 35).
  • Income-approach cap re-anchored to the tract heuristic — a hot comp base can no longer float its own ceiling.
  • Circular ARV fallback removed: tract price null → unscored with a reason, never (ask + rehab) × 1.12.
  • Unscored states: deal_score: null with reason strings; null ≠ 0.
  • Client/worker parity: the in-map calculator now runs the same math as the API (it had a leftover 1.30 comp premium, 1.85× rent multiplier, and 2× income cap).
  • Public beta: reads and /mcp opened, 30 calls/min/IP.

What changed for your numbers: addresses with mixed-condition comp sets now refuse instead of returning an inflated ARV; the map popup and the API can no longer disagree; some listings that previously had scores are now honestly null.

  • Economics factor (0.35–1.0) multiplicatively gates the deal score.
  • Income-approach ARV capped at 1.5× (was 2× — too loose).
  • Bedroom-aware market rents: SAFMR ladder blended with ZORI, bounded 0.7–1.6× SAFMR.
  • Renovated rent multiplier 1.85× → 1.5× in the engine.

What changed for your numbers: deal scores collapsed on bad deal math even in good tracts; rents and income-approach ARVs dropped to defensible levels. Scores broadly moved down — that was the point.