Error taxonomy
Error taxonomy
Section titled “Error taxonomy”Every error the API and MCP tools can return comes from one closed set of codes. No misleading errors: a missing price says price_required, not “outside mapped tracts”. The canonical list ships as error-taxonomy.json (one of the machine-readable spec contracts) and is mirrored in packages/spec/error-taxonomy.json.
Response envelope
Section titled “Response envelope”Every /api/* JSON response carries these fields alongside its existing payload (additive — nothing was removed):
{ "…": "existing top-level fields (data / listings / value / …)", "as_of": "2026-07-10T13:00:00.000Z", "model_versions": { "model": "p3s-2026.07", "underwrite": "p3s-2026.07", "serving": "p5s8-2026.07" }, "warnings": []}On error, HTTP routes add a structured error object (plus flat code/message for convenience):
{ "error": { "code": "invalid_params", "message": "request failed input validation", "field": "strategy", "expected": "one of [flip,brrrr,hold,house_hack]" }, "code": "invalid_params", "message": "request failed input validation", "field": "strategy", "as_of": "…", "model_versions": { "…": "…" }, "warnings": [] }MCP tool results keep the code in a string error field (backward-compatible and still machine-readable), e.g. { "error": "units_required", "message": "…" }. Validation failures are surfaced through the tool result with isError: true.
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
units_required |
400 | no | Unit count unknown and no matching listing supplied it — pass units. Never a phantom duplex. |
price_required |
400 | no | A positive price is required and none was resolvable. |
outside_mapped_tracts |
400 | no | The point is outside Cook + Lake County, IL (current coverage). |
tract_not_ingested |
400 | no | Sales data for this census tract is not yet ingested — comps unavailable (never a silent tract substitution). |
insufficient_renovated_comps |
200 | no | Tier-3 suppression: not enough renovated comps. No ARV number is emitted (INV-5); a reason is attached. |
dispersion_exceeds_threshold |
200 | no | Tier-3 suppression: comp dispersion exceeds the confidence guardrail. |
luxury_thin_comps |
200 | no | Tier-3 suppression: the luxury stratum has too few comps. |
rate_limited |
429 | yes | Anonymous 30/min/IP, or a keyed rpm_limit token bucket was exceeded. Retry after ~60s. |
invalid_params |
400 | no | Failed the JSON-Schema-subset validator (type / enum / required / min / max / pattern). Carries field + expected. |
not_found |
404 | no | No such listing, tract, or collection. |
unauthorized |
401 | no | Authentication required — send Authorization: Bearer pm_<key>. |
forbidden_scope |
403 | no | Your key lacks the scope for this call (write tools need write-state). |
payload_too_large |
413 | no | Request or response exceeds a size budget; responses truncate + warn instead where possible. |
upstream_unavailable |
502 | yes | A county open-data source (Socrata) or the state store was unreachable. |
Suppression is not failure
Section titled “Suppression is not failure”The three *_comps / dispersion_* codes return HTTP 200 with arv: null and a suppression_reason. That is the honest Tier-3 state — “not enough evidence to value this confidently” — not an outage. Agents should render the reason, not retry.
