WTWilly Tai
Case studyData & Knowledge

Every invoice, any layout, into one register you can trust.

A pipeline that reads an invoice, whether it arrives as a PDF or a scan and in any vendor's layout, into structured fields with a cheap vision model. It then re-checks the arithmetic in deterministic code and routes anything that doesn't reconcile to a human. Measured at 98.8% field accuracy for about 44 cents per thousand invoices. This page walks through how it works, where the hard parts are, and what it takes to trust the output.

01 / The pain

Someone keys the invoice by hand, and every vendor's invoice looks different.

Accounts payable is one of the last places where a person still retypes numbers off a document into a system: invoice number, dates, every line, the tax, the total. It is slow, it is where typos become payments, and it doesn't scale, because every supplier's invoice has a different layout. The traditional fix is a per-vendor template or an OCR rules-engine. That works until the next new supplier arrives, then it breaks. And when teams reach for AI here, they tend to do the one unforgivable thing: they let a language model read the numbers off the page and trust whatever it returns, with no independent check.

02 / Why the naive approach fails

"Write a parser per vendor" and "just trust the model" both break in production.

There are two naive routes and they fail in opposite, specific ways:

So the real job is not "read the document". It is to read it with something layout-agnostic, then prove the read is internally consistent before anyone trusts it, and route what can't be proven to a human.

NAIVE ✗ one template per vendor → breaks on new layouts ✗ OCR-first → line-item tables mangled ✗ trust the LLM → hallucinated totals, dropped lines REFINED · read layout-agnostically, then re-check in code PDF / scanany layout vision modelimage → strict JSON reconciliation gatecode · sum = total? registeror → human
fig.1 · why templates and blind-trust fail, and the read-then-verify design that replaces them
03 / How it works

The model reads the pixels. Deterministic code decides whether to trust it.

This is the load-bearing decision of the whole system. A vision model is the only part that touches the document. It reads the image (PDFs are rendered first) into one strict JSON object, told to copy values verbatim and never invent. Everything downstream is plain Python: amounts are normalised to real numbers, the arithmetic is re-checked, and a routing decision is made. A model never gets to declare an invoice "clean".

MODEL · reads pixels, returns JSON DETERMINISTIC · re-checks & decides, reproducible IngestPDF→img · JPG · PNG Vision modelstrict JSON · temp 0 · never invent Normaliseamounts → floats Reconcilesum = total ?required fields ? Routeok / review / abstain Register · register.csv / register.jsononly invoices that reconciled land here automatically Human reviewanything that didn't tie
fig.2 · the trust boundary: the model reads, code re-checks and decides where each invoice goes

The gate itself is small and explicit, and it does the real work. Two checks decide the routing, and nothing is left as "probably fine":

abs( Σ line_items.total − total ) ≤ max(2% × total, 0.05)→ reconciled? invoice_number, total, seller all present→ complete? ok review abstain both pass → ok · a check fails → review (confidence dropped) · nothing extracted → abstain every field is also kept with its source document, so a reviewer sees exactly what was read and from where
fig.3 · the hard part: a two-check reconciliation gate turns an extraction into a routing decision
04 / One worked example

The invoice the gate caught, and one it correctly waved through.

Take the invoice the model got slightly wrong. It read a line item as 1800 while the printed total read 1980, off by exactly the 180 tax line. The model had no idea it had erred. The reconciliation gate did: the items don't sum to the total, so it dropped confidence to 0.57 and routed it to a human instead of writing a wrong row. Compare a clean one, where the items sum to the total to the cent, so it reconciles and posts automatically.

InvoiceΣ line itemsExtracted totalGateConfidenceResult
Simon-Colon · 933561189,645.879,645.88ties (≤ tol)0.82ok · auto-posted
Andrade Inc · 575672301,800.001,980.00off by 180 (the tax line)0.57routed to review
This is the entire argument. The model is good but not perfect: about 1 in 50 fields is off. Deterministic arithmetic catches the consequential ones, because a misread line item almost always breaks the sum. You are never asked to trust the model on faith. Each invoice has to prove that it ties, and the rest goes to a person.
05 / Numbers & honest trade-offs

What it scored, and what those numbers do and don't prove.

20
invoices, 20 layouts
Held-out, each a different seller.
98.8%
field accuracy
Best model, vs known ground truth.
S$0.44
per 1,000 invoices
Real token cost, cheapest model.
1
routed to a human
The one that didn't reconcile.

The cheap-model bake-off, run on the same 20 invoices, scoring accuracy against ground truth against real cost. Native-image reading, no fine-tuning, no per-vendor templates:

ModelAccuracyCost / 1,000 invoicesRouting
Qwen3-VL-32B (open)98.8%S$0.4420 ok · 0 review
Gemini 2.5 Flash-Lite97.9%S$0.5319 ok · 1 review
Nemotron Nano VL (free)97.2%*S$0.00sampled

Where these come from, honestly. The invoices are a public synthetic dataset with published ground-truth fields, so I can measure accuracy field by field. What the figures prove is that the mechanism generalises across 20 unseen layouts, not that it survives one specific real supplier base. The free model is starred because it is rate-limited and was sampled on only 3 invoices: capable, but not for batch. Per field, the weak spots are line-item totals (90%) and long addresses (95%); invoice number, dates and seller are 100%.

The trade-off you actually tune is the reconciliation tolerance. Tighten it and more invoices route to review, which means fewer wrong rows posted but more analyst time. Loosen it and more auto-post, which is faster but lets a small misread slip through. The engine never pretends a high score means "trust it". It posts only what ties, and the tolerance is the dial that sets how much lands on a person's desk.

06 / Governance, shown, and its honest boundary

The decision to trust each invoice is a record, not a vibe.

Every invoice leaves the pipeline with a structured verdict: the routing decision, the confidence, whether it reconciled, and the specific reason if it didn't, alongside the fields kept against the source document. That verdict, not the model's say-so, is what decides whether a row posts. Here is one, verbatim from the run:

Routing record · one invoicedeterministic · reproducible
file        : andrade-inc-57567230.pdf
status      : review            confidence : 0.57
reconciled  : false            line_item_sum : 1800.00   total : 1980.00
flag        : "line items sum to 1800.00 but total reads 1980.00"
required    : invoice_number ✓  total ✓  seller ✓
decision    : route to human , do not auto-post
Honest boundary: this engine's control is the reconciliation gate, the routing, the provenance and a human queue. It is not yet wired into the hash-chained AI Control Ledger that governs the finance agents. That overlay is on the roadmap. What's shown here is what the code actually does today.
07 / Old vs new

The same data entry, re-engineered.

TRADITIONAL · hand-keyed, per-vendor templates read & retype each field new layout → new template typos become payments RE-ENGINEERED · upload any layout, minutes any layout · no template 99% fields · S$0.44 / 1k every row reconciled or flagged
fig.4 · from hand-keyed per-vendor work to any layout, reconciled, in minutes

What this says about how I work.

I don't point an AI at a document and trust the output. I draw the trust boundary first. The model reads the pixels, but a small piece of deterministic code decides whether to believe each read, by checking that the invoice ties to itself. Then I confront the hard parts out loud: the layouts that defeat templates, the misread the arithmetic catches, the tolerance that controls the workload, and the honest line between the gate I built and the ledger I haven't wired in yet. The model is good but not perfect, and that is fine, because it never has the last word. Deterministic arithmetic checks every read, and the one that doesn't tie goes to a person. The cheap model does the seeing, and the cheap arithmetic decides whether to believe it. That division of labour is the whole design.

See it run.

The numbers above are from a live run over 20 held-out invoices, measured against published ground truth, and fully reproducible from the documented code and dataset. There is also an upload app: drop in any invoice (PDF, JPG or PNG) and watch it come back as structured fields, line items and a reconciliation verdict.

Back to the work →The upload app and the full reproducible pipeline run on my machine. Ask me for a 90-second walkthrough or a screen-share.