Status: accepted. Implemented 2026-08-24 in kevinvu184/up. Builds on ADR 0005 (gold layer) and consumes ADR 0003's silver contract. The monthly mart described here is superseded by ADR 0009's daily grain; the reconstruction decision itself stands.
The Up API has no balance history endpoint: GET /accounts returns each account's current balance only, so any balance-over-time capability must be manufactured. Reconciling live balances against silver sums on 2026-08-24 established the ground truth for how Up represents balance changes. Every change is a transaction except round-ups, which Up records asymmetrically: the debit on the source account is metadata on the purchase (roundUp field, no transaction of its own) while the credit on the receiving Saver is a real "Round Up" transaction. Summing amount_cents alone therefore materially overstated the round-up-enabled account, while accounts without round-ups reconciled to the cent. Adding the round-up metadata back closed nearly the whole gap; a small residue remained, attributed to a closed Saver, because closed accounts vanish from /accounts and their transactions vanish from the transaction feed.
Balances derive from data already in silver rather than from a new accounts sync: the running sum of amount_cents + COALESCE(round_up_cents, 0) over SETTLED rows, per account. The silver schema already carried round_up_cents, so no new ingestion, no new entity, and no new cron were needed. The approximation this buys is accepted; the reconstruction is an estimate, not a ledger.
The expression lives once, as BALANCE_EXPR in models/goldModels.ts. The monthly balance mart builds its SELECT from it and tools/balanceCheck.ts reads the mart's output rather than recomputing, so no second copy of the arithmetic can drift.
gold_monthly_balance (migration 9) holds one row per month per account, from each account's first transaction month through the current month, with the balance carried forward through months that had no transactions. A sparse mart would make "net worth in month M" silently miss any account inactive that month. It rebuilds on the existing gold cron through the same DELETE plus INSERT pattern as the other marts; months bucket on created_at_utc as UTC YYYY-MM, consistent with them.
Net worth and per-account balance history become one-row reads over gold, at the cost of approximation: accounts with round-up history carry a small fixed residue, and net worth before a closed account's closure understates by whatever that account held at the time, which the API can never return. tools/balanceCheck.ts doubles as a drift detector, comparing the mart's latest month against live balances; a delta that moves between runs means a new kind of non-transactional balance change has appeared. Month boundaries are UTC, so figures near the 1st of a month shift relative to Melbourne local months, the same accepted limit as the other gold marts. Should exact balances ever be needed, the upgrade path is an accounts snapshot sync that anchors the curve forward without replacing the historical reconstruction.