case study · check-in 01in progress

The Budget Engine: trust as an architecture decision

A household finance engine built in five days of commits: stdlib-only Python, every number traceable to a source row, and a decision log that records the wrong turns on purpose.

pythonsqlitelaunchdemail
project
Budget Engine
published
2026-07-27

In late July 2026 I built a household budget engine: Python that ingests three differently-shaped bank feeds, reconciles them into one canonical ledger, applies sixteen categorization rules that exist as tested code rather than intentions, and emails the household a scorecard every Friday at 8 AM with no human in the loop. Thirty-seven commits over five calendar days, 181 tests, zero dependencies beyond the Python standard library. There is no web dashboard yet. That's deliberate, and it's why this article series exists: the authenticated web view is the committed next chapter, and it's being built as part of this site's V2.

Why build it at all

The problem wasn't tracking spending, plenty of tools do that. The problem was trust. Aggregator feeds disagree with bank archives, categories drift, duplicates double-count, and a budget you can't audit is a budget you quietly stop believing. So the design constraint that shaped everything was that every number traces to a source row, and every rule is tested code. No report ships until reconciliation is complete. That gate was decision #10 in the project's append-only decision log, and it held.

The architecture in one breath

  • Three ingest paths (an aggregator sheet, a bank CSV archive, and a second archive that signs every amount the opposite way) normalize into one canonical row shape at the loader boundary.
  • A strict-match merge engine collapses cross-source duplicates, with a human review queue for anything it refuses to decide.
  • Sixteen numbered rules classify every row; human overrides always beat rules.
  • A weekly scorecard computes pace against a handful of household targets and emails itself via launchd every Friday morning. A refresh failure doesn't block the send; a send failure is loud.
  • Everything is standard-library Python. No pandas, no pip, no venv. A few thousand rows don't need a dataframe, and a dependency-free engine has nothing to bit-rot over a ten-year horizon.
  • The email HTML is written for exactly two iPhones on a Friday morning: tables not divs, inline styles, status as a word in a chip, never color alone.

The decision log is the real product

The engine ships with 26 numbered decisions (FD-001 through FD-026), append-only, each recording the trade-off and not just the choice. A few that taught me the most:

Amend the rule, don't delete it

Early on, payments to a store card that couldn't be linked were counted as groceries, because the payoff was the only signal that existed. When the card finally linked, the obvious move was to delete the rule. Instead it was scoped to a date range: the old behavior holds for the period where it was the only truth, and reverts after the cutover. Deleting it would have silently understated a year of groceries, and a category total gives you no hint that it happened.

Record the compromise instead of making it in passing

The engine's stated outcome was "no AI agent in the loop." Then I added one bounded AI call: a single generated insight sentence per scorecard block. That's a change to the project's outcome, so it's logged as one: the model receives numbers already computed and can't change any of them, failures render a visible "Insight unavailable" rather than being silently dropped, and every payload goes to an audit log. The first live call produced a false explanation, right numbers and wrong time window, caught because, as the commit put it, "the insight path had never actually run. Only the failure path was proven."

The decision deliberately not made

Amazon still has no category rule, on purpose. "What category is Amazon" is a household question, not a coding question, and inventing a category to make the code tidy would have pre-empted it. The docs self-assess that target as the least trustworthy of the three. Leaving a decision visibly open beats hiding it inside a plausible default.

The bugs that taught the most

The merge review queue started at 517 entries and ended at exactly one, a genuine collision that must never merge. The path there included the best bug of the project: the retention logic, which carries forward rows that age out of a feed's rolling window, couldn't tell a merge from an age-out, so every time the merger collapsed a duplicate, retention faithfully resurrected it. The ledger came back from a dedup run one row larger than it went in. One transaction had quietly reached seven copies. The class of mistake: two subsystems answering the same question ("does this row still exist?") against different sources of truth.

And one more, because it's the safety net working: the mortgage was miscategorized as household consumption for eleven months of history. The prose rules always said debt service, but the code never named the servicer, and an upstream feed's own labels had been hiding the gap. It surfaced when the "running hot" anomaly detector flagged the category. The fix was framed in the log as "the note is canonical and the code was wrong, so this is the code catching up."

What it looks like today, honestly

Runs hands-off: a daily cloud-side export, a Friday-morning scorecard, failure alerts on two layers. Still manual or missing: the trip-spend view, the Amazon decision, and most relevantly, any web UI at all. The authenticated finance dashboard on this site is committed in the project's own backlog, sequenced last on purpose: it carries hosting, auth, and data-exposure decisions that the project's architecture rules require to be made as a formal decision record with a privacy review, not as a feature ticket. That work is now underway as part of this site's V2, and it's what the next check-in in this series will cover.

Built with Claude as co-author on nearly every commit. Directed decisions, logged trade-offs, and tests were the human contribution the log can prove.

← all Budget Engine check-ins