engine LIVE
The buyer-intent engine: five /api/engine/* endpoints that score real parcels from Charlottesville's public GIS into ranked deal-finding output. This page is a working reference of the deployed functions code (apps/poc-web/functions/api/engine/ + functions/lib/validate.js). Every constant, field, weight, and error string below was re-derived from that code before a word was written. Planned capabilities stay DRAFT-badged.
CANDIDATE_DISCLOSURE, engine/lib.js)the five endpoints LIVE
All three per-ZIP endpoints share one input contract (validateZipParam, lib/validate.js): zip is required; 5-digit ZIP or ZIP+4 (ZIP+4 is normalized to its 5-digit base, e.g. 22903-1234 → 22903); the exact 400 error strings are zip required: pass a 5-digit ZIP (ZIP+4 accepted), zip too long: max 10 characters, and invalid zip: expected 5 digits (e.g. 22903) or ZIP+4 (e.g. 22903-1234). An upstream GIS failure returns { ok: false, error } with status 502. Responses are JSON, top 10 parcels per endpoint.
GET /api/engine/assumable?zip=
Assumable-mortgage candidates. Response fields: ok, signal: "assumable_mortgage_under_4pct", zip, count, cached, properties[] (each: parcel_id, owner, address, assessed_value, assumable_score 0–100, reasons[], estimated_rate { low, high, era, basis }, monthly_savings_illustrative), methodology (basis: "FHA/VA/USDA loans assumable by federal law; 5.2M gov loans under 4% per FHFA Q1 2026"; rate_bands: "Freddie Mac 30-yr averages: 2020-2021 FHA 2.8-3.2%, 2022 4-5%"; savings: "Illustrative: assumes 80% LTV, 30-yr fixed, vs 7.0% market rate"), disclosure, source, retrieved. Only parcels with score > 0 are returned, ranked by the deterministic ranker. Scoring constants from engine/lib.js:
FHA_LIMIT = 472030; // 2026 FHA standard single-unit limit VA_PRACTICAL_MAX = 750000; LOW_VALUE_THRESHOLD = 150000; // Charlottesville distress marker CURRENT_MARKET_RATE = 7.0; // illustrative "today's rate" ILLUSTRATIVE_LTV = 0.8; // assumed balance = 80% of assessed value
GET /api/engine/motivated?zip=
Motivated-seller candidates. Response fields: ok, signal: "motivated_seller", zip, count, cached, properties[] (each: parcel_id, owner, address, assessed_value, motivation_score 0–100, signals[] — each { type, weight, evidence }), tier2 ("OSINT framework ready: job relocation posts, life events — public sources only"), disclosure, source, retrieved. Only parcels with score > 0 are returned. Signal types and weights, verbatim from scoreMotivation:
| type | weight | evidence |
|---|---|---|
| absentee_owner | 40 | Owner address differs from property address |
| low_value | 25 | Assessed $… — under $150,000 |
| entity_owner | 20 | Entity-owned: <owner> |
| distress_combo | 15 | Absentee owner AND assessed under $150,000 — compounded motivation signal |
| stale_record | 10 | Assessment year <year> — record may predate a recent sale (weak proxy; sale dates not published) |
The code's own Tier-1/Tier-2 framing (motivated.js header): Tier 1 LIVE — parcel-derived signals, enriched with distress-combo boost + tenure proxy. Tier 2 — framework for social ingestion, not live data; see the DRAFT section below.
GET /api/engine/combined?zip=
Dual-signal properties — BOTH assumable candidates AND motivated sellers. The engine's top-value output. Response fields: ok, signal: "dual_signal", zip, count, cached, properties[] (each: parcel_id, owner, address, assessed_value, composite_score, dual_signal boolean, assumable_score, motivation_score, estimated_rate, monthly_savings_illustrative, why[] — the first 2 assumable reasons + the first 2 motivation evidence strings), methodology (composite: "mean(assumable, motivation)"; synergy: "+15 when both scores >= 40 — dual-signal properties rank highest"; ranking: "composite desc, ParcelNumber asc (deterministic)"), disclosure, source, retrieved. Only parcels with composite > 0 are returned. The synergy constants, verbatim: DUAL_SIGNAL_THRESHOLD = 40 (both scores must clear 40), DUAL_SIGNAL_BONUS = 15 (added to the mean, capped at 100).
GET /api/engine/stats
Coverage and signal penetration across all covered ZIPs. No parameters. Response fields: ok, zips[] (each: zip, place, parcels, zero_value_excluded, assumable_candidates, assumable_pct, motivated, motivated_pct, dual_signal, dual_signal_pct), totals (sums + pct), disclosure, source, county_coverage (wave-1 county parcel inventory: live counts per FIPS and the honest gap — no owner/value/address on county layers, phase 2), retrieved, cached (5-minute in-memory cache; constant CACHE_TTL_MS = 5 * 60 * 1000). Percentages are one decimal: Math.round((n / d) * 1000) / 10.
GET /api/engine/geoip
Visitor ZIP via IP geolocation — $0, no key. No parameters. Resolution order, verbatim from geoip.js: Cloudflare's request.cf.postalCode first (returned with source: "cloudflare-cf-postalCode", plus city/region/country from cf); otherwise the answer says Cloudflare had no postal code: { zip: null, city, region, country, source: "cloudflare-cf-no-postal" }. No outside service is called. Response wraps the geo result with ok, retrieved, and this verbatim disclosure: "IP-based geolocation is approximate. ZIP may reflect ISP location, not physical address."
the scoring model LIVE
All scorers live in functions/api/engine/lib.js. Scores cap at 100 (Math.min(100, score)); ties break deterministically (rankBy: score desc, then parcel_id asc); per-ZIP endpoints slice the top 10.
| assumable rule | points | reason text |
|---|---|---|
| assessed value ≤ FHA_LIMIT (472,030) | +40 | Within FHA limit — FHA loans assumable by federal law |
| assessed value in (472,030 … 750,000] | +25 | VA-typical range — VA loans assumable by federal law |
| owner-occupied (not absentee) | +30 | Owner-occupied — more likely FHA/VA than investor conventional |
| assessed value < $300,000 | +20 | Sub-$300k — strong FHA first-time-buyer profile |
Absentee owners get a counter-note instead of points: "Absentee owner — more likely conventional investor loan (not assumable)".
| rate band | low–high % | era |
|---|---|---|
| fha_core (value < $300k) | 2.8–3.2 | 2020-2021 FHA cohort |
| fha_range (value ≤ FHA_LIMIT) | 3.0–4.0 | 2019-2022 gov-loan cohort |
| va_range (value ≤ VA_PRACTICAL_MAX) | 3.2–4.5 | 2019-2022 VA-typical cohort |
| above | 3.5–5.0 | 2019-2022 jumbo/conventional cohort |
Illustrative monthly savings: assumed loan balance = 80% of assessed value (ILLUSTRATIVE_LTV = 0.8), 30-yr fixed at the band-low rate vs CURRENT_MARKET_RATE = 7.0 — labeled illustrative everywhere it appears, and null when there is no balance or no band.
caching & data source LIVE
Parcel records come from the City of Charlottesville Open Data GIS — Parcel Owner Points (layer 74), $0 (public records), none required (sourceInfo(), lib/properties.js). Covered ZIPs are hard constants verified 2026-09-29 (COVERED_ZIPS): 22901 (2,337 parcels), 22902 (6,146), 22903 (7,321) — all Charlottesville, VA. The first request per ZIP hits the GIS with up to 2 retries and 1.5s×(attempt+1) backoff; the 90-second server-side ZIP fetch degrades to a client-side filter if the source predicate fails.
The parcel cache (engine/cache.js): Cloudflare KV binding SHINNSLIST_CACHE, key template parcels:v1:<zip> (PARCEL_CACHE_VERSION = 'v1'), TTL PARCEL_CACHE_TTL = 3600 seconds (1 hour). The code's honesty contract is verbatim: the cache stores raw normalized parcel records only — scoring runs fresh and deterministic on every request, so a cached parcel set always produces the same rankings as a live one. Every endpoint returns a cached boolean telling you whether the data came from KV. If the KV binding is absent (local dev, misconfigured deploy) every call degrades to a live GIS fetch — nothing breaks, nothing throws.
access LIVE
The site is public — no password gate (removed 2026-10-10). /api/health and /api/status are listed in PUBLIC_PATHS in _middleware.js and skip rate limiting; every other endpoint is open too, limited only by the edge: 100 requests/min per client IP (retry with the Retry-After header; see rate limits). The engine code itself asks for no API key and no plan — parcel data is $0 (public records), none required.
what's planned, not yet shipped DRAFT
- Tier-2 OSINT ingestion — DRAFT. The
tier2field ships live in every motivated response, but the ingestion it describes (job-relocation posts, life events from public sources) is a framework only: the code says "framework for social ingestion", and no ingestion job, schedule, or source exists anywhere infunctions/. - Per-signal engine endpoints — DRAFT. Today the engine exposes exactly five endpoints:
assumable,motivated,combined,stats,geoip. There is no engine endpoint per signal; the full signal catalog lives at/api-reference.html. - Live per-parcel streaming — DRAFT. Endpoints score whole ZIPs and return the top 10; there is no per-parcel streaming, watch list, or push. For the polling-based alert substitute that exists today, see webhooks (
/api/health→errors.spike).
When any of these ships, the draft badges flip to LIVE and the page becomes its reference.
next: docs hub · api reference · v1 tutorials (assumable walkthrough) · data freshness · webhooks