Statement AI is live  ·  Reconciling Canadian PDFs to the cent

Guide

Why we replaced 6 hand-coded bank parsers with one AI

Brittle regex, layout drift, and the day Statement AI ate the per-bank template parsers. Why one parser scales better than six.

The Balancely Team7 min read

For the first eighteen months of Balancely, we maintained six bank-specific parsers. One file per issuer: rbc.ts, td.ts, scotia.ts, bmo.ts, cibc.ts, canadian-tire.ts. Each was a tower of regular expressions, layout heuristics, and special cases for the three different ways that issuer's statement might render across a year.

It worked. It also broke roughly every six weeks, in unpredictable ways, on customers we couldn't reach.

This is the story of how we replaced all six with a single Statement AI extractor backed by an LLM, what we lost in the migration, and why the tradeoff was worth it.

The template parser era

The first parser we wrote was for Canadian Tire Bank, because that's the statement the founder happened to be expensing the week we started. It was about three hundred lines of regex and column-coordinate heuristics, lifted from pdfjs-dist text positions. It was deterministic, it was fast, and it was correct — for the exact layout we'd trained it on.

Then Canadian Tire moved a column. Two pixels left, on statements issued after a certain date. Our amount-column regex stopped matching. The fix was a one-line tolerance bump, but the bug had been silently producing workbooks with one missing transaction per page for two weeks before a customer noticed.

We added regression tests. We pinned PDF samples. We wrote a layout-drift detector that flagged statements whose column positions didn't match our golden samples. By the time we had six banks, the regression suite was 80MB of PDFs and a CI run that took 14 minutes.

Every parser was a fortress. Every issuer was a different kind of war.

What broke, in order

  • RBC: changed the date format from MMM DD to MMM D for single-digit days, breaking our regex anchor.
  • TD: introduced a new “Cash Back Rewards” section that we misclassified as a purchase row, throwing off reconciliation by the rewards-redemption value every cycle.
  • Scotiabank: rendered foreign-currency rows as two visual lines, and our single-line parser only caught the first half.
  • BMO: shipped statements where some pages were rotated 90° in the PDF, which destroyed our column-coordinate logic.
  • CIBC: three different layouts depending on whether the cardholder had a joint account, a balance protection plan, or neither.

We were spending two engineering days a week keeping parsers alive. New issuer support — a common request from customers with Tangerine, EQ Bank, or business-card accounts — was blocked behind that maintenance.

The day we tried Gemini

In early 2026, we ran an experiment: extract a Canadian Tire statement using Gemini with a structured-output schema instead of regex. Same input PDF, same output shape (an array of transactions with date, description, signed amount, section). We didn't change anything downstream — the reconciliation step still ran, the workbook still rendered.

It worked on the first try. It also worked on a Scotiabank statement we'd never trained on. And on a Tangerine business statement. And on an old TD layout from 2019 that our template parser had given up on years ago.

We ran it against the regression suite. The LLM matched the template parsers' output on 94% of statements exactly, and on the remaining 6% the LLM was usually more correct — it caught the rotated-page BMO statements and the joint-account CIBC layouts that our template parsers had been fudging.

The tradeoffs we accepted

Statement AI isn't free, in a few different senses.

  • It's slower. A template parser runs in ~200ms. An LLM extraction with structured output and reconciliation cross-checks runs in 30–90 seconds depending on statement length. We hide this behind a queue and a progress UI, but it's a real cost.
  • It's slightly less deterministic. Two runs on the same PDF can differ in transaction description normalization (“TIM HORTONS #4521” vs. “Tim Hortons #4521”). Amounts and dates are stable; descriptions sometimes drift. Our vendor normalizer downstream washes most of this out.
  • It costs per request. Pennies per statement, but it's a real line item instead of zero.

In exchange, we got: any-bank support, no per-issuer maintenance, no layout-drift bugs, and a single code path to reason about. The reconciliation layer — the part that actually matters for correctness — runs identically whether the extraction came from a template parser or the LLM.

What we kept

We didn't delete the template parsers. They still run as a fast path for the most common issuers when we have high confidence in the layout. If they produce a result that reconciles to the cent, we use it. If they fail or produce a non-reconciled result, we fall back to Statement AI. The LLM is the safety net; the template parsers are the express lane.

Reconciliation is the gatekeeper for both. No matter where the data came from, if it doesn't balance to within 2¢, the workbook says so. That's what we trust — not the parser, but the proof that the parser got it right.

Try Balancely on your next statement.

Two free conversions to start — no credit card required.