See what it prints before you install it
Every output on this page is copied verbatim from a real run against a real bank statement. Nothing here is a mockup.
$ statementproof january.pdf statementproof -- bank statement extraction that tells you when it is wrong runs entirely on this machine | no network | nothing uploaded opening balance : 69.96 closing balance : 586.71 transactions : 21 extracted, 0 row(s) skipped VERIFIED (21 transactions, checks: chain, aggregate) The extracted transactions reproduce this statement's own balances. That means the arithmetic is consistent. It does NOT mean every description or date is correct -- errors that do not change the totals cannot be detected this way.
$ statementproof january.pdf opening balance : 69.96 closing balance : 586.71 transactions : 21 extracted, 0 row(s) skipped FAILED (21 transactions, checks: chain, aggregate) [chain_break] row 7: running balance does not follow: 722.50 69.00 should give 791.50, statement shows 653.50 (off by -138.00) [aggregate_mismatch] statement: opening 69.96 plus 21 transactions totalling 654.75 gives 724.71, but statement closing balance is 586.71 (off by -138.00) The extraction does NOT reconcile. The row(s) named above are where the running balance stops following. Do not import this without checking them against the PDF.
$ statementproof ./january-statements/ --record january.html 4 statement(s) in ./january-statements/ B_debit_credit.pdf VERIFIED 10 txns D_multipage.pdf VERIFIED 10 txns G_real_impact_bank.pdf VERIFIED 21 txns corrupted_scan.pdf COULD NOT READ (PdfminerException) VERIFIED 3 UNREADABLE 1 <-- not validated; they are not counted as passing wrote batch reconciliation record to january.html
The failing run above is a real extraction with one amount's sign flipped on row 7 — the single most common silent error in statement conversion, and the one that produces a CSV that looks perfectly clean.
The check it is running
A bank statement carries its own checksum. Every row prints a running balance, so each row
must satisfy balance[n] = balance[n-1] + amount[n], and the whole statement must
satisfy opening + Σ amounts = closing.
If a debit lands in the credit column, or a decimal shifts, or a page header is picked up as a transaction, the chain breaks at exactly that row. That is why the output names row 7 rather than saying the file failed.
We are not the only tool that checks this
Two others do, and it would be dishonest to imply otherwise. monopoly is open source and validates totals. Statemently is a hosted converter whose pitch is the same as ours — it reconciles against the statement's balances and flags bad rows.
What is actually different here is narrower, and worth stating precisely rather than inflating: this runs on your machine with no network code in the package at all, it names the specific row where the chain breaks rather than passing or failing the file, and it produces a record you can file — source hash, named reviewer, and the files it could not read. A hosted tool can promise it deletes your client's statement after processing. It cannot offer you the option of never sending it.
Three verdicts, and it will not bluff
| Verdict | Meaning | Exit |
|---|---|---|
| VERIFIED | The extraction reproduces the statement's own balances. | 0 |
| FAILED | It does not. The breaking row is named. | 1 |
| UNVERIFIED | The statement carries no balances to check against, so nothing is claimed. | 1 |
The third one is the point. A converter that quietly returns a clean-looking CSV for a statement it could not verify is the failure mode that costs money. Reporting "I cannot tell" is more useful than reporting a pass.
Why there is no "upload your statement" box on this page
Because the entire promise is that your statement never leaves your machine, and a demo that asks you to upload one would contradict it on the same page it is claimed.
The package contains no networking code at all — not an optional telemetry flag, not a version check. A test walks the import graph of every module and fails the build if a networking library appears anywhere in it. That is a structural fact you can verify in the source, rather than a privacy policy.
Runs locally
A CLI on your own machine. No account, no server, no upload.
Writes nothing uninvited
No file is created unless you name one with --csv or --record.
Shareable diagnostics carry no money
--diagnostic reports column positions and date shapes — no amounts, dates, names or account numbers.
Install
pip install statementproof
statementproof statement.pdf
statementproof statement.pdf --csv transactions.csv
statementproof ./statements/ --record january.html
MIT licensed. One dependency (pdfplumber). Source at
github.com/OrbitalKeyAi/statementproof.
The reconciliation record
--record writes a document recording which exact file was checked, by whom,
on what date, and what the check found. It opens in any browser and prints to PDF.
The load-bearing field is the SHA-256 of the source PDF. A record that only names a file proves nothing — filenames change and statements get re-exported. The hash binds the record to one exact document, so if that PDF is later altered by a single byte, the record visibly stops describing the file it is attached to.
In batch mode the record also names every file it could not read, in the printed copy as well as the JSON. A record that quietly omitted the statements it choked on would assert a clean review of a folder it never finished reading.
Licensing, stated plainly
The tool is free and every figure it produces is real whether you pay or not. A licence
changes one thing: it puts your firm's name on the record. Without it the
record is stamped UNLICENSED — the numbers are still correct, but no one is
named as having performed the review, and an unsigned document is not evidence that anyone
did.
Unlimited statements, unlimited machines, no subscription. There is no key server and no activation call; the licence is your firm's name in an environment variable, and the package still contains no network code.
Related
sqliteproof — the same idea for
SQLite databases. PRAGMA integrity_check gives you a page number; sqliteproof
tells you which tables survived, how many rows were lost, and what is safe to export.