A governed agent re-derives every procurement exception from the raw records. It works over the whole ledger rather than a sample, cites each exception to the invoice it came from, and never touches a number with a language model. This walks through exactly how it works, where the hard parts are, and what it would take to break it.
An internal auditor reviewing procurement can't read every transaction, so they pull a sample, maybe 25 of a few hundred invoices, and match each by hand against its purchase order and goods-receipt note. It takes weeks, it's point-in-time, and the duplicate payment or the overbilling that matters is, by definition, just as likely to sit in the 90% nobody looked at. When teams reach for AI here, they usually do the one thing you should never do: they point a language model at the ledger and trust the number it returns.
The obvious version of three-way match is a single equality: invoice.amount == po.amount. It would pass a demo and fall apart in a real accounts-payable ledger, because procurement is messy in specific, knowable ways:
So the real job isn't arithmetic. It is a join across three tables with the right keys, the right tolerances, and the right cross-record comparisons, plus an honest decision about where a rule ends and a human's judgment begins.
This is the load-bearing decision of the whole system, so it's worth drawing precisely. Every number and every flag is computed in Python from the joined records, reproducible to the cent, re-runnable, and auditable. The language model sits downstream and read-only. It drafts the request-for-information email about a finding the engine already produced, and it must cite that finding's record IDs or it writes nothing. A model never decides whether an exception exists.
Inside the match engine, each invoice is joined to its PO and goods-receipt note on po_id and run through five checks. The parameters are explicit rather than guesswork:
Take invoice INV-00001. The engine joins it to PO-00001 and GRN-00001, finds the invoiced quantity (32) exceeds the ordered quantity (28), prices the gap at the invoice unit price, and emits a typed, cited exception. Only then does the model draft the email, quoting those exact IDs.
| Invoice | Check that fired | Observation | Value at risk | Evidence | Result |
|---|---|---|---|---|---|
| INV-00001 | Quantity over PO | invoiced 32 units vs 28 ordered | S$3,612.16 | INV-00001 · PO-00001 · GRN-00001 | flagged |
| INV-00088 | Price variance | invoiced @ 0.6% over PO, inside the ±1% band | none | INV-00088 · PO-00088 | correctly cleared |
| INV-90001 | Duplicate scan | same vendor & amount as INV-00011, but 33 days apart | none | INV-90001 · INV-00011 | not a duplicate |
Where these come from, honestly. Origin Company is a synthetic enterprise I generate, with exceptions seeded deliberately. So these figures prove the mechanism works, not that it survives a specific real ledger. The sigma figure is a process-quality index over five opportunities per invoice (805 total). It is illustrative on synthetic data, not a benchmarked claim.
The trade-off every auditor actually buys on is noise. 46 exceptions on 161 invoices is a deliberately high seed rate. In production the lever is the tolerance band. Widen the price tolerance from 1% to a contracted 2.5% and the price-variance count falls sharply, trading recall for fewer false positives. The engine doesn't pretend a flag equals a problem. It produces a cited candidate list a human triages, and the band is the dial that controls how much lands on their desk. A continuous version (see a continuous-monitoring version of this engine) adds the other half: only what's new since the last run reaches the queue, so the daily volume is the delta rather than the whole book.
Every action, from the scope approval to each finding to each RFI, is written to a hash-chained ledger. Each entry's hash covers its contents and the previous hash, so altering any record after the fact breaks the chain at exactly that row. The decision fields are the gate's structured output. The model never writes them.
seq : 17 agent : Finance Audit Bot action : raise_rfi subject : INV-00001 tier : low decision : escalate → control owner evidence : [INV-00001, PO-00001, GRN-00001] prev_hash : 9f2c…a17b hash : d4e8…c012 ← = sha256(prev_hash + canonical(entry))
I don't point an AI at a ledger and trust the output. I draw the trust boundary first, with the calculations in code and reproducible to the cent, and the model strictly downstream and cited. Then I confront the hard parts out loud: the edge cases that defeat the naive version, the tolerance that controls the noise, the near-miss the system must get right. Anyone can sample twenty-five invoices. The harder and more useful thing is to run the control over all of them, name the tolerance that separates a real finding from noise, and keep the model's hands off every number it touches. That discipline is what makes the output worth acting on, not the model.
The artifact above is real output from the engine over Origin's ledger. The full interactive console, with its risk-ranked audit universe, the 46 exceptions, and the RFI drafts, runs as a working application.