Stop LLM Hallucinated Numbers in Bank Statement Extraction Jobs
The spreadsheet came back complete. Every transaction has a date, a description, an amount, and a balance that increments down the column in a way that looks entirely reasonable. Somewhere in there are numbers the model produced rather than read, and nothing in the output distinguishes them from the numbers it got right.
Short Answer
Hallucinated amounts are not distributed evenly across a statement. They concentrate in two places: regions where the scan is too degraded to read confidently, and the running balance column, which a model can compute from context instead of transcribing. Fix this by extracting the balance as an independent observed value, never as a derived one, then walking the balance continuity chain in code. A broken chain converts a silent wrong number into a specific row you can go look at.
Where the Bad Numbers Actually Come From
| Condition on the page | Typical bad output | Balance chain catches it | First move |
|---|---|---|---|
| Low-resolution or faxed scan | Digit substitutions: 3 for 8, 1 for 7, 5 for 6 | Yes, unless amount and balance are misread consistently | Re-render at higher DPI before blaming the model |
| Running balance column present | A balance consistent with the amount the model read, not the number printed | No, this is the case that defeats the check | Extract balance and amount in separate passes, then compare |
| Page break mid-table | Dropped or duplicated rows at the boundary | Yes, the chain jumps by exactly the missing amount | Process page by page with expected row counts |
| Parenthesised or trailing-minus negatives | Sign flips on debits | Yes, the chain moves the wrong way by twice the amount | Assert sign conventions against the column header |
| Locale-varying separators | 1.234,56 read as 1.23 or 1234.56 | Usually, the magnitude error breaks the chain | Pin the locale per document, not per job |
| Skewed or photographed pages | Column bleed, amount in the wrong column | Yes when amounts move, no when only descriptions move | Deskew, then check column boundaries on a sample page |
| Faint carbon or thermal originals | Rows invented to fill a visually implied gap | Yes, invented rows rarely satisfy the chain | Route to human transcription, not more prompting |
Have a messy PDF? Upload 1-3 sample pages and we will tell you if it is clean, OCR-heavy, or needs human QA.
Why the Running Balance Is the Most Dangerous Column
A running balance is the one field on a bank statement that is fully predictable from the fields around it. Prior balance plus signed amount equals current balance. A model that cannot clearly see the printed balance can still emit a number that looks right, because it has everything it needs to compute one.
That is the trap. If the model reads an amount as 421.30 when the page says 421.80, then computes the balance from its own misread instead of transcribing the printed one, the rows are internally consistent. The continuity check passes. The data is wrong and your validation says it is fine.
The defence is to treat the balance as an observation, not a calculation:
- Extract amounts and balances in separate passes, each blind to the other. A pass that only reads the balance column has nothing to derive from.
- Tell the extraction to transcribe the printed balance and return null when it is illegible. A null is useful. A guess is not.
- Compare transcribed against computed for every row. Disagreement points at either the amount or the balance, and you know which row to open.
Structured output modes do not help here. Constrained decoding guarantees the shape of the response, not the truth of the values in it. A schema that requires a number gets you a number every time, including on rows the model could not read.
Making the Chain Do the Work
Run balance continuity as three separate assertions, because each fails differently.
Row-level chain. For every transaction after the first, prior balance plus signed amount equals current balance. Record the first row that breaks. The error is at or just before it, which turns a 2,000-row review into a single lookup.
Statement-level identity. Opening balance plus total credits minus total debits equals closing balance. This fails on any dropped or duplicated row anywhere in the file, including ones a reconstructed row chain would smooth over.
Page-level totals. Where the statement prints per-page subtotals, check the extracted rows against them. This localises a break to one page even when no row-level balance exists.
Log which assertion broke and where. The output you want is not a confidence score. It is a row number.
Confidence Scores Are Not the Answer Here
Filtering on confidence and reviewing everything below a threshold has two problems.
Most general-purpose LLM extraction returns no per-field confidence at all. Token log probabilities, where an API exposes them, measure how certain the model was about the next token, which is a different question from whether the number matches the page.
Document AI products that do return field-level confidence, including Azure AI Document Intelligence, describe it as an estimate of the likelihood a value was detected correctly, and their own guidance is to use it for routing into human review on financial documents rather than as a correctness guarantee. Useful for routing. Not a substitute for arithmetic the document already supplies.
Worth knowing before planning around a specific tool: Azure's prebuilt bank statement schema carries BeginningBalance and EndingBalance plus DepositAmount and WithdrawalAmount per transaction, but no per-transaction running balance field. If you need that column, you are capturing it with a custom model or a separate pass.
Fix the Input Before Tuning the Prompt
When hallucinated digits cluster on particular files, the file is usually the cause. Prompt engineering on an illegible scan produces confident guesses faster.
- Check the render resolution. Standard office documents scan adequately at 300 DPI. Faded originals, bleed-through, tightly spaced characters, and anything that went through microfilm or a fax want 600 DPI or higher. If you rasterise the PDF yourself, this is the cheapest quality improvement available.
- Check for a text layer. Digitally generated statements often have one. Reading text directly removes glyph ambiguity entirely.
- Deskew and crop. Column bleed on a rotated page is a layout failure, not a reasoning failure.
- Split by page. Long tables invite truncation and value propagation down a column. Page-scoped extraction with an expected row count makes losses visible.
When This Approach Does Not Apply
- Statements without a running balance column. Many credit card statements print transactions and a closing balance only. You keep the statement-level identity but lose row-level localisation, so review has to be sampled rather than targeted.
- Offsetting errors. Two amounts swapped between rows with identical balance impact satisfy every check. So does a description misread. Arithmetic validates numbers, not narratives.
- Multi-currency and multi-account files. Run the chains per account and per currency. Mixing them produces breaks that look like extraction errors and are not.
- Carried-forward and adjustment rows. Interest postings, reversals, and holds sometimes sit outside the transaction table but inside the balance. Model these before you trust a break.
When a Tool Beats a Service
If you process the same two or three statement layouts every month, a template-based IDP product or a custom-trained model beats both a general LLM and a managed service on cost per page, because stable layouts are where templates are cheap. If your bank offers CSV, OFX, or QBO exports covering the period you need, use those and skip extraction entirely. Data that was never guessed at needs no validation.
DataConvertPro handles the other case: mixed layouts, scanned and historical statements, closed accounts, one-off backlogs, and banks nobody has templated. We run the balance chains on every job, put a person on the rows and pages that break, and deliver Excel or CSV with the failed checks documented. If a bank export or a template tool solves your problem faster, we will say so.
Frequently Asked Questions
Can I just tell the model not to hallucinate?
Instructions help at the margin, particularly a clear instruction to return null for illegible values instead of estimating. They do not remove the failure, because the model is not aware it is guessing. Treat prompt wording as one input among several and put the actual guarantee in the arithmetic.
Does a second model pass catch these errors?
It catches the ones driven by ambiguity, which is a useful share. Two independent reads that disagree on a cell tell you exactly where to look. It does not catch errors both models make identically, and on a genuinely bad scan they often will, because both are looking at the same degraded pixels.
What if the running balance itself is the hallucinated field?
That is why the balance is extracted in its own pass and compared against the computed value rather than assumed. A disagreement means one of the two is wrong, and you open the page to find out which. The balance column is often the corrupted one, because it is the column a model can most easily fabricate.
How do I know whether my files are the low-quality kind?
Sample ten pages across the batch, render them at your pipeline's resolution, and read them yourself on screen. If you have to squint at a digit, the model is guessing at it too. That five-minute check predicts extraction quality better than any accuracy figure quoted without seeing your documents.
Ready to Convert Your Documents?
Stop wasting time on manual PDF to Excel conversions. Get a free quote and learn how DataConvertPro can handle your document processing needs with AI-assisted extraction and human verification.