Home / Blog / Engineering

Engineering

Verifiable document extraction: evidence, a second reader and checks you can edit

Vision-language models read documents well, but a confident wrong number is worse than no number. How aiXtractor makes every extracted value checkable, and why the checks live in prompts rather than code.

13 October 2026 · 9 min read

Modern vision-language models can read a bank statement, an invoice or a customs form with no template and no training. Hand one a page, describe the JSON you want, and it gives you back clean, structured data. That is a real step forward from template OCR.

It also creates a new problem. When template OCR failed, it failed loudly: a field came back empty or obviously garbled. When a language model fails, it often fails fluently. A 5 becomes a 50, a negative in brackets loses its sign, a row at the bottom of page two disappears, and the JSON is perfectly well formed. Nothing in the output tells you which values to trust.

For finance documents that is the whole problem. A bookkeeper does not need a number; they need a number they can defend. aiXtractor is built around one idea: every extracted value should come with the means to check it. In practice that means three layers.

Layer 1: Evidence: where on the page did this come from?

Each value aiXtractor extracts is returned with a bounding box: the page number and the rectangle the value was read from. Not one box per row, but one per value, so in a ten-column table the -5 in the last column has its own box.

This matters for two reasons.

It makes review fast. A reviewer clicks a value and the viewer jumps to it. Checking a sample of twenty values takes a minute, not a careful side-by-side read of the whole PDF.

It makes some errors visible on their own. A value whose box lands on the wrong row is suspicious whatever the value is. A value with no box at all could not be found on the page as written, which is worth a look.

Generative models are good at reading text and unreliable at reporting coordinates, especially in dense tables. So aiXtractor does not ask the model where a value is. It matches each extracted value to the words actually on the page: the PDF's own text layer when there is one, which gives exact positions, or OCR word positions for scans and photos. Candidates are scored by where they sit relative to the row and column the value belongs to, so a -5 is matched to the right -5 and not to the one in the row above. When a value cannot be matched reliably, no box is shown. No box is better than a wrong box.

Layer 2: A second reader

The second layer is independent re-reading. After extraction, a validation pass gives a model the page image and the extracted JSON and asks it to read every field again for itself, returning its own value and a confidence for each one.

The design detail that matters: the validator is asked for its own reading, not a verdict. A prompt that says "is this value correct?" invites agreement, because a model shown a plausible answer tends to accept it. A prompt that says "what does the document show for this field?" gives two independent observations, and the code compares them. Where they agree, confidence goes up. Where they disagree, the field is flagged with both values, the document's and the extraction's, side by side.

Optionally, the second reader can be a different model. Two models from different families rarely make the same mistake on the same digit, so cross-model agreement is strong evidence and cross-model disagreement is a precise pointer to the values a person should look at.

The per-field confidences also give an honest overall score: the average of what the validator actually checked, not a number the extraction model reported about itself.

Layer 3: Document checks: does the document agree with itself?

Financial documents carry their own redundancy. A bank statement's running balance must chain from row to row. An invoice's lines must add up to its subtotal. A card statement's summary must match its transactions. These constraints catch errors that no amount of re-reading will, such as a row dropped at a page break, because the arithmetic stops working.

The obvious way to implement these checks is in code: a function per document type that adds up the columns. We started there and moved away from it, for a reason that applies to anyone building in this space.

Hard-coded checks encode one customer's idea of the document. A bank statement check written for one layout assumes there is a running balance, that debits and credits are separate columns, that the period is printed. The next customer's statements have none of those. A lender wants to check that every transfer over a threshold has a reference. An auditor wants a check nobody anticipated. Each requirement becomes a code change, a deploy and a new edge case.

So in aiXtractor the checks are written in plain language in the validation prompt for each document type:

DOCUMENT CHECKS:
1. Balance reconciliation: opening_balance + sum of all credits
   - sum of all debits = closing_balance.
2. Running balance: for each transaction row, previous row's
   balance + credit - debit = this row's balance.
3. Statement period: every transaction date is within
   statement_period_start and statement_period_end.

The model works through each rule against the extracted JSON, showing its working, and returns a result per rule: pass, fail or warn, with the expected value, the value found and the field it points at. The code's only job is to parse those results and show them. Adding a rule is editing a sentence.

There is an obvious objection: language models are not calculators. Two things make this workable. The model is asked to compute step by step and show its working, which makes arithmetic errors rare and visible when they happen. And the check is not the only line of defence: it sits alongside the evidence boxes and the second reading. A false alarm costs a reviewer one click; a missed error costs much more.

Keeping the prompts honest

If the prompts are the product, they need testing like code. Preset prompts in aiXtractor are tested against an evaluation corpus: generated statements and invoices with known ground truth and deliberately planted errors, such as a misprinted running balance, a duplicated row or a total that does not add up. For each prompt version we measure:

  • Field accuracy: does the extracted value match ground truth?
  • Check recall: of the planted errors, how many did the document checks catch?
  • False alarms: how many checks failed on documents with nothing wrong?

A prompt change ships only when it improves those numbers without regressing others. Because the prompt also carries its own settings, such as image resolution and output length, a change to how a document type is read is a single reviewed edit, not a code release.

What this adds up to

Put the three layers together and each extracted value arrives with:

  1. Where it came from: a box on the page.
  2. Whether an independent reader agrees: a confidence and, if not, the alternative reading.
  3. Whether the document agrees with itself: the checks that involve it.

A reviewer no longer reads the whole document. They look at the handful of values that the evidence, the second reader or the arithmetic has flagged, and they confirm or correct each one in a click. That is the difference between extraction that is fast and extraction you can sign off.

If you want to see it on your own documents, the free plan includes 50 pages a month.

Keep reading

Try it on your own documents

50 free pages every month. No card required.

Start free