All work
Projects/2026

Invoice Exceptions

Role
Design and build
Stack
TypeScript, deterministic parsers with LLM fallback, structured exception classification
What it is
An accounts payable tool where a pipeline settles routine invoices automatically and a workspace makes the exceptions fast to resolve

A portfolio project rather than client work. The reasoning below is the point of it.

The problem

Accounts payable looks like a matching problem. An invoice arrives, you check it against the purchase order and the receiving record, and if everything lines up you pay it.

Watch someone actually do the work and a different picture shows up. Roughly 80 percent of invoices match cleanly and take about two minutes each. The other 20 percent are exceptions, and they take the day. A unit price is off by a few dollars. The quantity billed does not match what was received. The purchase order number is missing or points at a closed order. A vendor added a line nobody ordered.

Each of those means opening the purchase order, finding the receiving record, digging through an email thread, and deciding. The information exists. It is just scattered.

Most AP automation targets the wrong 80 percent. Making a two minute invoice take ten seconds is a real improvement to something that was never the bottleneck.

What I built

Two halves, because the work has two shapes.

A pipeline that runs on arrival. An invoice lands and the workflow fires. It extracts the line items, matches against the purchase order and goods receipt, applies tolerance thresholds, and either settles the invoice or classifies why it could not. Nobody opens anything for the cases that work.

A workspace for the cases that do not. When the pipeline cannot settle an invoice it assembles the evidence and proposes a resolution, then puts it in a queue. The clerk is reviewing a decision rather than starting an investigation.

The exception types it handles: price variance, quantity variance, missing or invalid purchase order, duplicate submission, tax and freight mismatch, unauthorized line items, currency differences, and payment terms that do not match the contract.

Decisions worth explaining

Extraction is deterministic first, with a fallback.

Every vendor formats invoices differently, but the fields underneath are the same. One says Net Amount, another says Subtotal, and both mean the same thing. For vendors that appear regularly, a parser maps their layout to a canonical schema with no model call at all. Cheaper, reproducible, and auditable.

The long tail works differently. A vendor who sends three invoices a year will never justify a parser, and a brand new vendor has no samples to build one from. Those go to an LLM for extraction, a person confirms the field mapping once, and the system writes a template from that confirmation.

The system graduates from model to code as it learns a vendor. Month one, most volume runs through the model. By month six most of it runs through parsers and the cost per invoice has collapsed, without anyone running a migration project. If a layout changes and confidence drops, it falls back to the model and re-graduates.

The model does not do arithmetic.

Matching quantities, applying tolerances, converting currency, comparing dates. All code. If a prompt is asking a model to add numbers, that is a bug. Every run re-derives the same calculation, pays tokens for it, and introduces variance for no benefit. The model's job is reading an unfamiliar document, classifying why something did not match, and drafting the vendor email. Nothing else.

The queue proposes resolutions, not flags.

There is a real difference between these two outputs:

Price variance: $340
Price variance of $340. The purchase order reflects Q1 pricing. This vendor updated their price list on March 1, and the contract permits an annual adjustment. Recommend approve.

The first is detection and it leaves the clerk exactly where they started. The second is resolution. Getting there means assembling the purchase order line, the price history for that item from that vendor, the contract terms, and any prior invoices against the same order, before anyone opens the record.

The human boundary is written down per exception type, in advance.

Under a materiality threshold, resolve automatically and log the reason. Above it, route to a person with the evidence attached. Anything the system cannot classify goes to a person untouched, along with what it found.

That last category matters more than it looks. A system that always has an answer is one that is confidently wrong sometimes. One that says it does not recognize a pattern is one you can trust the rest of the time.

Nothing moves money.

The system proposes, a person approves, the ledger executes. Not because the logic cannot be trusted, but because the failure mode is unrecoverable and the approval costs seconds.

Every decision carries an audit trail.

What the system saw, what it concluded, what a person decided, when, and which version of the logic produced it. In most domains that is good practice. In finance it is the requirement.

What it demonstrates

The measure people reach for is invoices processed, and it is the wrong one. The numbers that matter are on the exception side: time to resolve an exception, the share resolved without opening another system, and money caught that would otherwise have been paid wrong. The duplicate that slipped through. The price creep nobody noticed across six months of small increases. The payment terms that did not match the contract.

That last one is what funds the next project.

Open the demo

A working recreation with synthetic invoices, vendors, and purchase orders. Open an exception to see the evidence assembled before anyone opened the record, and watch a vendor graduate from model extraction to a written parser.

Interactive recreation. Synthetic data and pre-generated outputs. It contains no employer systems, customer data, or production code, and makes no live model calls.

Open demo
EXPLORE MORE WORKAgentic QA WorkflowRebooked OrdersAnvra