HomeZero
HomeZero · internal · the build plan

From the picture
to the build.

How we turn the two-agent model into shippable work: where data lives, how a market is born anywhere in ten minutes and then teaches itself its own edges — and the exact order we build it in.

The build in five lines

01What we're building

One template, born anywhere, that teaches itself.

Today HomeZero is one hand-raised market in Delta County, Colorado. The plan makes that a recipe — a market agent that any new buyer's location can stamp out in minutes, cheaply, and that gets smarter the more it's used.

The shape is settled (see how-it-works): two agents stand all the time — an advisor bound to a person, a market bound to a place — and a twin is born only when a buyer scans a home in person. This document is the engineering plan underneath that picture: the data stores, the birth mechanics, the self-tuning loop, and the order of operations.

It was assembled the way we build now: a fan-out of agents mapped the live repository, three independent planners drafted it from different angles, and an adversarial reviewer red-teamed the result. The seven corrections that pass surfaced are folded in below — the plan you're reading is the post-review version, not the first draft.

The one-sentence goal

A buyer says “I'm looking near Marble” — and within ten minutes a market agent exists for that place, has taken a quick read of its listings, has read the home they're looking at, and has handed their advisor a real brief. No canvassing, no per-county setup, no human in the loop.

A lens we build by

Wherever we can, policy moves into the agent and out of the platform code. Convex and the scripts hold facts — ledgers, records, the shared schema; the agent holds judgment — what's enough, what's ready, when to grow. Determinism is fine inside a skill (the agent can do math on real numbers); we just don't hardcode the decision around it. What stays deterministic on purpose: auth, secrets, money, idempotency, and the shared schema itself — guarantees, not judgments.


02Data architecture

Four stores. One rule. A brain in three layers.

The hardest question wasn't “SQL or vector” — it was where does each thing live. The answer: sort by access pattern and regenerability, never by data type.

The decision rule — first question that hits, wins
1
Does an app render or filter it reactively?  → Convex
The serving layer and app-facing source of truth — listings, verdicts, the roster, the briefing feed of cards.
2
Is it a large rendered file served by a URL — image, PDF, recording, a shareable flyer?  → R2
Object storage, with a small Convex pointer row when an app references it.
3
Does the agent reason over it but no app reads it — the 34 record layers, embeddings, comp models, demand vectors?  → Stoolap
On the cell. It publishes conclusions up to Convex.
4
Is it raw, scratch, or rebuildable working state — the DB file, raw dumps, vendored binaries?  → Filesystem
On the cell, backed up to R2. Throw it away and rebuild.

Governing line: Convex holds the truth the app serves; Stoolap holds the truth the agent reasons over; R2 holds the bytes too big to query; the filesystem holds what you can regenerate.

The four stores

Convex publish · system of record

Reactive, app-facing. Everything the web app and extension query to render UI. Also the fleet registry and the demand-signal ledger.

holds — listings · verdicts · roster · briefing cards · cellRegistry · demandSignals
Stoolap the agent's brain

Embedded SQL + native HNSW vector in one binary. Replaces today's SQLite + the flaky sqlite-vec extension. Per-cell, rebuildable from Convex + scrapes.

holds — 34 record layers · listings · embeddings · comp models · demand vectors
R2 rendered bytes

Object storage we already run for backups. Anything file-shaped and served by URL — the shareable flyer/brief, scan photos and recordings, the DB backup.

holds — flyers · brief PDFs · pix portraits · scan media · DB backups
Filesystem scratch

The Linux disk on the cell. The Stoolap DB file lives here, plus raw scrape dumps and working files. All rebuildable; nothing unique.

holds — the Stoolap file · raw dumps · binaries · working state
One Convex schema, many markets

Every market agent writes the same Convex schema — one shared, uniform contract, keyed by market, never a schema per place. That's what lets the apps render across markets at all. Where a market gets to be itself is Stoolap: a rural-ag market and an urban one can carry completely different layer tables in their own store, and it never touches Convex. Convex is the uniform public contract; Stoolap is the private, per-place brain. Convex is also where agents share with each other — anything two agents both read (a jurisdiction's parcel layer that overlapping markets lean on) lives in Convex, keyed by jurisdiction, not re-scraped into each private store.

The market cell's brain — three layers

The store is the substrate. Over it sit two things that make the market smart, not just stocked:

StoreStoolap
What it holds. Parcels, the 34 layers, listings, embeddings — the structured ground the cell queries and writes. Convex stays system-of-record; this is rebuildable.
Memorydreams nightly
What it learns. A memory index with facts hung off it; a nightly consolidate-and-dream pass compresses the day, links facts, retires wrong ones, surfaces patterns. This is where boundary judgment lives — reasoned, not encoded as SQL thresholds. It also holds the market's yearnings — standing questions about its territory, each with a way to learn it and a confidence that grows — which drive what it studies next.
Wikithe new idea
What compounds. Every brief is woven into its neighbors as it's written — same street, ditch, water system, comparable sales, shared hazards. The market becomes a linked graph that densifies with every zero. This is the comps engine: “most similar recently-sold + explain the delta” is graph traversal, not a fresh query.
The briefing is two things — and that's the answer

The running feed of cards the app renders (market authors the home card, advisor weighs in) is structured and reactive → Convex. The rendered brief/flyer you share back into Zillow is a file with a stable URL → R2, with a Convex row for its metadata. Card in Convex, bytes in R2, a pointer joins them. The same home lives in three stores at three fidelities — full analysis in Stoolap, the published verdict in Convex, the rendered portrait in R2 — and the market agent owns the projection.


03The evolving market

You don't guess the boundary. You earn it.

A market isn't a county and can't hold millions of homes. What bounds it is an attention budget — the comp model and watch load one cell can keep current. The boundary starts as a cheap guess and sharpens through use, across three timescales.

v0
At birth · mother · ~2s

Word-association

Mother feeds the buyer's fuzzy “where” to the model and gets a manifest back from its own geography alone — “Marble” → Crystal River Valley, jurisdictions Gunnison + Pitkin, low confidence. No data fetch. Wrong-but-cheap is fine by design: the first brief needs only the listing page + parcel identity, both geography-agnostic.

Mother owns v0
v1
After birth · the market

Structural canvass

On a later pulse — not at birth — the cell runs a cheap structural pass: ZIP price-series clustering, school zones, commute sheds. It proposes a firmer manifest and raises its own confidence. The market is the only writer of its boundary after birth.

Market owns v1
v∞
Ongoing · revealed

Revealed substitution

Which homes do the same buyers actually zero? Co-consideration is the market. The seam where it drops off is the split line; an idle sub-region is a merge candidate. This lives in the wiki and the memory — and it overrides the cheaper guesses as evidence accumulates.

Market owns v∞

v0.5 — the birth sample: a quick read of the room

Before any of that sharpening, the moment a market is born it does one thing the old design didn't: it samples the live Zillow listings in its boundary. Not the retired county canvass — a bounded, one-time, aggregate read: run the boundary as a Zillow search, capture the inventory count and a sample of cards (price, type, location). Deep per-home analysis stays demand-gated; this is shallow-and-wide, just enough to know the lay of the land. The rule of thumb: sample broad-but-shallow at birth, analyze deep-but-narrow on demand.

It pays for itself three times. It orients the agent — price range, inventory, property mix — so the first briefs aren't written blind. It is the first attention-budget reading: the search's total count tells the cell how big its boundary actually is. And it reality-checks the word-association guess on day one — forty thousand listings means the v0 boundary is too big, split before birth even finishes; a dozen means flag it for merge. The sample closes the loop from “guessed boundary” to “right-sized market” inside the same ten minutes.

The daily study — controlled curiosity, balanced with demand

That birth sample isn't a one-off — it generalizes into a daily study: the market wakes on its pulse and goes to learn something about its territory. The control structure is an old idea from our archived agents — yearnings: standing questions the agent holds (“what's really driving price on the north end?”, “where's the water-secure inventory?”), each carrying a way to learn it and a confidence that grows as it does. The birth sample seeds the first yearnings and answers the cheap ones.

The balance is the whole point. Each day it pursues one yearning on a bounded budget — never nonstop scraping. Demand reweights the agenda: when buyers keep zeroing homes near Marble, the yearnings about Marble rise to the top. But a floor of its own curiosity persists, so the market learns its place over time instead of only reacting to today's clicks. Demand steers; curiosity persists; the budget bounds — and that same budget is what keeps every market's scraping light enough to share one residential IP.

Yearnings aren't just for markets

Every agent gets them. Mother seeds yearnings at birth — some shared, some customized. An advisor's are about its buyer (“what does a home that takes care of them look like?”, “they said acreage on a tight budget — what's the real tradeoff?”), drawn from onboarding so each advisor is tuned to its person. A twin's will be about its house. Same mechanism — standing questions, a way to learn them, a confidence that grows — across the whole fleet.

Two forces set every edge — and can disagree

Economic coherence

Do the same buyers weigh these homes against each other? Don't split a real market in two.

VS
Attention budget

Can one cell hold it in its head and keep it current? Don't let it sprawl. Over budget → split; idle → merge.

Demand finds its own signal — and the Marble test

Two things write to the demand ledger, both forced in code, not runbook prose: every zero (the buyer hit ⊕) and every question an advisor asks the market about a place. That second one is how “when an advisor starts asking, the market learns this buyer is looking here.”

So a single zero near Marble — outside every market's boundary — is served as a one-off by the nearest market (no new cell, flagged confidence). That's the anomaly default. But when a market keeps seeing distinct buyers pulling toward Marble, it doesn't trip a counter — it reasons that the interest is real and says so in its staff channel, and on a nod mother births one in ten minutes from the same word-association. The place earned a cell because demand gave it something worth knowing — not because a threshold fired.

Fork / split / merge — judgment, surfaced to the staff channel

There are no deterministic thresholds here — no “split at 2,000 listings.” The market reasons over its own load and demand, and when it feels it should grow or consolidate, it surfaces that to its staff Slack channel in plain language, with its evidence. A human (or mother, watching) makes the call; then mother — who alone births or retires cells — runs the split, merge, or fork, re-homing affected buyers transactionally and reversibly. The agent senses; the operator decides; mother acts.


04The build

Three phases. One critical path.

Phase 0 lays the substrate everything reads and writes. Phase 1 is the unlock — a market born anywhere in ten minutes. Phase 2 layers in the intelligence. The phases overlap; the critical path is the spine.

Critical path 0·Fact-find→ Converge→ Kitchen→ Cheap birth→ NFV birth
Phase 0

Foundation

Land the pure-Convex schema, merge the contract doc every skill cites, and clear the pre-pivot specialist machinery. All cheap, all parallel — nothing downstream can register a market or learn demand without it.

WS0·GATEFact-find: which VM holds the real Delta brainMacS

Confirm whether delta-market or zero-delta-co-market holds the live 34-layer DB and the working Zillow fetchers — the seed is a 90 KB stub and imports fetchers that aren't in the repo. This is a gate: we package the canonical template from the cell that actually has the brain. We're not killing the handmade cells — they stay as reference — so this is about packaging from the right one, not a destructive cut.

Done ✓ (2026-06-16): both cells hold the full ~800 MB brain — identical 20-table schema, 18,970 parcels, working Zillow fetchers, sqlite-vec embeddings, the demand loop. The 90 KB stub was only the seed repo; no data-loss risk, nothing killed. Package from zero-delta-co-market (the namespaced canonical the app already routes to). delta-market is a near-identical twin (~12% more parcel_facts) kept as reference + data fallback.
WS0·MODELMerge the model spec + reconcile the twin triggerMacS

Bring agent-model.md and the agent specs to main so the dream-test contract every birth skill cites actually exists. Reconcile the contradiction: home-twin.md still says “born on zero-in”; the decision is born on scan. Fix the spec so the merged contract is self-consistent before anything builds to it.

Done: specs on main; twin trigger reads “scan” everywhere; references resolve.
WS0·REGMarket manifest + operational state on cellRegistryConvexM

Add a manifest — {name, geometry, jurisdictions[], narrative_seed, comp_set, confidence, placeType} (geometry is a ZIP list at v0, a polygon only once v1 has data), written by mother at v0 and the market thereafter — and a separate operational object for live metrics (active listings, watched homes, queue depth, readiness, boundary version). Two objects, one writer each, so a pulse metric never fights the boundary. Readiness is a status the cell asserts by its own judgment (“can I actually serve briefs here?”), not a coded transition rule. Add a coverFor lookup the agent uses (point → the market covering it, else null) — a fact-lookup, not a routing decision.

Done: register persists both; coverFor returns Delta for a Delta point, null for Marble.
WS0·DEMANDdemandSignals — located demand, forced in codeConvexL

A new ledger keyed by place: every zero writes a row from stars.starHome; every advisor-to-market territory question writes one as a side effect of the call — not a runbook instruction the model may skip. Convex just stores the raw rows (the ledger is a fact, not a policy), with by-place / by-zpid indexes; the market agent reasons over them — clusters, substitution, “is this emerging?” — inside its skill, where it can use numbers deterministically. No cluster or threshold logic lives in Convex.

Done: a zero writes both queue + history rows; an advisor question emits a demand row; the agent can read the raw ledger by place and reason about the patterns.
WS0·CLEANSpecialist teardown + roster guardConvexM

Retire the five leftover specialist template rows and drop their seed blobs; guard the roster so a regressed mother can't write a third member; keep the enum literals (history needs them). Namespace is settled: zero- denotes this project, so zero-advisor-pete is canonical; tend-advisor-pete belongs to a different project entirely — leave it alone. The dead zero-advisor-* rows are just history.

Done: no available/draft specialists; roster two-member-guarded; nothing in the tend- namespace touched.
Phase 1

Birth a market anywhere in ≤10 min

The unlock. Package one portable template from the live Delta cell, give a newborn its own kitchen, make birth cheap from word-association — then prove it by finding Delta County's real sub-markets and birthing North Fork Valley first.

Delta County is the first test — and it's already more than one market

Perfect proving ground: it's where we have the most data, and it's plainly not one market. Mother's word-association should find roughly three — the North Fork Valley (Paonia · Hotchkiss · Crawford — orchards, vineyards, arts), Delta & the Uncompahgre (the county-seat town and its valley — ranching, the commercial center), and Surface Creek (Cedaredge · Orchard City · Eckert — apple country up toward the Grand Mesa). The birth sample checks each. We birth North Fork Valley first (it's where Paonia lives), then the others as we want them. The two handmade Delta cells stay as reference — nothing gets killed.

WS1·CONVERGEPackage one canonical template from the live cellmarketXL

The long pole. Take the capability from the cell that has the brain (WS0·GATE) — the brief recipe, the analyze/watch runbook, the loose scripts — off the live disk into one versioned market template served via cellTemplateFiles{type:'market'}. Port the store to Stoolap (schema → Stoolap, retire sqlite-vec). Abstract the per-county source endpoints into a jurisdiction adapter; ship Delta County as the first. Parameterize placeType (rural-ag / suburban / urban) to flex the chapter set. The handmade cells stay as reference — this packages a template, it doesn't kill anything.

Done: a new place can pull a complete market seed; ingest reads from a jurisdiction adapter file; the handmade cells are untouched.
WS1·KITCHENThe newborn mints its own storemarketM

The red-team's #1 catch: birthing a market is three jobs — recipe, local supply, and a kitchen. Phase-B must stand up a fresh, empty Stoolap store (schema applied) before the first zero can be cooked. Stoolap makes this cheap — one binary, no native-extension ABI dance — which is exactly why it suits a template that births everywhere.

Done: a never-touched cell stands up an empty market store at Phase-B and can write its first brief into it.
WS1·PRICELock price-off-listing (no scrape)marketS

Already true today — this makes it a contract. Price and listing facts come off the buyer's own Zillow detail page (the exact page they saw), never a separate price scrape. Name it the priceFromListing adapter (geography-agnostic, so every market reads price identically), add provenance fields, and alert — never silently null — if both fetchers miss. This is the cold-start enabler: a fresh market needs zero county data for its first brief.

Done: the analyze path provably reads price only from the buyer's page; absent-price alerts; no scrape path exists.
WS1·SAMPLEThe birth sample — read the market on arrivalmarketM

As part of birth, right after the v0 manifest, the newborn runs a bounded Zillow search over its boundary — capturing the inventory count and a sample of listing cards (price, type, location), not per-home detail fetches. It orients the agent, seeds the store and the wiki, gives the first attention-budget reading, and reality-checks the boundary (huge inventory → split before finishing; tiny → flag merge). It also seeds the price/inventory structure the v1 canvass otherwise lacks. One-time and aggregate — not the retired canvass; deep analysis stays demand-gated. Runs through the cell's residential egress.

Done: a newborn holds an inventory count + a price/type read of its boundary in its store, and has confirmed or flagged its v0 size — inside the birth window.
WS1·BIRTHCheap birth + the master dispatchermotherL

Replace the Delta clamp with the v0 word-association manifest, reuse-or-birth via coverFor (then the birth sample, WS1·SAMPLE, reads the room). Add the missing master zero-birth skill that sequences the birth. Correction: the advisor births first / in parallel — it's instant and person-bound, it must never block on a market. The market births or attaches concurrently; the roster shows it “warming” until ready. Stop hardcoding the market in intake creation.

Done: a non-Delta intake gives the buyer an advisor immediately and a market within ten minutes; intake binds via coverFor.
WS1·EGGMeasure the budget + warm-egg fallbackcellsM

The ten-minute promise rests on Phase-B self-config time, which is unmeasured. Instrument wall-clock (birth → alive → first brief); if it blows the window, stand up a warm market-egg pool so a newborn starts from a pre-baked image. Don't claim “birth anywhere in ten minutes” until it's measured.

Done: time-to-first-brief is recorded; the egg-pool fallback is built or proven unnecessary by the measurement.
WS1·NFVNorth Fork Valley, end to end — the first real birthMacL

Birth North Fork Valley fresh from the template (Paonia · Hotchkiss · Crawford) — the first properly-bounded market, on home turf where we have the data. Born in ≤10 min, birth sample read, readiness honest (the cell asserts it), one real brief from one real zero, advisor born independently. Then birth Delta & the Uncompahgre and Surface Creek (Cedaredge) the same way, as we want them. This is the “a county is many markets” proof, run on Delta itself.

Done: a registered North Fork Valley market + an independent advisor exist; wall-clock measured; one real NFV brief live; Delta & Surface Creek birthable on the same path.
Phase 2

The evolving market

With a market born anywhere, add the intelligence: it measures its load, weaves its wiki, reshapes from demand, and the twin is born on scan.

WS2·TUNEAttention-budget meter + v1 canvassmarketL

Each pulse, the cell publishes its load (active listings, watched homes, queue depth, pass time) into the operational object — as telemetry the agent reads, not a trigger that fires code. The grow/consolidate call is the market's own judgment (surfaced to staff Slack, per the boundary protocol). Run the v1 structural canvass after birth. Honest flag: the canvass needs ZIP price-series / school / commute data the cell doesn't ingest today — the birth sample and yearnings feed that, but sourcing it is part of this stream, not a free pass over existing layers.

Done: Delta's pulse writes its load daily; the agent can read its own load and reason about whether it's over- or under-budget.
WS2·WIKIThe brief wiki — the comps enginemarketL

Every brief is woven into its neighbors as it's authored (street, ditch, water system, comparable sales). Comps become graph traversal over linked briefs; revealed-substitution clusters densify the graph. This is v∞ made concrete — the market's intelligence compounds with every zero, not just with scraping.

Done: a new brief links to its neighbors on write; “most similar recently-sold + explain the delta” resolves by traversal.
WS2·YEARNYearnings — the controlled daily study agendamarketM

Port the archived yearnings mechanism: standing questions in the cell's memory (subject + “how to learn” + a confidence that grows), seeded by the birth sample. On each pulse the market pursues one yearning on a bounded fetch budget — a targeted Zillow read or layer query — and updates its confidence, store, and wiki. Demand (zeros, advisor questions) reweights priority so study concentrates where buyers are, with a floor of self-directed curiosity. The nightly dream surfaces new yearnings and retires answered ones. The budget doubles as the rate-limiter that keeps every market light on the shared residential IP.

Done: the cell holds yearnings with confidences; a daily pass advances the top one within budget; demand visibly reprioritizes the agenda.
WS2·DECODERetire the deterministic verdict + freedom fallbacksmarketM

The de-coding lens on real code (from the sweep). The render-time heuristic libs — lib/{signals,lives,distress,sold} — are deterministic policy (price>avg×1.4, DOM bands, 15 life-archetypes, the negation-first distress regexes, the 1.5×/36-mo flip rule). Four already carry a // TODO retire header and a cell-authored path, so this is finishing the deletion: cells author via listingVerdicts:setX, the fallback functions go. The real win is freedom.ts: its cost-of-living coefficients become a per-market authored assumptions ledger (rural Delta ≠ urban), while the amortization and the single canonical freedomNumberFor stay a guarantee so site and extension never disagree. Keep untouched: the tally/aggregators (they re-derive headline-from-cards so an authored summary can't desync), idempotency, deploy-key gating, schema CRUD.

Done: the four verdict fallbacks are deleted (cells author); the Freedom Number reads per-market assumptions the agent authored; every guarantee (tallies, single-source compute, idempotency) is untouched.
WS2·BOUNDARYSplit / merge / fork protocolmotherL

Give the market a staff Slack channel at birth (the operator hat, like the advisor's). When it judges it should split / merge / fork it posts a plain-language proposal there with its evidence — no coded threshold. On approval, mother's tend-market-boundary skill acts — re-homing buyers transactionally (roster + intake + markDead, ordered, reversible from the demand history) so a half-finished split never orphans a buyer.

Done: the market surfaces a split proposal to staff Slack; on a nod, mother births a sibling and re-homes a test buyer reversibly.
WS2·MARBLEThe anomaly pathmarketM

A single unclaimed zero is served one-off by the nearest market (no new cell, flagged confidence). When the nearest market judges the repeated pull toward Marble is real, it surfaces an emergent-market proposal to staff Slack — no counter, no threshold. Framing correction: this is the attention-budget / economic-coherence test for a place, not the dream-test (which promotes a skill to an agent) — kept distinct so the two never get conflated.

Done: one Marble zero gets a one-off brief with no new cell; sustained Marble interest surfaces as a staff-Slack proposal mother can act on.
WS2·TWINZero → twin, born on scanConvexL

Wire the missing half: a completed in-person visit (photos, walkthrough) fires the twin doorbell, idempotently. The twin registers as the third roster member — event-born, short-lived. The market hands off the parcel bundle, and the brief the market already authored is the system-of-record the twin inherits and writes back to. Ship the trigger + doorbell + handoff plumbing and a stub birth; the full twin cell is a separate build.

Done: a scan fires the twin doorbell exactly once; a twin registers and inherits the home record; nothing fires on a non-scan status change.

The seven corrections the red-team caught — folded in

1The kitchen. Phase-B now mints a fresh Stoolap store before the first zero — birthing a market is three jobs, not one. (WS1·KITCHEN)
2Twin trigger contradiction. The spec says zero-in; the decision is scan. Reconciled in home-twin.md before anything builds to it. (WS0·MODEL)
3Advisor independence. The advisor births first and never blocks on a market — preserving “works day one anywhere.” (WS1·BIRTH)
4Manifest vs. operational state. Boundary and live metrics are separate objects, one writer each — no write race on every pulse. (WS0·REG)
5Jurisdiction is shared (in Convex), memory is private. The parcel layer two overlapping markets both read lives in Convex, keyed by jurisdiction — agents share at the Convex layer; the wiki, embeddings, and dream stay market-private in Stoolap. (WS1·CONVERGE)
6Demand signal in code, not prose. Advisor questions emit a demand row as a side effect of the call — the boundary can't depend on the model remembering to log. (WS0·DEMAND)
7The ten-minute budget is measured. Instrumented, with a warm-egg fallback — the headline can't rest on an unmeasured Phase-B. (WS1·EGG)

05Decisions for Pete

The calls that change the build.

Most of these just got answered. Here's what's settled — and the few that are still open.

Settled this round

No deterministic thresholds — grow / consolidate / promote are the agent's judgment, surfaced to the staff channel; mother executes. v0 geometry = a ZIP list (polygon later). Shared data lives in Convex — agents share at the Convex layer. Advisor namespace = zero-advisor-pete is canonical; tend- is a different project, left alone.