All notes

Sep 2026 · 4 min read

Published

Build Log #5: Teaching the App to Read One Real Bank Statement

AuthorKervintz Noel
Filed underAI Engineering · Claude Code · Building in Public · Python
SeriesPart 5 of 11 · View the full case study

Everything before this point — extraction, the job queue — moved bytes around without understanding a word of them. This is the post where the app reads an actual bank statement for the first time, using twelve consecutive months of my own real Santander checking statements as the test data.

Detection before parsing, never the filename

A statement's bank, account type, and layout version are detected from what's actually printed on the page, with a confidence score. Below that threshold, the statement is marked unsupported rather than run through a best-guess parser — a wrong parse that looks plausible is worse than an honest refusal.

The real statements turned up a layout wrinkle no synthetic fixture would have caught on its own: the combined PDF has a checking section followed by a savings section on the same pages. The parser reads checking only and explicitly stops before the savings section — a documented v1 scope decision, not an oversight.

  1. 01Transaction columns are read by the x-position of each amount on the page, not by text order — the columns sit far enough apart that position is a more reliable signal than sequence.
  2. 02A negative, overdrafted balance prints with its sign intact and is preserved exactly.
  3. 03Every parsed row is cross-checked against the running balance, and the parser's own credit/debit totals are cross-checked against the statement's own printed summary line — a mismatch there is treated as a misread, not a statement that merely fails to reconcile.

A parser that's confident and wrong is more dangerous than one that admits it doesn't know. Detection has to be willing to say "unsupported."

What the real data proved

All twelve months processed into 245 real transactions, correctly resolved to a single account — masked to its last four digits. I grepped the database directly for my real, full account number afterward. It came back nowhere. Every month's closing balance reconciled exactly against the next month's opening balance, across the entire year.


Next

Deduplication and the analytics engine — the first things that actually read what this parser produces.

I write these as I go. You can follow along here or on Hashnode, where I'll start cross-posting.

All notes