termroam
TermRoam · field guide

Reconciling books with AI agents

Eighteen months of unreconciled accounts across four data sources. The lesson is not that agents can do bookkeeping — it is that reconciliation has an arithmetic proof, and once you insist on that proof, automation stops being dangerous.

A three-part series. Part 1 — Reconciling books with AI agents (the mental model and the proof) · Part 2 — Automating a monthly close · Part 3 — Categorizing transactions safely
01

The gap is never where you think

A company stops reconciling in October. By the following August, a dozen bank and card accounts have drifted. The obvious story is neglect: someone got busy, the ledger rotted, and now every month needs picking through by hand.

The likelier story is structural. In our case a new business bank account opened in late September, and a week later most card spending moved onto a corporate card platform. Neither had a matching account in the ledger, so there was nowhere for those transactions to go. The bookkeeping did not decay gradually — it hit a wall on a specific date, because the financial stack changed and the chart of accounts did not follow.

That changes the work. Gradual decay means grinding through twelve months of small errors. A structural break means creating the missing accounts, importing the missing streams, and finding the few genuine mistakes hiding in the noise.

In the run this series is drawn from, roughly two thousand seven hundred bank transactions across fourteen accounts and eighteen months reduced to about a thousand that had never been entered — and seven ledger entries that were actively wrong. All seven were made in the same week.

The point of automation is not to categorize faster. It is to narrow thousands of transactions down to the few that are actually wrong.

02

Balances first, categories second

The instinct with messy books is to start labelling: open the oldest uncategorized transaction, decide it is a software expense, move on. It feels like progress because the queue shrinks.

It is the wrong order, and the reason is worth internalising: categorization cannot detect a missing transaction. If a payment never reached the ledger, no amount of careful labelling on the transactions that are there will reveal it. You can categorize a set of books perfectly and still have them be wrong by six figures.

Reconciliation can. It asks "is everything here?" rather than "what is this?", and answers with arithmetic rather than judgement:

The bank's closing balance, minus the ledger's closing balance, equals the sum of everything missing from the ledger. If those two numbers do not match, you have not found everything. If they do, you have — and no judgement was involved.

That converts "did we find everything?" from an opinion into a check. A list that sums to the variance is provably complete; one that does not is provably incomplete, however plausible each line looks.

Only once the balances tie is categorization safe, because now you are labelling a set you know is whole. Part three covers that step. In the other order you are decorating an unknown.

03

What the bank will actually give you

First you need the transactions. Most of the surprises live here, and they are all boring practical limits.

Aggregator APIs have a history horizon, and it moves

The first sync returns whatever the bank makes immediately available — often about a month — and the full backfill arrives asynchronously over the following minutes or hours. Query too early and you will conclude the bank holds thirty days. The maximum you can request is also a parameter, it defaults low, and you cannot change it afterwards without re-linking.

Direct downloads stop at a hard wall

Web banking exports are typically capped — eighteen months is common — and measured from today, not from a calendar boundary. Nine months into a year, the earliest months of the prior year are already out of reach through the export tool, and the only remaining route is the PDF statements.

Structured formats carry an ID; spreadsheets do not

An OFX or Web Connect file gives each transaction a bank-assigned identifier, which is what makes an import idempotent: run it twice and the second run books nothing. A CSV has no such field, so duplicate protection falls back to amount, date and description — which breaks the first time someone buys two identical coffees three days apart. Where a bank offers only a spreadsheet, accept the weaker guarantee knowingly rather than by accident.

Different sources disagree, and that is useful

An aggregator feed and the bank's own export do not always contain the same rows. In one case the feed silently dropped about half the credit-card finance charges — immaterial in dollars, but a good demonstration that a single source is an assumption, not a fact. Pulling two and comparing is cheap: where they agree you have corroboration, where they disagree you have found something.

None of this is difficult, and all of it is invisible until it bites — each one silently changing what "complete" means. Treat collection as its own phase with its own verification, the way you would a context window you are trying not to overflow.

04

The four ways a matcher lies to you

The naive approach is to match on amount and date and call the remainder "missing". That produces a list, and the list is wrong in four specific, repeatable ways.

The simulation below runs a matcher over one account. Each pass claims what it can explain — note that the naive pass alone would have you booking transactions already in the ledger.

Matching passes — illustrative account, six bank rows
DateAmountDescriptionStatus
Six transactions left this account according to the bank. The ledger may or may not already contain them. Press Run next pass.

All figures in these simulations are invented for illustration.

Lie one: the entry is on a sub-account

Ledgers nest. A corporate card with two cardholders is often one parent account with a child per person, and the spending posts to the children. Search the parent by name and you find nothing, then conclude every charge is missing. Match against the account and all of its descendants.

Lie two: a transfer is one entry, not two

When money moves between two accounts you own, the bank reports it twice — a debit in one, a credit in the other. Most accounting systems represent it as a single transfer object that moves both sides at once.

That object appears in a transaction report exactly once, filed under the sending account, with the receiving account named only in a "split" column. Query the receiving account and the money is not there. It arrived, the balance is right, but no row carries its name.

Consequence: a per-account matcher reports every inbound transfer as missing. Book them and you double the money.

Lie three: the counterparty entered it

Related, and subtler. A bookkeeper records money arriving: a deposit on the receiving account whose offset is the sending account. One entry, two accounts moved, filed under the receiver. The sender's bank debit now has no corresponding row under the sender's name — and never will, because the entry exists and is correct.

A matcher that requires each ledger entry to be consumed by only one bank row cannot see these, because the entry was already consumed by the other side. One ledger entry legitimately satisfies two bank rows. Encoding that is a one-line change and a genuinely counter-intuitive one.

Lie four: the sign convention flipped

Every source signs money differently. Aggregator feeds commonly report outflows as positive. Bank export files sign them from the account holder's perspective, so outflows are negative. Card exports report spending as positive debits. And a credit card's balance is stated as the amount owed — a liability, positive — while its transactions are signed like everything else.

Invert once too often and every expense becomes income. The books still balance internally, every total is wrong, and nothing looks broken. Convert at the boundary, once per source, and write a test that pins the direction with a real example. Sign errors are not caught by reading code; they are caught by asserting that a known expense comes out negative.

05

Proving completeness instead of asserting it

The test again, because it is the whole discipline: the unmatched bank rows must sum to the account's variance. Both sides come from somewhere authoritative — the bank's statement, the ledger's balance sheet — and neither is computed by the thing being tested.

Click transactions to include them and watch the meter. The account is only reconciled when the bar sits on the centre line.

Completeness meter — illustrative account, variance −60,000.00
over-claimedsum 0.00under-claimed
Candidates found in the bank, absent from the ledger:
Nothing selected.

Notice what "plausible guess" does: it picks the transactions a reasonable person would, and lands close to the target without hitting it. Close is indistinguishable from correct by eye and completely different in a filing.

A variance has two causes, and one is invisible

That test is right and also incomplete. A ledger can disagree with a bank because transactions happened and were never recorded, or because entries were recorded that never happened. Only the first can appear in a list of bank rows missing from the ledger — the second has no bank row to find.

So on an account carrying a phantom entry the test can never pass. Chase it and you either force-fit something or call the account broken when it is understood perfectly. The identity needs both terms:

bank rows missing from the ledger  +  ledger entries with no bank event  =  variance

One card in our run demonstrated it cleanly: its list overshot the variance, and the excess was a handful of ledger entries sharing a single date, none with a counterpart on any account. Two agents worked it independently and both refused to close the gap by trimming rows. That refusal is the useful behaviour — a check that can fail loudly while the work underneath is correct is worth more than one that always passes.

Several bad entries across several accounts, all inside one week, is not carelessness. It is one bad session at a keyboard. Errors cluster in time because their causes do, which makes when a better first question than what.

06

What agents are actually good for here

Pointing a capable model at a pile of transactions and asking it to reconcile them works badly, for a reason that generalises past bookkeeping: it will produce an answer that looks right, and looking right is not the standard.

The division that worked was blunt. Anything with a provable answer is code — balance arithmetic, matching passes, the completeness check. Handing those to a model adds cost and removes certainty.

Anything requiring judgement is where agents earn their place. A dozen accounts each had a residual variance and no obvious explanation. That is genuine diagnosis: form a hypothesis, write a query, check it, discard it, try another. A dozen agents ran that loop in parallel, one per account, each with the same database and the same non-negotiable success criterion — your list must sum to the variance, and if you cannot get there, say so.

That last clause matters more than the parallelism. An agent told to reconcile an account will reconcile it, dropping an inconvenient row if necessary. An agent told that an honest "unresolved, and here is the exact unexplained amount" beats a padded answer will tell you when it is stuck — and one did, surfacing a duplicate ledger entry that no amount of importing would have fixed.

Each finding then went to a second agent instructed only to disprove it: recompute the arithmetic, re-run the cited queries, confirm every referenced transaction exists, and hunt for rows quietly dropped to make a total work. That catches the failure mode that matters — not a wrong answer but a plausible one.

Same shape as running several agents against one repository: independent workers, a shared substrate, a check that does not depend on any of them being honest.

What the agents found that the code did not

More than a dozen transfers entered from the receiving side, invisible to per-account matching. A duplicate whose signature was a perfect mirror — one account short by exactly the amount another was long. A card payment with the accounts reversed, unnoticed because it balanced. All three became deterministic rules, so the next run finds them without a model. Agents discover the rule; code enforces it.

Where the agents and the code both got it wrong

Two entries looked like phantoms — no corresponding bank transaction anywhere in the collected data — so the obvious move was to delete them. The accounting system refused: both were matched to downloads in accounts that had never been collected. The reasoning had been "absent from our data, therefore absent from reality", which holds only if the data is complete. It was not. A confident conclusion drawn from a partial dataset is indistinguishable from a correct one until something external contradicts it.

07

How TermRoam handles this

This work is long-running, partly interactive, and needs to survive a closed laptop — the agent investigating account nine should not die because a phone locked.

TermRoam gives each project persistent terminal sessions that outlive the browser, so a run started at a desk can be checked from a phone an hour later and resumed next morning in the same state. Agents run inside those sessions; you attach and detach at will. When the work is a queue of judgement calls rather than a script, that persistence is the difference between one continuous effort and a dozen restarts.

Next: automating the monthly close so this never has to be a catch-up project again.