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.
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.
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.
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".
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":
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 items | Extracted total | Gate | Confidence | Result |
|---|---|---|---|---|---|
| Simon-Colon · 93356118 | 9,645.87 | 9,645.88 | ties (≤ tol) | 0.82 | ok · auto-posted |
| Andrade Inc · 57567230 | 1,800.00 | 1,980.00 | off by 180 (the tax line) | 0.57 | routed to review |
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:
| Model | Accuracy | Cost / 1,000 invoices | Routing |
|---|---|---|---|
| Qwen3-VL-32B (open) | 98.8% | S$0.44 | 20 ok · 0 review |
| Gemini 2.5 Flash-Lite | 97.9% | S$0.53 | 19 ok · 1 review |
| Nemotron Nano VL (free) | 97.2%* | S$0.00 | sampled |
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.
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:
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
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.
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.