Get in touch
All systems

// teardown

The valuation data model

A home value you can re-derive.

in production·2025 – present
ConvexTypeScriptFRED APIPlaid LiabilitiesWorkOS

Inputs

Fed data feed
Tiered refresh
Plaid sync
↓

Transactional core

Domain schema
Worker pool
Valuation ledger
↓

Read path

Cached read rows

prior factor seeds tomorrow's value

// overview

A home's value moves every day, and an owner should be able to see exactly why. bartr recomputes a value for every tracked home each morning from market comps and Federal Reserve data, pairs it with the mortgage balance Plaid reports overnight, and keeps every term of the math, so any number on screen can be re-derived.

// how it works

01 · feed

Fed data, once a day

Early each morning a cron pulls five Federal Reserve series from FRED: the S&P 500, the NASDAQ-100, 30-year conventional and FHA mortgage rates, and one-year inflation expectations. Ninety-day moving averages are computed from bartr's own stored history, and the day's stock and rate impacts are calculated once and written to a single market data row. Rates carry most of the weight, and rising rates pull values down. Market history is never purged.

02 · budget

Base values, refreshed by engagement

Every valuation orbits a base value from market comps, and that data is bought per request. So refresh follows engagement: claimed and listed homes are due weekly, recently sold homes and homes drawing buyer knocks every two weeks, off-market homes monthly on a county rotation, and never-enriched addresses not at all. A daily sweep demotes homes that go quiet. Before a tier schedules anything it checks the month's request budget, and past 95 percent only the hot tier runs.

03 · model

One schema, split by domain

Those inputs land in one Convex schema of 165 tables, written as 20 domain modules (property data, valuation, Plaid, CRM and more) and spread into a single definition. Tables are grouped by what changes together in a transaction, so adding one touches one file. Two conventions wrap it: authenticated functions go through factories that inject the signed-in user, and soft-deletable tables share one pair of fields and one set of helpers.

04 · fan out

A worker pool, not a loop

An hour after the feed, the valuation scheduler pages through every live home with a base value, skips any already valued today, and splits the rest into batches of 50. Each batch becomes its own small mutation, queued at once through Convex's scheduler, and the scheduler hands off to a fresh copy of itself every 500 batches. One job row per date counts batches and homes as workers finish, and a failed run alerts Slack.

05 · record

Every term is stored

Each worker prices a home as its base value times one plus a weighted, smoothed economic factor. Smoothing is 80/20 against that home's previous factor, with no daily cap, so a noisy day is damped and a sustained move still shows through. The row keeps the base value, both market impacts, the raw and smoothed factors, the prior value, the daily change, the equation as text and its inputs as JSON, linked to that day's market data.

06 · equity

Equity from overnight balances

Overnight, a second pool syncs every Plaid connection that is active or retryable: batches of 10 actions, staggered a second apart, each calling Plaid Liabilities for the current balance, payment and rate. A fresh balance schedules an equity calculation against the home's latest value, stored as a dated row of equity, loan-to-value and payoff pace. Plaid's webhook never writes balances. It only updates connection health, so the sync stops retrying links a homeowner has to repair.

07 · serve

Reads hit a cached row

The mutation that writes the history row also updates one summary row per home: the current value, when it changed, and the daily move. Property pages, search results, equity math, the morning digest email and the AI assistant all read that row with one indexed lookup instead of scanning history. The history itself stays behind an ownership check: anyone else sees a claimed home's last seven days, while owners and admins see the full record.

// decisions

The parts that were a call, not a default — and what the alternative would have cost.

chose

Convex crons, scheduler and transactions

not

Postgres with a separate job queue and worker fleet

The whole daily pipeline is platform primitives: 47 scheduled jobs declared in one typed file, fan-out through the built-in scheduler, and mutations that are transactions. The history row and its cached summary commit together, and an audit event commits with the write it describes. For a solo build, that removes a queue, a worker fleet and a cache-invalidation layer that would otherwise need operating.

chose

Cached summary rows on every read path

not

Deriving today's value from history at read time

A home's value changes once a day but is read on every page view, search result, digest and assistant turn. Paying for it once, inside the worker's own transaction, keeps each read a single indexed lookup, and keeps the history table append-by-day and ownership-gated instead of scanned by every list view.

chose

Exponential smoothing on the daily factor

not

A hard cap on how far a value can move in a day

A cap treats a real trend and a noisy day the same way: both hit the ceiling and flatten. An 80/20 blend with the prior factor damps a single volatile day, lets a sustained shift in rates reach the price, and keeps every value anchored to its market-comps base.

chose

Plaid's webhook as a health channel only

not

A second refresh path that writes balances on update events

Mortgage balances move monthly, and the nightly sync already re-reads every connection. Acting on update events would buy hours of freshness at the cost of two writers racing on the same record. What the webhook uniquely knows is whether a link needs the homeowner, so connection health is the only thing it writes.

chose

Domain modules merged into one schema

not

A single schema file holding every table

Grouping tables by what changes together makes each module a review boundary, and a new table touches exactly one file. Merging them into one definition keeps cross-domain references typed and transactions whole: a single valuation mutation writes the valuation module's history row and the property module's summary row atomically.

// guarantees

Re-derivable values

Every valuation row stores its inputs, factors and equation, and market history is kept, so any past number can be recomputed and explained.

Safe to re-run

One job row per date, and writes keyed by home and day. A rerun skips homes already valued and updates an existing row instead of adding a second.

One writer per balance

Only the nightly sync writes mortgage balances. The webhook changes connection health, and a job guard keeps two syncs from running at once.

// by the numbers

165
tables in 20 domain modules
5
Fed series behind every valuation
80/20
smoothing, no daily cap

What it did

bartr case study

// let's talk

Let's build something.

Have a product that needs both sharp strategy and someone who can ship it? I'd love to hear about it.

Get in touch

Command Palette

Search for a command to run...