DEV Community

Mokshraj(ssr7)
Mokshraj(ssr7)

Posted on

I built a bank reconciliation engine that never uploads your files

First post here, so bear with me.

I've spent the last few weeks building a site full of browser-side calculators and tools, and
somewhere along the way I went down a rabbit hole about what bookkeepers actually spend their
time on. The answer, over and over: reconciliation. Books against the bank statement, every
month, mostly by hand, mostly in Excel.

There are tools for this. But every one I found — the CSV converters, the SaaS reconcilers, the
inevitable "just paste it into ChatGPT" suggestion — starts the same way: upload the bank
statement. And that's exactly the step a lot of these people can't take. A client's bank
statement might be the most sensitive file a bookkeeper touches. Sending it to some website's
server is either against firm policy or just a bad instinct they've rightly developed.

So my pitch is almost stupid in its simplicity: this one can't upload anything, because there's
nowhere to upload to. No server side at all. You drop two CSVs — your ledger and the bank
export — and the parsing and matching run entirely in the browser. You can open the network tab
and watch nothing leave.

It's here if you want to poke at it: https://stepwisecalc.com/tools/reconciliation
(free, no signup — and I'm a developer, not an accountant, so part of why I'm posting is to
find out where it falls apart).

The rest of this is the four parts that were genuinely fun to build.

Working out which column is which, without trusting headers

My first version trusted header names. That survived about two test files. Banks rename headers
constantly — Date, Txn Date, Value Date, Posted — so matching on names is hopeless.

What stays consistent is the data itself. Dates look like dates. Amounts look like amounts. So
each column gets scored for date-ness, amount-ness and description-ness, and the best-scoring
assignment wins.

The case that took me longest: banks that split debits and credits into two separate columns.
The giveaway turned out to be fill patterns — you get two numeric columns where every row has a
value in exactly one of them. When the detector sees that disjoint pattern, it folds the pair
into signed amounts. Same general approach handles (500.00) meaning negative, trailing
DR/CR markers, and day-first vs month-first dates (you disambiguate from the rows where the
day is bigger than 12).

Matching in tiers, because fuzzy matching in one pass produces confident garbage

I learned quickly that one big fuzzy match gives you results that look great and are wrong. So
matching runs in three labelled tiers, strictest first:

  1. Exact — same amount, same date.
  2. Near-date — same amount within a small date window, because deposits clear days after they're booked.
  3. Combined — a bounded subset-sum search in both directions: one bank line that equals the sum of several book lines (a batched deposit), or the reverse. This is the case that breaks everyone's VLOOKUP, and honestly it's the reason the tool deserves to exist.

Every match carries its tier in the output, so you know exactly how much to trust it.

The divide-by-9 trick

This one I stole from audit folklore. If two digits get transposed — 54 typed as 45 — the error
is always divisible by 9. Always. So when two amounts almost match, the engine checks the
difference, and if it divides cleanly by 9 it flags "possible transposed digits" instead of a
bare mismatch. Decades-old accountant knowledge, three lines of TypeScript. Easily my favourite
part of the whole build.

The part that makes it trustworthy (I hope)

Here's the thing that bugged me about fuzzy matching: how do you know the reconciliation as a
whole is right, even if individual matches are heuristic?

The answer I landed on: the exceptions have to explain the difference. So the engine computes
two numbers completely independently — the difference between the files (books total minus bank
total), and the exception schedule (missing-in-bank minus missing-in-books plus the
mismatches). It only ever claims "fully explained" when the two agree to the cent. If they
disagree, the UI says so instead of quietly absorbing it.

That property, not any particular match, is what the test suite actually asserts. The matcher
is allowed to be heuristic. The arithmetic isn't.

Boring implementation notes

TypeScript. Zero dependencies in the engine, including the CSV parser — RFC 4180 is small
enough that writing the ~40 lines felt more honest than pulling in a library. The reconciler is
one of 350+ tools on the site, all built on the same idea: the computation happens on your
machine, not mine.

What I actually want from you

A bank export that breaks it. I've tested against constructed files and the arithmetic holds,
but real bank CSVs are their own kind of chaos, and the failure modes are where this gets
better. Tell me in the comments what mangled it and I'll dig in.

Also a genuine question for anyone who's done bookkeeping work: is CSV enough? If most banks
in your world only give PDF statements, the tool needs a different front door, and I'd rather
find that out now.

Top comments (1)

Collapse
 
mokshrajssr7 profile image
Mokshraj(ssr7)

hello guys this is bad right??