Getting a bank CSV into double-entry books
Turning a bank CSV into double-entry books means mapping each row to two accounts rather than one — the account the money moved through, and the category it belongs to — so that every transaction sums to zero.
A bank export is a list of things that happened to one account. Double-entry books need every one of those things recorded against two. That gap is the whole job, and most of the difficulty in it is unglamorous.
The three shapes an amount column comes in
Banks have never agreed on this, and it is the first thing an import has to decide. Get it wrong and every sign in the file is inverted.
| Shape | Looks like | The trap |
|---|---|---|
| Signed amount | -42.00, 1200.00 | Some banks sign from their side, so a deposit is negative |
| Split debit/credit | Debit: 42.00, Credit: | Empty string, not zero — a naive parse reads it as 0 |
| Running balance only | Balance: 4,182.00 | The amount is a difference between rows, so row order is load-bearing |
The third is the one that ruins imports silently. If the file is sorted newest-first and you difference it as if it were oldest-first, every amount is correct in magnitude and wrong in sign.
One row becomes two postings
2026-08-11 * "FIGMA MONTHLY"
Expenses:Software 49.00 USD
Assets:Bank:Checking -49.00 USD
The bank told you about the second line. The first is the actual bookkeeping — the decision about what kind of thing this was. That decision is the part worth an agent’s judgement, and the parsing is the part that should never require any. Building
Duplicates are the hard part
Nobody imports a statement once. They import August, then re-import it because the first attempt was interrupted, then import a “last 90 days” export that overlaps it.
Two entries with the same date, payee and amount are not necessarily a duplicate — you can genuinely buy the same coffee twice on the same day. So the check has to be a stable key per source row, held against what the book already contains, rather than a similarity test on the entries themselves.
What to do about the rows nobody can classify
Flag them. A ! on an entry means it is recorded and pending, so the money is
accounted for and the question is still open:
2026-08-13 ! "SQ *UNKNOWN"
Expenses:Unknown 218.60 USD
Assets:Bank:Checking -218.60 USD
The alternative — guessing — produces books that balance perfectly and describe a business that does not exist.
Where Countbean is on this today
Honest status, because a post about imports that overstates the importer would be a strange thing to publish:
- Reading a CSV and booking it works today, because the agent reads the file itself and writes validated entries. Shipped
- A deterministic, server-side importer — the same column mapping every run, duplicate detection, and reachable from the chat agent as well as the plugin — is being built. Building
- Bank feeds are not planned. Not built