v1 tutorials LIVE
Three end-to-end walkthroughs you can actually execute — in the browser or with curl. Every endpoint, param, response shape, error code, and ID below was re-derived from the deployed functions code (apps/poc-web/functions) before a word was written. Steps that don't exist yet are marked DRAFT and never presented as working.
tutorial 1 — find assumable mortgages in a ZIP LIVE
Goal: pull the top assumable-mortgage candidates for a ZIP, understand what the scores mean, and see exactly where the v1 recipe path stands today.
step 1 — hit the working endpoint: GET /api/engine/assumable?zip=
This is the endpoint the assumable-mortgage screen itself uses (functions/api/engine/assumable.js). It scores parcels on public attributes — FHA/VA/USDA loans are assumable by federal law — and returns the top 10 candidates with a score above 0.
curl "https://shinnslist.com/api/engine/assumable?zip=22903"
The response shape, copied from the handler:
{
"ok": true,
"signal": "assumable_mortgage_under_4pct",
"zip": "22903",
"count": 10,
"cached": false,
"properties": [
{
"parcel_id": "...",
"owner": "...",
"address": "...",
"assessed_value": 385000,
"assumable_score": 87,
"reasons": ["..."],
"estimated_rate": 3.1,
"monthly_savings_illustrative": 412
}
],
"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": "CANDIDATE rankings from public parcel attributes only. Does NOT confirm a loan exists, its rate, or assumability — verify via title search / MLS / listing agent...",
"source": { "...": "..." },
"retrieved": "2026-09-29T14:00:00.000Z"
}
- ZIP rules (
lib/validate.jsvalidateZipParam): 5 digits, or ZIP+4 (22903-1234) — max 10 chars. Missing ZIP →zip required: pass a 5-digit ZIP (ZIP+4 accepted); bad ZIP →invalid zip: expected 5 digits (e.g. 22903) or ZIP+4 (e.g. 22903-1234). Upstream failure →502 {ok:false, error}. - read the disclosure, not just the score. These are candidates ranked from public parcel attributes — the page never confirms a loan exists, its rate, or its assumability. The
monthly_savings_illustrativefigure assumes 80% LTV, 30-yr fixed, vs a 7.0% market rate. cachedtells you whether the parcel set came from the 1-hour KV cache (parcels:v1:<zip>,PARCEL_CACHE_TTL = 3600) or a live fetch. Scoring re-runs fresh on every request, so a cached parcel set ranks identically to a live one.
step 2 — see the same search through the intel pipeline: GET /api/intel?q=
The pipeline (functions/api/intel.js → lib/pipeline.js) runs the query, geocodes it, and returns every signal's record with its own freshness stamps:
curl "https://shinnslist.com/api/intel?q=22903"
Key output fields: query.type (address | locality | coordinates — a bare ZIP lands as locality), geocode, jurisdiction, signals[] (each with signal_id, status — live or degraded — retrieved_at, data_as_of), and pipeline_version (v0.1.0). Auth: key as x-api-key header or ?key=; no key = anonymous path. The pipeline stamps retrieved_at itself per run — cache-control: no-store, so every call re-runs.
step 3 — the recipe version (the honest answer)
Trying to compile an assumable recipe against the v1 surface today fails — and the failure is the honest state of the build. The catalog names exist (three_percent_mortgage_buyers_list, military_assumable_loan, assumable_equity_arbitrage, assumable_seller_finance_flag, stale_90day_x_assumable_buyers) but 19 signals have working v1 adapters (lib/v1-registry.js): tax_delinquency_early_warning, amenity_qol_score, bedroom_add_arbitrage, burn_scar_fire_channel, census_flip_tract, farm, gold_stream_prospecting, hidden_water_feature, middle_man_lending, openfema_flood_buyout, post_purchase_tax_reassessment, public_land_access_verify, river, rv_parks, tax_delinquency_early_warning_formula, unique_home_lender_match, va_boundary_check, va_land_intel_score, waterfall. Compile an assumable recipe:
curl -X POST https://shinnslist.com/v1/recipes \
-H "Content-Type: application/json" \
-d '{"recipe":{"geography":{"type":"parcel_list","parcel_keys":[{"state_fips":"51","county_fips":"540","apn_normalized":"123-4-56"}]},"signals":[{"signal":"three_percent_mortgage_buyers_list","params":{}}]}}'
→ 404 signal_not_built: the name is in the 228-signal catalog, but there is no working v1 adapter yet. That is the code telling the truth — the catalog count is a planning count, never a working-API count.
The same three calls with the built signal do execute — POST /v1/recipes (save, 201, recipe_id = r_ + 24 hex), POST /v1/recipes/estimate (the estimate binds the charge), POST /v1/recipes/{id}/run (Idempotency-Key required). Full shapes, error codes, and billing rules are on the v1 recipes page.
planned: assumable recipe execution DRAFT
When the v1 adapters for the assumable catalog signals ship, the exact three calls above run them unchanged — same parcel_list geography, same estimate-binds-charge rule, same signal_not_built contract for anything still unbuilt. Nothing about the request shape changes; only the registry entry flips.
tutorial 2 — buy a list from chat, and where it actually goes LIVE
Goal: follow a "buy this as a list" chip from chat through the real handoff, the lists page, and the demo checkout — and know exactly what is (and isn't) recorded.
step 1 — in chat, the chip captures the rendered set
On a rendered result set in /chat.html, the "buy this as a list" chip captures the currently rendered entries — nothing more. buyListHandoffV1 builds the handoff: parcel ids, the kinds present (assumable, motivated), the query summary/text, source:"chat", plus ranking context (rankBy, rankDrivers) and the market ZIP when the chat knows it.
step 2 — the handoff crosses pages in localStorage
buyListStoreV1 writes the handoff under one localStorage key — shinnslist.buyAsList — and the chip deep-links to /lists.html?from=chat. The handoff shape is {v:1, ts, pids, kinds, query:{summary, text}, source:"chat"}.
step 3 — the lists page reads it (only if both halves hold)
chatHandoffReadV1 renders the panel only when the URL carries ?from=chat and a valid handoff is stored — missing param, missing storage, or an invalid handoff, and the lists page is unchanged. Pricing comes from the lists page's own basis (CHAT_HANDOFF_BASIS_V1): the assumable-mortgage screen $49 one-time, the motivated-seller compound $39 one-time. Unknown kinds get no price line — never invented. The "created from chat" attribution renders only when the handoff carries source:"chat".
step 4 — the demo checkout records a browser-local purchase
Clicking "buy this list — demo (no charge)" calls api.onBuy, which records the list in the purchased-lists library: localStorage key shinnslist.purchasedLists, every entry carrying demo:true, then takes you to the "your lists" library page (/my-lists.html). The page copy is explicit throughout: "demo checkout: buying records this list in your lists library, in this browser only — no charge is possible, nothing is sent anywhere."
planned: real checkout DRAFT
Payment is not implemented — there is no charge path, no server-side order, and no fulfillment beyond the browser-local library. When real checkout ships, this tutorial's steps 1–3 stay identical; only step 4 changes from a demo record to a real purchase.
tutorial 3 — check data freshness before trusting a price LIVE
Goal: use the real freshness surface — no separate freshness API, no SLAs — to decide whether a number is fresh enough to act on.
step 1 — read the two stamps on every signal record
Every signal record in an /api/intel response carries retrieved_at (set by the pipeline per run — one value per call, never by the adapter) and data_as_of (the adapter's declared upstream source date, or null when the adapter doesn't know). null is an honest unknown, never fabricated into a date, and it is not a staleness verdict. Full contract on the data freshness page.
step 2 — check the engine's own stamps
The /api/engine/assumable response carries two freshness signals of its own: the cached boolean and the retrieved ISO timestamp. Raw parcel data per ZIP is cached in KV for 1 hour (PARCEL_CACHE_TTL = 3600, key template parcels:${PARCEL_CACHE_VERSION}:<zip> — resolving to parcels:v1:<zip> today); scoring re-runs fresh on every request. If cached is true and the retrieved instant is within the hour, you're looking at at-most-one-hour-old parcel attributes with fresh scoring.
step 3 — check the record age in your purchased lists
Purchased lists carry per-record parcel freshness (assets/purchased-lists.js): sync stamps live under localStorage shinnslist.parcelSyncs, and records older than PL_FRESHNESS_THRESHOLD_DAYS = 30 are honestly marked stale. Never-synced records never get an invented date. One-tap refresh re-prices off the page's basis, re-syncs the stamps, and records the refresh.
step 4 — treat degraded as a staleness signal
A throwing adapter becomes degraded with the reason in the unavailable field — never a 500 — so a partial answer still arrives with its stamps. When you see degraded, the number beside it deserves extra skepticism: the adapter failed and the pipeline is telling you so instead of silently dropping it.
planned: freshness SLAs DRAFT
There are no freshness SLAs, no staleness thresholds, and no refresh schedule anywhere in the codebase today. The timestamps plus the caches above are the entire freshness story. When SLAs exist, this tutorial gains a step — the steps above don't change.