statementtool100% FREE

The benchmark, and what it can and cannot tell you

Every converter in this market publishes an accuracy figure from a private test set that nobody else can run. This page is the opposite: the statements, the ground truth, the script that scores them and every result, so that anyone can check the figures and reproduce them.

Read this first. Every statement here is synthetic, generated by a script we wrote, and the engine was built by the same people. A full score on a corpus like that mostly shows the engine does what we thought to test. It says nothing about how any real bank's PDF converts. Until real statements have been through it — with the owners' consent and redacted — there are no per-bank accuracy figures on this page, and the bank-shaped statements below are layouts, not banks.

The method

Each statement is generated with known ground truth: every row's date, description, amount and running balance. The converter's engine reads the statement's positioned text with no hints — no date order, no bank profile — the way the browser reads a file it has never seen. Every field is then compared with ground truth, exactly: an amount one penny out is wrong, and a description with a missing word is wrong.

Each statement also has an expected outcome. Most should reconcile. A statement with no running balance should have every row reported as not checkable, and never as verified. A statement that contradicts itself should be flagged at the contradiction and nowhere else. A scan should be refused, not guessed at.

Results

38 of 38 statements reached their expected outcome. Of 897 rows: 896 amounts, 877 of 877 printed balances, 897 dates and 897 descriptions were exactly right.

The amount that did not match was on the statement built to contradict itself: it prints one figure where its own running balance moved by another. The engine reads what is printed, and flags the row — which is the right answer, and the reason that amount does not match ground truth.

StatementExpected outcomeRowsAmountsBalancesDatesDescriptions
ruled
Ruled table, money in / money out / balance
Met
Reconciled
12/1212/1212/1212/1212/12
unruled
The same layout with no table lines
Met
Reconciled
12/1212/1212/1212/1212/12
wrapped
References wrapped onto a second line
Met
Reconciled
6/66/66/66/66/6
multipage
Five pages with brought- and carried-forward rows
Met
Reconciled
12/1212/1212/1212/1212/12
drcr
One amount column marked DR / CR
Met
Reconciled
12/1212/1212/1212/1212/12
parens
Money out in parentheses
Met
Reconciled
12/1212/1212/1212/1212/12
eu
European number format, 1.234,56
Met
Reconciled
12/1212/1212/1212/1212/12
mdy
US month-first dates, deposits before withdrawals
Met
Reconciled
6/66/66/66/66/6
reversed_cols
Money out before money in
Met
Reconciled
12/1212/1212/1212/1212/12
no_headers
No column headers: direction settled by the arithmetic
Met
Reconciled
12/1212/1212/1212/1212/12
india
Lakh grouping, 1,23,456.78, password-protected
Met
Reconciled
5/55/55/55/55/5
long
240 rows over ten pages
Met
Reconciled
240/240240/240240/240240/240240/240
batch_q1
January to April, first of three overlapping statements
Met
Reconciled
80/8080/8080/8080/8080/80
batch_q2
April to July, overlapping both neighbours
Met
Reconciled
80/8080/8080/8080/8080/80
batch_q3
July to December, password-protected
Met
Reconciled
120/120120/120120/120120/120120/120
split_words
Every word its own text item, day-month dates split in two
Met
Reconciled
6/66/66/66/66/6
sample_chase
Shaped like the Chase layout recorded on this site: MM/DD, no year
Met
Reconciled
12/1212/1212/1212/1212/12
sample_barclays
Shaped like the Barclays layout recorded on this site: DD Mon, no year
Met
Reconciled
10/1010/1010/1010/1010/10
sample_hdfc
Shaped like the HDFC Bank layout recorded on this site: DD/MM/YY, lakh
Met
Reconciled
8/88/88/88/88/8
claim_opening_each_page
Opening balance reprinted on every page (a claim on the Chase page)
Met
Reconciled
12/1212/1212/1212/1212/12
claim_fee_table
A fee table after the closing balance (a claim on the Chase page)
Met
Reconciled
12/1212/1212/1212/1212/12
claim_bilingual
English and French header lines (a claim on the Canada page)
Met
Reconciled
12/1212/1212/1212/1212/12
overdrawn_minus
Overdrawn account, negative balance printed -54.52
Met
Reconciled
12/1212/1212/1212/1212/12
overdrawn_od
Overdrawn account, negative balance printed 54.52 OD
Met
Reconciled
12/1212/1212/1212/1212/12
overdrawn_dr
Overdrawn account, negative balance printed 54.52 DR
Met
Reconciled
12/1212/1212/1212/1212/12
overdrawn_parens
Overdrawn account, negative balance printed (54.52)
Met
Reconciled
12/1212/1212/1212/1212/12
year_end
Across the new year, day-month dates with no year
Met
Reconciled
7/77/77/77/77/7
date_once_per_day
The date printed only on the first transaction of each day
Met
Reconciled
7/77/77/77/77/7
currency_symbol
A currency symbol on every amount and balance
Met
Reconciled
12/1212/1212/1212/1212/12
balance_end_of_day
The balance printed only once a day
Met
Every day adds up; 2 single-row days verified, 5 rows checked only as part of a day
7/77/74/47/77/7
newest_first
Transactions listed newest first
Met
Reconciled
12/1212/1212/1212/1212/12
amount_on_last_line
Amount and balance on the last line of a wrapped entry
Met
Reconciled
6/66/66/66/66/6
two_accounts
Two accounts in one PDF, each with its own balances
Met
Reconciled
12/1212/1212/1212/1212/12
mdy_early_days
Month-first dates all on or before the 12th; only the period decides
Met
Reconciled
4/44/44/44/44/4
nobalance
No running balance column
Met
Every row reported as not checkable, and said so
12/1212/12none printed12/1212/12
claim_dual_dates
Credit card: transaction and posting dates, no running balance (a claim on the Chase page)
Met
Every row reported as not checkable, and said so
5/55/5none printed5/55/5
misprint
An amount that contradicts its own running balance
Met
Flagged the contradicting row (row 31) and no other
60/6059/6060/6060/6060/60
scanned
An image of a statement with no text layer
Met
Refused as a scan; nothing sent anywhere
none printed

What it has caught us getting wrong

The corpus exists to find these. Each one became a permanent test, and none is hidden once fixed.

  • Numbers dropped from descriptions (found and fixed 13 September 2026). “TRANSFER FROM SAVINGS 4417” came out without the 4417, and a cheque number would have been lost the same way. Amounts and verdicts were unaffected, which is exactly why the arithmetic could not catch it: only comparing descriptions with ground truth did.
  • A month at the start of every description (found and fixed 13 September 2026). On statements printing dates as “01 Apr”, where the date arrives as two pieces of text, every description began with “Apr”.
  • A fee table that cost a whole statement its verification (found and fixed 13 September 2026, by drawing the Chase page's own claim as a statement). Fee rows printed after the closing balance were read as transactions, and because they carry no running balance, every row in the statement dropped to “not checked”. The claim on the page said they were excluded; until that day they were not.
  • Posting dates glued to descriptions (found and fixed 13 September 2026, the same way). On a credit card statement with a transaction date and a posting date, the second date became the first word of every description.
  • Every overdraft read as money in the bank (found and fixed 13 September 2026). A negative running balance printed as “-54.52”, “54.52 DR” or “(54.52)” was read as +54.52, flagging correct rows either side; printed “54.52 OD” it was not read at all, and the whole statement fell to “not checked”. The corpus had never contained an overdrawn account.
  • Most of a busy day silently dropped (found and fixed 13 September 2026). On a statement that prints the date only on the first transaction of each day, every later transaction that day was skipped: four of seven rows gone.
  • A month with one deposit lost the deposit (found and fixed 13 September 2026). A money-in column holding a single figure was not recognised as a column, so the only salary payment vanished — and on a statement printing its balance once a day, every sign was inverted as well.
  • Newest-first statements flagged from top to bottom (found and fixed 14 September 2026). A statement listing its newest transaction first had every row flagged, because its running balance runs the other way. It is now put in ledger order when the arithmetic confirms that order, and the result says so.
  • Entries with their figures on the last line lost (found and fixed 14 September 2026). Where a wrapped entry prints its amount on its final line, four of six transactions disappeared.
  • A one-click fix that could turn a missing transaction into “Reconciled” (found and fixed 14 September 2026). When a row was missed, the next row was flagged and the first button set its amount to whatever made the balance add up: a card payment became a deposit, and the whole statement showed “Reconciled” with a transaction missing. A figure is now offered only when it is a plausible misreading of the printed one — a digit, a swap, a decimal point, money in and out — and the flag says which. Otherwise the row stays flagged and the page to check is named.
  • A one-click “fix” that would have corrupted a correct amount (found and fixed 14 September 2026). A PDF holding two accounts was read as one ledger: the first row of the second account was flagged, and the suggested fix would have overwritten its correct amount. Each account is now checked against its own opening and closing balance.
  • DR read as a heading (found and fixed 14 September 2026, by fuzzing: hundreds of random statements checked against ground truth). On a statement without column headings, the “CR” on an amount like “1,450.04 CR” was taken for a column heading, so the whole amount column became “money in” and every DR was read as a deposit.
  • The wrong heading claiming a column (found and fixed 14 September 2026, the same way). Headings were matched in reading order, so “Money in”, 90pt away, could claim the money-out column before “Paid out” directly above it.
  • Batches that crawled (found and fixed 13 September 2026, by the page-limit test rather than this corpus). Ten 500-page statements took six and a half minutes instead of eight seconds.

What it cannot tell you

  • How a real bank's statement converts. Only real statements can. That is the next thing we are doing, and when there are results they go here, losses included.
  • Everything about the reader in your browser. This corpus is scored on text positions extracted with pdfplumber. The browser uses pdf.js, which groups text differently. Separate end-to-end tests run the bank-shaped statements through pdf.js and compare every date and description, but this table is not those tests.
  • Whether a description is right on your statement. Arithmetic proves amounts and balances. Nothing proves payee text except reading it, and the converter says so on every result.

Run it yourself

From a clean checkout of the source, with Node 22 and Python 3:

npm ci
pip install reportlab pdfplumber pikepdf pypdfium2 openpyxl
npm run fixtures     # generate the corpus and its ground truth
npm run benchmark    # score it; writes benchmark/results.json

The generator is scripts/make_fixtures.py and the scoring script is scripts/benchmark.ts. Continuous integration re-runs it on every change and fails if the results on this page are not the ones the current engine produces.