Move the Delta county brain — parcel spine, 34 layers, 3.1M facts — onto Flue/Turso by importing what we already scraped, then keep it fresh with Worker-native scrapers. Validated build spec · 2026-06-24
homezero-market, loaded additively (the agent's existing listings=170 / market_reports=2 untouched): parcels 18,970 · parcel_facts 3,114,247 · parcel_listings 482 · pending_resolution 387 — exact-count match to source. Read path proven live (zpid 103871512 → parcel 292329201006 → facts across 20 sources). Code shipped as PR #65 (parcel store + Gemini embed client + parcel_brief staff tool; 21 tests). End-to-end vector search verified on real Turso.
parcel_embeddings table + ANN index are live and empty. (2) Prod load was additive via libSQL batch() (~650–1500 rows/s) — turso db import for the staging bulk, then in-place batch insert to the live DB (no cutover); turso db shell was too slow (~150/s).In one breath: We don't re-scrape — we import. The old delta-market cell's populated brain is alive on its VM (822 MB · 18,970 parcels · 3,114,247 facts across all 34 layers) and we have admin on the target Turso DB, so the initial fill is a bulk copy, not a 10-hour backfill. The one thing the old DB doesn't hold is embeddings (the vector table is empty) — so those we generate fresh via Gemini (key found, 768-dim, round-trip proven in Turso). The scrapers then become the keep-fresh layer, not the initial-fill layer: we live-validated 33 of 34 sources today (only Tyler-recorder deeds is captcha-gated — parked to the end of the roadmap), including the Google-Maps key that was hiding in the cell's own .env. This spec is the build: Phase 0 import the brain + generate embeddings, Phase 1 stand up the keep-fresh scrapers behind a chunked beat, Phase 2 author the deep Brief + comps.
Before writing a line of build, we proved every load-bearing assumption against live systems. This is the de-risk.
egg-c5e25a)The brain is at egg-c5e25a:/root/skills/home0/delta-co-market.db (reached via well exec). Counts:
18,970 parcels · 3,114,247 parcel_facts · 482 parcel_listings · 387 pending_resolution · 0 embeddings (the vec0 table was scaffolded but never populated). Top layers by fact volume: tax_parcels 531k, poi 449k, epa 405k, soils 239k, climate 215k, zillow 210k, groundwater 170k, cdl 155k, frost 151k, ndvi 131k, mineral 101k, wildlife 62k, landcover 56k, sales 52k, fire/drought 37k each, zoning 24k, assessor 22k, distress 16k, water_rights 7.7k, tax_history 6.7k.
The live Flue market store is the Turso DB homezero-market (libsql://homezero-market-peteknowsai…) — it already holds listings (170), ingest_runs, market_reports. We have turso-CLI admin (account peteknowsai), so we load straight in — no Worker-secret juggling.
| Cluster | Result | Notes for a Cloudflare Worker |
|---|---|---|
| ArcGIS (parcels, tax_parcels, zoning, flood ×2, drought, land_status, protected, wildlife, oil/gas) | 10/10 live | open public-gov, no auth; PAD-US host slow → use backoff |
| Federal + CO-DWR REST (soils, landcover, CDL, NDVI, climate, fire, EPA, POI, water rights, wells, groundwater, frost, irrigated) | 13/13 live | no keys; Overpass needs home0/1.0 UA (not Mozilla) |
| Portals (EagleWeb assessor/sales/tax, Public Trustee, Column notices, DORA permits) | 6/7 live | EagleWeb guest-login cookie + Public Trustee __VIEWSTATE both still work; datacenter IP may draw throttle on full-county volume — hence we import the backfill |
| Google Maps family (elevation, solar, roads, address-validation, aerial, imagery) | key found + valid | key was in the cell's /root/skills/home0/.env; Elevation API returned HTTP 200 + real result. Copy to Worker secret. |
| Tyler recorder deeds | human-fed | Google reCAPTCHA blocks any IP — parked to the end of the roadmap |
F32_BLOB(768), and ran a cosine search. For the query "irrigated land with senior water rights," the ranch parcel ranked nearest (cos 0.1776) and the condo far behind (cos 0.4278) — correct semantics. vector_top_k over libsql_vector_idx also returned. Scratch DB destroyed. Every mechanic in this spec has been demonstrated, not assumed.
| Source (retired cell) | Target (Flue) | |
|---|---|---|
| Store | SQLite delta-co-market.db (822 MB) on egg-c5e25a | Turso homezero-market (libSQL) |
| Access | well exec -s egg-c5e25a + sqlite3 | turso CLI (admin) / Worker @libsql/client |
| Spatial | R-tree (parcels_rtree) | bbox btree columns + JS point-in-polygon |
| Vectors | sqlite-vec vec0 (empty) | native F32_BLOB(768) + libsql_vector_idx |
| Facts | parcel_facts(parcel_id, source, fact_key, fact_value_json, observed_at) | same shape — a near-1:1 transform |
listings)CREATE TABLE IF NOT EXISTS parcels ( parcel_id TEXT PRIMARY KEY, geometry TEXT, -- GeoJSON polygon centroid_lat REAL, centroid_lon REAL, bbox_min_lat REAL, bbox_min_lon REAL, bbox_max_lat REAL, bbox_max_lon REAL, acres REAL, updated_at INTEGER ); CREATE INDEX IF NOT EXISTS parcels_bbox ON parcels(bbox_min_lat,bbox_max_lat,bbox_min_lon,bbox_max_lon); CREATE TABLE IF NOT EXISTS parcel_facts ( parcel_id TEXT NOT NULL, source TEXT NOT NULL, fact_key TEXT NOT NULL, fact_value TEXT, observed_at INTEGER NOT NULL, PRIMARY KEY (parcel_id, source, fact_key, observed_at) ); CREATE INDEX IF NOT EXISTS facts_by_parcel ON parcel_facts(parcel_id); CREATE INDEX IF NOT EXISTS facts_by_source ON parcel_facts(source); CREATE TABLE IF NOT EXISTS parcel_listings ( parcel_id TEXT, source TEXT, source_id TEXT, lifecycle TEXT, first_seen INTEGER, last_seen INTEGER, last_active_price INTEGER, resolution TEXT, confidence REAL, PRIMARY KEY (source, source_id) ); CREATE TABLE IF NOT EXISTS pending_resolution ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, payload TEXT, reason TEXT, created_at INTEGER ); CREATE TABLE IF NOT EXISTS parcel_embeddings ( parcel_id TEXT, kind TEXT, embedding F32_BLOB(768), embedded_at INTEGER, PRIMARY KEY (parcel_id, kind) ); CREATE INDEX IF NOT EXISTS parcel_emb_idx ON parcel_embeddings(libsql_vector_idx(embedding)); -- ingest_runs already exists in homezero-market; extend it with a resume cursor: ALTER TABLE ingest_runs ADD COLUMN cursor TEXT; -- (no-op-guard in code if already present)
homezero-flue WorkerGOOGLE_MAPS_API_KEY — lifted from egg-c5e25a:/root/skills/home0/.env (validated). Unlocks elevation / solar / roads / address-validation / aerial / imagery.GEMINI_API_KEY — already in ~/.cells/secrets.json, validated 768-dim; set as Worker secret for the embedding-generation beat.TURSO_DATABASE_URL / TURSO_AUTH_TOKEN (point at homezero-market), BRIGHTDATA_API_TOKEN (Zillow), CONVEX_*, AUTH_PASSWORD, ANTHROPIC_API_KEY.All secrets piped in via wrangler secret put without ever echoing the value (the Slack-secret pattern).
study beat (keep-fresh runtime)A Durable-Object alarm gets one Worker invocation's budget, so refresh work is chunked and cursor-resumable. study picks the most demand-relevant stale source, processes one bounded batch, records progress in ingest_runs.cursor, and re-arms until current.
async study(){
const job = await pickSource(); // demand-weighted: listed-parcel sources first
if (!job) return; // nothing stale → idle
const { facts, pending, nextCursor } =
await job.adapter.ingest(manifest, { cursor: job.cursor, batch: BATCH });
await writeFacts(facts); await recordPending(pending);
if (nextCursor){ await saveCursor(job.source, nextCursor); this.scheduleOnce(POLITE_GAP,'study'); }
else await finishRun(job.source);
}
Two modes per source: daily-incremental (listed parcels only — assessor/sales/reconcile/distress, cheap, in the sweep tail) and periodic refresh (the static layers, rarely). The county-wide backfill mode that the old VM ran for hours is not needed — Phase 0 imports it.
This is the move that makes the whole thing cheap. Nothing here scrapes; it copies what we already have and mints the one thing we don't (vectors).
ensureSchema() in flue/src/market/store.ts with the §2.2 tables + the ingest_runs.cursor guard. Test: applies idempotently against a :memory: client and against homezero-market (additive — does not touch listings).well exec -s egg-c5e25a -- sh -c 'gzip -c /root/skills/home0/delta-co-market.db' > delta.db.gz → gunzip locally (822 MB → ~a few hundred MB gz). Verify counts match (18,970 / 3,114,247).parcels / parcel_facts / parcel_listings / pending_resolution from the SQLite file, map columns to §2.2 (rename fact_value_json→fact_value; derive bbox_* from geometry; drop the parcels_rtree/vec0 shadow tables). Bulk-load into a fresh homezero-market-staging via batched @libsql inserts (chunks of ~1–2k rows; 3.1M facts = a one-time run). Test: staging row counts equal source counts; a spot-check parcel's facts match.turso db import (cleaned snapshot; vec0/rtree dropped), counts verified == source. Promoted to live homezero-market via libSQL batch() (additive, in-place, resumable) — prod counts match exact, listings/market_reports untouched. Staging scaffolding destroyed.gemini-embedding-2 @ 768 the moment there's real buyer interest in it — no county-wide pre-compute. The table + ANN index are live and empty; embed.ts (canonical Gemini impl) + upsertParcelEmbedding + vectorSearchParcels are the on-the-fly path, proven end-to-end on real Turso (NL query → Gemini → vector_top_k → ranked parcels). Note the Gemini per-minute rate cap (~1300) if a bulk pass is ever wanted.listings, resolve zpid→parcel by bbox-prefilter + JS point-in-polygon (port the ≤50 m nearest fallback → pending_resolution). Test: a listing inside a parcel binds; report the resolved/pending split.egg-c5e25a cell is dead weight — fold its teardown into the cells-retirement plan. Keep one cold archive of delta-co-market.db in R2 first.The import gives us history; the scrapers keep it current and cover new listings/parcels. Port the 27 adapters from the cells/market/ blueprint into Flue (Worker-safe fetch, writes via store.ts, invoked by study). Build in value order.
study beat + demand-weighted schedulerstudy (§2.4) into the market agent; scheduleEvery daily + self-continue while a cursor is open. pickSource() ranks listed-parcel sources and advisor-requested sources first. Add the daily-incremental tail to sweep. Test: stale source picked → one batch ingests → cursor advances → re-arms; current sources idle.account.jsp parse (validated working). Account numbers pre-resolved from tax_parcels. Daily-incremental over listed parcels only (keeps datacenter-IP volume low). Test: a fixture account.jsp parses to the expected value/sales/tax facts.home0/1.0 UA and PAD-US backoff. Periodic refresh cadence. Test one adapter per fetch-pattern against a fixture.__VIEWSTATE scrape (validated; drop the old TLS-ignore hack), Column notices API, the pure-SQL distress deriver, and reconcile-listings (sold/failed-to-sell lifecycle → feeds distress). Test: a parcel with a lien + estate notice scores the expected distress level.GOOGLE_MAPS_API_KEY set: elevation, solar, roads, address-validation, aerial, imagery — listings-only, bounded. Address-validation also strengthens listing→parcel resolution. Skip cleanly if the key is ever absent. Test: key-present → elevation fact written for a listed parcel.marketHealthingest_runs; extend marketHealth to report per-source age so the staff desk + silent-death guard see layer staleness. Test: a stale source surfaces in marketHealth.reasons.{status:'not-started'} with a beat that reads a parcel's facts and authors the 34-layer appendix. The drain/briefing flow gains read_parcel_facts so verdicts are grounded in assessed value, recorded sales, distress, water, hazards — not just Zillow. Test: a briefed home publishes a deepDive with layersExamined > 0 and real sections.semanticSearch over parcel_embeddings (the proven vector_top_k path) for revealed-substitution / comp-set. Test: a query parcel returns nearest neighbors by cosine.parcel_facts read; the advisor's scout can lens on real signals (water, distress, hazard); the billboard read can cite record truths. Public surfaces stay real-data-only, institutional-names-only. Test: staff read_listing returns joined parcel facts.import comes in via Phase 0 (already scraped) free worker EagleWeb session maps key Google (key in hand) derived human-fed parked
| # | Layer | Initial fill | Keep-fresh fetch | Facts imported |
|---|---|---|---|---|
| — | parcels (spine) | import | ArcGIS (live: 19,009) | 18,970 rows |
| 1 | tax_parcels | import | ArcGIS | 531,076 |
| 2 | assessor | import | 22,840 | |
| 3 | address_valid | import | maps key | 425 |
| 4 | zillow | import | Bright Data (live) | 210,761 |
| 5 | sales | import | 52,645 | |
| 6 | tax_history | import | 6,732 | |
| 7 | foreclosures | import | Public Trustee | 80 |
| 8 | deeds | — | human-fed · parked | 0 |
| 9 | notices | import | Column API | 129 |
| 10 | permits (DORA) | import | free worker | — |
| 11 | distress | import | derived | 16,832 |
| 12 | cdl | import | free worker | 155,567 |
| 13 | ndvi | import | free worker | 131,987 |
| 14 | irrigated | import | CO DWR | 14,470 |
| 15 | water_rights | import | CO DWR | 7,790 |
| 16 | well_permits | import | CO DWR (fix source collision) | incl. |
| 17 | groundwater | import | CO DWR | 170,730 |
| 18 | soils | import | USDA NRCS | 239,347 |
| 19 | landcover | import | MRLC | 56,901 |
| 20 | terrain | import | maps key | 190 |
| 21 | climate | import | NOAA | 215,654 |
| 22 | frost | import | CO DWR | 151,760 |
| 23 | fire_risk | import | USFS | 37,940 |
| 24 | drought_monitor | import | ArcGIS | 37,940 |
| 25 | flood_500yr | import | ArcGIS | 412 |
| 26 | flood_2009 | import | ArcGIS | 300 |
| 27 | zoning | import | ArcGIS | 24,072 |
| 28 | land_status | import | ArcGIS | 140 |
| 29 | protected_areas | import | ArcGIS (slow) | 7,782 |
| 30 | wildlife | import | CPW ArcGIS | 62,033 |
| 31 | mineral_activity | import | CO DNR | 101,808 |
| 32 | epa | import | EPA ECHO | 405,645 |
| 33 | poi | import | OSM Overpass | 449,309 |
| 34 | roads & solar | import | maps key | 559 |
Net: 33 layers fill from the import + refresh live; only deeds has no automated path and is parked. Every other layer's keep-fresh fetch was validated live on 2026-06-24.
turso db import staging → verify → additive libSQL batch() into live homezero-market. Embeddings on-the-fly. Listings→parcels links imported (482, already resolved). Outcome: the deep-Brief data is live in Turso. Remaining: retire egg-c5e25a (keep a cold R2 archive of delta-co-market.db first) — don't kill until prod is proven in practice.study beat + EagleWeb daily-incremental + the free ArcGIS/REST refreshers + distress chain. Outcome: the brain stays current without the VM.Parked — at the very end of the roadmap:
deeds facts (currently 0) mean nothing is lost by waiting. Not on the critical path.gemini-embedding-2 @ 768, native Turso vectors (proven end-to-end).homezero-market (exact counts, live tables untouched).water.ts/waterrights.ts source='water_rights' collision + DELETE-wipe — give well-permits its own source='well_permits'.GOOGLE_MAPS_API_KEY (from the VM .env) + GEMINI_API_KEY into the homezero-flue Worker.