--- title: "Building a bank statement parser that beats AI models by 17%" description: "A frontier model reads a bank statement about as well as a careful intern. Here is where that breaks, and what we built instead." date: 2026-01-05 category: "Deep Dive" author: "The Moneyline team" reading_time: "4 min read" tags: ["Parsers", "Benchmarks", "Accuracy"] canonical_url: https://moneyline.co/blog/bank-statement-parser-benchmark --- # Building a bank statement parser that beats AI models by 17% > A frontier model reads a bank statement about as well as a careful intern. Here is where that breaks, and what we built instead. Deep Dive · Jan 5, 2026 · 4 min read · Source: https://moneyline.co/blog/bank-statement-parser-benchmark The obvious way to parse a bank statement in 2026 is to hand the PDF to a multimodal model and ask for JSON. It works surprisingly well, right up to the point where it does not, and the failure mode is the problem: the model does not know it got it wrong. ## Where general models fail Across our evaluation corpus, frontier models handle single-page, well-printed statements with clean transaction tables at high accuracy. Four situations degrade them sharply: - **Page-spanning tables.** A transaction table that continues across a page break loses its header. Models routinely re-read the continuation as a new table and drop or duplicate the boundary rows. - **Unsigned negatives.** Some institutions print debits without a minus sign and disambiguate by column. A model reading the number alone gets the sign wrong, and the sign is the entire meaning. - **Multi-account statements.** One PDF, three accounts, three balance sequences. Models tend to flatten them into one ledger. - **Long documents.** At 40 pages and 900 transactions, output quality falls off well before the context limit does. Rows go missing from the middle. ## The benchmark We measure transaction-level F1: a transaction counts as correct only when date, description, amount, and sign all match. Partial credit hides exactly the failures that matter downstream. | Approach | Transaction F1 | Balance reconciles | p50 latency | | --- | --- | --- | --- | | Moneyline parsers | 99.4% | 99.1% | 1.2s | | Frontier multimodal model | 82.1% | 61.4% | 8.4s | | Frontier model, page-chunked | 84.9% | 68.0% | 14.1s | | Generic OCR plus heuristics | 71.6% | 40.2% | 3.0s | *1,412 statements, 312 institutions, held out from all parser development. Higher is better.* The 17% headline is the transaction F1 gap against the better model configuration. The column that actually changed how we build is the third one. ## Reconciliation as a correctness oracle A bank statement carries its own checksum. Opening balance plus the sum of transactions must equal closing balance. That identity is printed on the document, which means a parser can grade itself on every single input with no labels and no human. *packages/parsers/bank-statement/reconcile.ts* ```typescript /** * The statement asserts its own arithmetic, so we check it rather than * trusting extraction. A drift of one cent is rounding; anything larger * means a row was missed, duplicated, or read with the wrong sign. */ export function reconcile(statement: ParsedStatement): Reconciliation { const summed = statement.transactions.reduce( (total, t) => total + t.amountMinor, statement.openingBalanceMinor, ); const drift = summed - statement.closingBalanceMinor; if (Math.abs(drift) <= 1) return { ok: true, driftMinor: drift }; return { ok: false, driftMinor: drift, // A drift that equals twice some transaction's amount is the // signature of a sign error on that transaction, not a missing row. likelyCause: statement.transactions.some( (t) => t.amountMinor * 2 === drift, ) ? 'sign_error' : 'missing_or_duplicate_row', }; } ``` This is why the balance column matters more than the F1 column. A parser that reconciles 99.1% of the time tells you which 0.9% to look at. A model at 68% is wrong about whether it is wrong, and every output needs a human. ## Layout first, text second The design follows from the failure list. We reconstruct the page geometry before reading any text: detect column bands by x-position clustering, detect table continuation by matching band geometry across pages, and only then attach glyphs to cells. Header text is a hint, never the source of truth, because a third of statements label their columns unhelpfully or not at all. > The model was not failing at reading. It was failing at bookkeeping, and bookkeeping has rules you can check. ## Where models still win Two places, and we use them there. Transaction descriptions are unstructured merchant strings, and a model normalizes `SQ *BLUE BOTTLE 4471` to a merchant far better than a rule set. And for a layout no parser has seen, a model produces a usable draft where our parser correctly produces nothing. So the pipeline is not parser-versus-model. Deterministic extraction handles structure and arithmetic, where being checkable matters. Models handle language, where it does not. The 17% comes from refusing to use one for the other.