DEV Community

Kayabyte
Kayabyte

Posted on Edited on AI-assisted

Why a green "balanced" check can be wrong on a bank statement

This was a bug in our own app. We found it in our own audit, not through a customer report, and it is fixed in Balanced 1.1.2. I am writing it up because the failure is easy to build into any statement parser, and easy to miss when you check a converted file.

The file

A plain CSV from a current account:

Example Bank Current Account Statement
Statement period: 01/03/2026 to 31/03/2026
Previous balance,1000.00
New balance,2720.00
Date,Description,Amount
02/03/2026,SALARY ACME LTD,2000.00
05/03/2026,PAYMENT TO LANDLORD,-800.00
13/03/2026,TESCO,-80.00
20/03/2026,PAYMENT TO HMRC,-100.00
28/03/2026,REFUND AMAZON,700.00
Enter fullscreen mode Exit fullscreen mode

1000 + 2000 - 800 - 80 - 100 + 700 = 2720. It adds up.

Balanced showed a green "Balanced" status. But it had read the file as a credit card statement and flipped every sign: the salary came out as -2000, the refund as -700, the rent as +800. The only hint was a grey info line saying it was read as a card statement. The exports carried the flipped signs and negated balances.

Why the check passed

With every amount reversed and both balances negated, the arithmetic still closes. The flipped file starts at -1000, ends at -2720, and the rows in between add up to the difference. A reconciliation that only asks "do the opening balance, the rows and the closing balance agree?" cannot tell the two readings apart. A total can pass with every sign reversed.

Where the wrong decision came from

Two things combined:

  1. The labels "Previous balance" and "New balance" counted as a strong sign of a card statement. Banks print those labels on current accounts too.
  2. Once the file was treated as probably a card, the word "payment" in a description was used to settle it. Every "PAYMENT TO ..." line was negative, which looks like outgoing payments, and that was taken as proof that the signs were swapped.

The word "payment" is weak evidence. On a card statement it can be a payment you made to the card. On a bank statement it is a payment to someone else. It appears on both, so it cannot decide anything on its own. It fired on any bank statement with those two balance labels (or two other weak cues) whose only "payment" lines were outgoing. A second probe with "Statement balance" and "Previous balance" and lines like PAYMENT TO ELECTRIC CO gave the same flipped result.

What we changed in 1.1.2

The word "payment" no longer decides the sign. When the evidence is weak and the totals cannot settle it, Balanced now asks "bank account or card?" instead of picking one and showing green. If you converted a statement with "Previous balance" and "New balance" lines in 1.1.1, convert it again and check it. The change is listed in the Balanced changelog.

The general lesson for a parser: when the evidence is weak, ask. A confident green on a guess is worse than a question.

How to check your own file

  • Pick two or three rows you know. Is your salary positive and your rent negative, or the other way round? Do this before you look at totals.
  • Compare the opening and closing balance in the converted file with the PDF or CSV the bank gave you, including the sign of each.
  • Check that money in and money out match the direction your bank shows, not just that the sum closes.
  • If you wrote the parser, add a test where every sign and both balances are negated and make sure it is not accepted silently.
  • Do not treat a single word such as "payment" or "credit" in a description as proof of the account type.
  • Treat a green check as "the arithmetic agrees", not "the signs are right".

Balanced is the Mac app we make for converting bank statements and checking the result: kayabyte.dev/mac/balanced/. It runs on your Mac and needs macOS 15 or later.

AI-assisted: I used an AI tool for drafting help and checked the facts against our own audit notes and changelog.

Kayabyte

Top comments (3)

Collapse
 
smartbudgetsheets profile image
Smart Budget Sheets •

A check that still catches a full sign flip, even when opening balance plus rows equals the closing balance, is the direction of known categories. Salary, refunds, and interest credits should increase a bank balance. A payment to a landlord or a tax agency should decrease it. If every inflow in the file reduces the running balance, the signs are reversed and the green total is not evidence. It also helps to keep the original signed amount next to the normalized one, so a later export can be compared with the source file and not only with the app's own running total.

Collapse
 
refaeldakar profile image
Refael Dakar •

"Opening plus rows equals closing" is invariant under negating everything, so it can only prove the file agrees with itself, never that it was read correctly. The comment above covers the category-direction check; the other cheap outside fact is chaining statements: last month's closing balance should equal this month's opening, sign included. A flipped month breaks that link even when it has no salary line to anchor on. Same idea for dashboards: a total that ties out to its own source says nothing about the definition, so add one check against something the pipeline didn't produce. And asking when the evidence is weak, like 1.1.2 does now, beats green by default.

Collapse
 
launchgatecheck profile image
Launch Gate •

A rare and valuable writeup - a vendor publishing its own misclassification, with the mechanism. The symmetry you found has a name worth using: the balance equation is negation-invariant, so it contributes zero bits of evidence to the bank-vs-card decision. That reframes the fix you shipped. "Ask when evidence is weak" is right, and the sharper statement is that the totals were never evidence at all - so the decision has to be built from features that actually discriminate, and "payment" appearing in both classes is likelihood ratio one, no matter how strongly it feels like it points somewhere.

Your regression suggestion generalises into a pattern worth adopting deliberately: for every check, enumerate the transformations that leave it green. The endpoint balance check has three: negation (your bug), permutation (any reordering of the rows preserves the sum, so a parser that scrambles or re-sorts rows still ties), and drop-and-duplicate (one row lost, another repeated at the same amount, net zero). Writing those three as explicit fixtures - negated file, shuffled file, dropped-plus-duplicated file - turns an audit finding into a permanent guard, and each fixture documents which OTHER check is responsible for catching that symmetry.

The negation fixture has one more use: when a statement carries a printed running-balance column, per-row comparison against it breaks the negation symmetry at the first row, because the bank's column does not flip with your parser's sign convention. That is evidence that discriminates without asking the user anything - worth reaching for before the question, when the file offers it.