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.

Diagram: a dense four-column ruled table with a single row picked out in amber and labelled ONE ROW; an arrow from it splits into two, leading to a stacked pair of boxes marked plus and minus, bracketed together and labelled TWO POSTINGS, with BALANCES TO ZERO beneath.
ONE ROW BECOMES TWO POSTINGS — THE FACTS OF THIS PLATE ARE IN THE TEXT

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.

ShapeLooks likeThe trap
Signed amount-42.00, 1200.00Some banks sign from their side, so a deposit is negative
Split debit/creditDebit: 42.00, Credit: Empty string, not zero — a naive parse reads it as 0
Running balance onlyBalance: 4,182.00The 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