Reconciliation errors announce themselves; categorization errors do not. The design goal is not accuracy in aggregate — it is making the uncertain cases visible rather than plausible.
By the time a reconciled month reaches categorization, the hard guarantees are already in place. Every transaction is accounted for, nothing is duplicated, and the list is provably complete. What remains is deciding what each one is — and that is genuinely a judgement call, which makes it the one place a model belongs.
It is also the place where a wrong answer is least visible. A miscategorized transaction does not break a balance. The books still tie, the totals still foot, and the error surfaces months later in a tax position or a margin that makes no sense. Reconciliation errors announce themselves; categorization errors do not.
So the design goal is not accuracy in aggregate. It is making the uncertain cases visible rather than plausible.
Three layers, in strict order, each handling what the one before could not.
A regular expression mapping a merchant pattern to an account is not a lesser version of a model — it is a better one for this job. It is inspectable, it is stable across runs, it costs nothing, and when it is wrong you can see exactly why. Anything recurring belongs here: the cloud provider, the payroll processor, the monthly fee that has been the same for two years.
Rules also encode decisions that are dangerous to re-litigate. A payment to a corporate card platform is a transfer against a liability, not an expense — the individual card charges are the expenses. A model asked "what is this $15,000 payment to a card company" will reasonably answer "expense", and booking both sides would double the year's costs. That belongs in a rule precisely because it is the kind of thing a plausible answer gets wrong.
Everything unmatched goes to the model in one batch with the actual chart of accounts, and comes back with an account and a confidence score. The constraint that matters: it must choose from accounts that exist. A model inventing a plausible-sounding account name produces work rather than saving it.
Below a threshold, nothing is applied. The transaction is flagged and surfaced for a person. In our run that was around 60% of the model's output — bare check numbers, unlabelled transfers, generic payment processor descriptors that genuinely could be anything.
A categorizer that labels everything has quietly converted "I don't know" into a wrong answer. The queue is the feature.
The layers feed each other in one direction. Every review decision is a candidate rule: if a person has resolved the same merchant twice, it should never reach a model again. The model's share shrinks over time, which is the right direction for cost, speed and auditability at once.
Writing rules by hand is slow and biased toward whatever you happened to remember. A better source is already in the books: every transaction a human categorized and that later tied to a statement has cleared two independent checks. Labelled training data, free.
Group by payee; where one payee went to one account consistently across enough examples, emit a rule. Score it by splitting on date — derive from the earlier period, test on the later one — because mining and scoring the same rows only proves the miner can remember.
One guard rail matters more than the rest. An early run mined the payee fi and, matched as a plain substring, it hit 360 real descriptors:
Every one wrong, every one silent. Three fixes, all cheap: anchor matches to token boundaries so a fragment cannot match inside a word, refuse keys below a few characters, and blocklist generic tokens that name a bank or a payment type rather than a merchant. Together they took accuracy on the held-out period from 97.9% to 100%.
A rule that fires far more often than the evidence it was derived from is not a productive rule, it is a wrong one. Test every proposed rule against your whole corpus, not just the cluster it came from.
Mined rules usually key on the payee, because that is the field a bookkeeper fills in. But the raw descriptor often carries structure the payee name throws away, and where it does, it is the stronger signal.
Payroll providers are the clearest case. Every ACH they originate carries a type code — contractor payment, employee net pay, tax remittance, their own subscription fee — and all of them share one payee. Mining on the payee alone sees a vendor categorized several ways and calls the history inconsistent, when it was several consistent rules wearing one name.
An "inconsistent" vendor is worth a second look before you write it off. Sort its transactions by descriptor rather than by payee and the inconsistency often resolves into two or three clean rules.
The absences matter too. In our books the payroll debits carried only a contractor code and a fee code — no tax remittance appeared anywhere in the year. A payroll run that produces no tax debit is one with no employees in it, which settled a question we had been about to ask a vendor's API for. Reasoning from what is missing in a complete data set is safe in a way that reasoning from absence in an incomplete one is not — and the difference is precisely whether you have proven the set is complete.
Every rule is written to catch transactions nothing currently handles. But a rule does not know that, and a categorizer re-runs over everything — so a rule aimed at the unresolved tail will also pick up rows that were already categorized, correctly, months ago, and move them somewhere else.
Consider a plausible one: every restaurant charge is cost of sales. Run against a real ledger it matched 304 transactions. Only 254 were unresolved. The other 50 already had an account — and would have been silently moved off it.
Before saving a rule, count what it catches in three buckets: rows it newly resolves, rows already going to that account, and rows it would move. The third number is the one worth reading, and it is the one no accounting package shows you.
The first two are the rule working. The third is the rule editing history, which may be exactly right — or may be one careless save away from restating a filed year.
Worth stating because it removes a large fraction of the queue. Moving money between two accounts you own is not income and not an expense — it is the same money in a different place. Reconciliation has already identified these, so they should never reach a categorizer, and a model asked to categorize one will invent something rather than say "this is not a category question".
The simulation below shows a batch coming out of categorization. Adjust the confidence floor and watch what moves between "book automatically" and "review". The point is the trade: a lower floor means less review and more silent error.
| Date | Amount | Description | Proposed | Conf | Action |
|---|
All transactions and figures here are invented for illustration.
Notice which rows never move. A recurring cloud bill matched by rule sits at full confidence regardless of the floor, because no model was consulted. A payment to a card platform is likewise fixed — as a transfer, not an expense. The floor only governs the genuinely ambiguous middle, which is where it belongs.
The floor is also a business decision rather than a technical one, and it should be set by how much a wrong answer costs. Categorizing office supplies wrongly is noise. Categorizing a capital contribution as revenue is a different kind of problem entirely. Where the two live in the same queue, set the floor for the expensive case.
A restaurant charge is not one kind of transaction. At forty dollars it is someone eating on a trip. At four hundred it is a client dinner. At four thousand it is an event with a room full of people in it, and the reason those people were in the room is that somebody pitched to them.
Those are three different accounts with three different tax treatments, and the descriptor is identical in all three cases. The only signal separating them is the size.
Thresholds shown are one business's policy, not a tax rule. Pick your own and write them down.
This generalises past meals. Anywhere a merchant serves two purposes at different scales — a print shop doing business cards versus a trade-show stand — the amount carries information the merchant name does not.
A banded rule is still fully deterministic. Same descriptor, same amount, same answer, forever. It is not a heuristic; it is a policy written down once and applied without further judgement.
Two details are load-bearing. A banded rule must sort ahead of the same rule unbanded, or the general one swallows every amount before a band is tested. And a banded rule handed no amount must FAIL rather than pass — ignoring the band is how "meals over $1,500" catches a twelve-dollar coffee.
The most expensive categorization error we found breaks nothing. It books the same spending twice, the accounts still tie, and the only symptom is that the year's costs are roughly double.
It happens whenever a card is settled from a bank account. The bank feed shows one payment to the issuer; the card feed shows every charge. Both are real and both belong in the books — but only one is an expense. The other is a transfer against a liability.
Toggle the treatment below.
All figures here are invented for illustration.
Nothing about the wrong version looks wrong. Both sides reconcile, every transaction has a source and a category, and a reviewer sees a payment to a recognisable card company and approves it — in isolation it is obviously a business cost.
The errors reconciliation cannot catch are the ones where both entries are real. Those need a rule, written once, by someone who understands the flow of money rather than the individual transaction.
The same shape appears with payroll providers, batch-settling payment processors, and any account funded by sweep: a gross figure and its components both visible, and booking both doubles the total. List them deliberately when you set up rules, or discover each the hard way.
Two classes of decision emerge here that no amount of reconciliation will reveal, because both involve transactions that are already correctly recorded.
Payments to individuals and unincorporated businesses for services may require an information return at year end, and gathering the paperwork is far easier during the year than the following January. Scanning categorized spend for likely candidates is cheap and mostly mechanical.
One subtlety worth encoding rather than rediscovering: payments made by card or through a third-party network are reported by the processor, not by you. A contractor paid entirely by card is generally not your filing obligation; the same contractor paid by bank transfer may be. A tool that aggregates a vendor's total spend without separating the rails will produce a list that is confidently wrong in both directions.
Investor funds arriving, loans, capital contributions, and instruments that convert later all land in a bank feed looking like ordinary inflows. Their classification is a real accounting decision with balance sheet consequences, and it is not one a categorizer should be making silently.
These belong in the review queue by construction, not by confidence score. A large inbound wire from an unfamiliar counterparty is not a low-confidence expense — it is a question for whoever knows what the money was for, and no amount of context in the prompt changes that. The right behaviour is to route it, not to guess well.
A related trap: an instrument classified once, early, tends to stay classified. If the first month books an investor inflow to a liability account and nobody revisits it, the rule engine will keep booking it there, and by year end the balance sheet is confidently and consistently wrong. Decisions with balance-sheet consequences deserve a second look before they become a rule.
The review step wants a spreadsheet, not a terminal. Rows, an editable account column, multi-select, bulk-apply. Reviewing eighty flagged transactions one prompt at a time is how a queue stops getting cleared.
But the surface that reviews should not be the surface that writes. In our setup the grid produces an approved batch and a single command-line path does the posting, because that path already holds every safeguard worth having: it is dry-run by default, it books only rows the reconciler proved were missing, it stamps each created entry so imports are identifiable and reversible, and it refuses input that does not balance.
Two ways to write to the ledger means maintaining those guarantees twice. The failure mode of getting it wrong is duplicate entries in books that feed a tax filing.
The cost is that posting requires a second action rather than a button in the grid. That is a few seconds, against a class of error that is genuinely painful to unwind — reversing a batch of duplicate entries in a live ledger is slow, visible, and lands in the audit trail forever.
Every entry written by a tool carries a marker identifying the batch that produced it. That one field turns "undo the import" from an archaeology exercise into a query. It also makes the next reconciliation honest: an entry the tool created and an entry a person created are different kinds of evidence, and being able to tell them apart is worth more than it costs.
The posting path prints exactly what it would create and exits without writing unless explicitly told otherwise. Not because the code is untrusted, but because the input might be: an approved batch assembled from the wrong month, or a file edited by hand between review and posting. The dry run is the last place a person sees the actual entries before they exist.
Categorization is bursty. A month's queue is an hour of decisions that rarely happen in one sitting — some at a desk, some on a phone, some a week later when the answer to "what was this wire" finally arrives.
TermRoam keeps that work in a session that survives all of it. The batch you were halfway through is still there, the agent you had digging into one vendor is still running, and nothing needs reconstructing because a laptop closed.
Start of the series: the mental model and the proof.