shinnslist

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.

honesty contract. Every score on this page is a candidate ranking from public parcel attributes — it does not confirm a loan exists, its rate, or a seller's intent. The disclosure below ships verbatim in every endpoint response: "CANDIDATE rankings from public parcel attributes only. Does NOT confirm a loan exists, its rate, or assumability — verify via title search / MLS / listing agent. Rate bands are era-based estimates (Freddie Mac 30-yr, FHFA NMDB), not actual loan rates. Savings figures are illustrative (assumes 80% LTV, 30-yr fixed). Not appraisals, not advice." (constant 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:

typeweightevidence
absentee_owner40Owner address differs from property address
low_value25Assessed $… — under $150,000
entity_owner20Entity-owned: <owner>
distress_combo15Absentee owner AND assessed under $150,000 — compounded motivation signal
stale_record10Assessment 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 rulepointsreason text
assessed value ≤ FHA_LIMIT (472,030)+40Within FHA limit — FHA loans assumable by federal law
assessed value in (472,030 … 750,000]+25VA-typical range — VA loans assumable by federal law
owner-occupied (not absentee)+30Owner-occupied — more likely FHA/VA than investor conventional
assessed value < $300,000+20Sub-$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 bandlow–high %era
fha_core (value < $300k)2.8–3.22020-2021 FHA cohort
fha_range (value ≤ FHA_LIMIT)3.0–4.02019-2022 gov-loan cohort
va_range (value ≤ VA_PRACTICAL_MAX)3.2–4.52019-2022 VA-typical cohort
above3.5–5.02019-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

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