Building live comp and trend analytics on MLS data: the feed-handling realities nobody warns you about, and the relisting trap that silently corrupts every days-on-market stat. Public excerpt; the full teardown lives in the Builder library.
Property analytics sounds like a dashboard project until you meet the data. MLS feeds are rich, standardized-ish, and full of local dialect — and the difference between a demo and a production stack is entirely in how you handle their quirks. Four layers carry the build.
Layer 1 — Feed ingestion, defensively. Listings arrive through the MLS's data access — modern RESO-style APIs where available, legacy formats where not — on a sync schedule with change detection. Two defensive rules from day one: treat every field beyond the core as optional (local MLSs extend and omit freely), and keep raw payloads, because the mapping you write in week one will need re-running when you learn what a field actually meant.
Layer 2 — The listing lifecycle model. A listing is a state machine — active, pending, back on market, withdrawn, expired, sold — and local conventions vary in how transitions are recorded. Modeling the lifecycle explicitly, rather than trusting each status snapshot, is what makes time-based stats (days on market, absorption, pending-to-close) computable and honest. This layer is where most naive stacks are wrong without knowing it.
Layer 3 — Comp-set logic as configuration. What counts as a comparable — radius versus neighborhood boundary, recency window, property-type strictness, adjustment factors — is a judgment an experienced agent holds tacitly. The build encodes it as explicit, tunable configuration. The goal isn't replacing the agent's judgment; it's making it repeatable, so every comp pull reflects the same judgment instead of whoever had time that day.
Layer 4 — Views built for the appointment. The consumption layer is shaped by where it gets used: the listing-appointment view (this street, this comp set, live) is designed to be shown to a seller on a screen, and the farm-area view is designed to answer "where is turnover actually happening" in one look. Analytics that don't fit into an appointment don't change any outcomes.
Relistings poisoned days-on-market. A listing that sits, gets withdrawn, and relists at a new price arrives in the feed looking like a fresh listing with a fresh clock. Naively counted, the market's slowest properties masquerade as its newest — and DOM stats skew optimistic exactly where sellers most need honesty. The fix is property-level identity: listings resolve to parcels across relistings, and DOM computes over the property's full marketing history. It's the single most consequential correction in residential analytics, and almost nobody's first version has it.
A status that didn't exist ate a week. One local MLS used a custom status value for a common transition; the lifecycle model didn't recognize it and quarantined thousands of listings as invalid. The lesson generalized: every enum from an MLS feed gets a catch-and-review path, not a rejection path, because local dialect is a certainty, not an edge case.
The full teardown — the lifecycle state model, the parcel-identity resolution approach, the comp-set configuration schema, and the appointment-view specs — lives in the Builder library.
Implementation detail, checklists, and the parts we'd rather not have public — for members.
Unlock with Builder