I want to tell you about the night I decided to build this. It wasn't when I
sat down to write code — it was three hours into an Excel reconciliation for a
client, at 11pm, when I noticed the sum of a column was off by ₦0.02 and spent
45 minutes finding out why.

The answer: floating-point arithmetic. Excel was storing ₦5,000.00 as
5000.0000000000003 because of the way it handles decimals, and my VLOOKUP was
failing because the two sides of the match had slightly different underlying
values. The ₦0.02 wasn't real money. It was the way Excel represented the
money.

I closed the spreadsheet, opened a blank Python file, and wrote:

from decimal import Decimal 

That was the first line of reconcile. It's been the most important line I
wrote all week.

The first decision I made, and why it stuck

I knew within five minutes of starting that the tool needed Decimal, not
float. Not because I'm disciplined about types — I'm not. But because I'd just
spent 45 minutes of my life on a bug that existed only because float arithmetic
rounds in ways a spreadsheet hides.

Money in float is a lie. 0.1 + 0.2 = 0.30000000000000004 in float math. For
a chart, that doesn't matter. For reconciliation, it's the entire bug class I
was trying to escape. Decimal doesn't have the problem. Decimal stores
5000.00 as exactly 5000.00.

I made that decision in the first five minutes and never reconsidered it. That's
rare for me. Normally I'd write the code with float, hit the bug, and rewrite.
This time I'd already hit the bug — in Excel, at 11pm — and I knew what the fix
was going to look like.

The lesson I didn't fully articulate at the time: the first decision sets the
tone for the whole project. Getting the type right on Day 1 meant I never had a
rounding bug on Days 2, 3, 4, or 5. The bug was pre-empted, not debugged.

The second decision: three sections, not one number

My first instinct for the report was a single line: "N transactions matched, M
didn't." That would be easier to write. It would also be useless.

The question I wanted the tool to answer wasn't "did everything match" — it was
"what didn't match, and what should I do about it?" A count of 47 matches
doesn't tell you the three things that need fixing.

So the report became three sections. Matched so you can verify the good
ones. Unmatched processor rows — the transactions the processor says
succeeded but that aren't in the bank. Those are the ones you chase. Unmatched
bank rows
— bank entries with no matching processor transaction. Those are the
ones you investigate. Are they refunds? Chargebacks? A different client paying
into the same account?

That decision added about 30 lines of output-formatting code on Day 2. Worth
every line.

The instinct I had to resist was "make it as simple as possible." A
single-number report would have been simpler to build and harder to use. The
simplest thing the user needs is not the simplest thing the code can produce.

The first thing that went wrong

On Day 2, I wrote the matcher. The exact-match version. The one that compares
reference column to reference column, amount column to amount column.

I ran it on the fixtures and got zero matches. All rows unmatched on both
sides.

The reason: the processor's reference is ORD-001. The bank's description is
TRF/ORD-001/PAYSTACK. Different formats. The column I thought I was comparing
was actually a free-text description that contained the reference somewhere
inside it.

This is the kind of bug that doesn't show up in a mental model. I knew
references wouldn't be exact matches — that's why I'd built in fuzzy matching
for Day 3. But I wrote the exact-match version first anyway, because Day 2's job
was the simplest possible thing. Simplest possible thing, it turned out, matched
zero rows.

The fix was a regex on Day 3. [A-Z]+-\d+, extract ORD-001 from
TRF/ORD-001/PAYSTACK, case-insensitive comparison. Ten lines of code.

Should this have been on Day 2? Probably. I could have designed the fixture
files to have exact-matching references and saved normalization for Day 3. But
the fixtures I picked reflected what real bank statements actually look like,
and the real problem was always going to need extraction.

The bug you find on Day 2 is often the bug you should have designed for on Day

  1. That's not a failure. That's what the process looks like.

The thing I almost shipped but didn't

There was a moment on Day 3, after the fuzzy matching worked, where I thought:
"Let me add a --verbose flag to the CLI." The output was one line per match,
and I wanted a flag that showed more detail for each row.

I sat with it for five minutes and killed it.

The reason: the output already showed everything. Adding --verbose would let
the user choose to see less. But the tool's entire purpose was showing the
full picture. A user who runs reconcile wants to see the three sections. A
user who wants one line of output wants a different tool.

This is the difference between "a feature I could add" and "a feature the
product needs." I default to adding. This time I didn't.

The rule I'd carry forward: if you're adding a flag to hide what's already
there, you're probably building a different tool than the one you set out to
build.

The 5-day build, honestly

Here's what actually happened, not the polished version.

Day 1 (2 hours): models.py, cli.py, print row counts. The CLI reported "4
processor rows, 3 bank rows" and I closed the laptop.

Day 2 (3 hours): matcher.py with exact match, two tests. Got zero matches
first, realized the reference-extraction problem, added a regex that probably
belonged on Day 3. Tests passed eventually. Went to sleep at 1am.

Day 3 (3 hours): fuzzy matching — amount tolerance, date window,
case-insensitive reference — plus 7 more tests. 9 tests total. Felt like the
hard part was done.

Day 4 (1.5 hours): README, LICENSE, CI, push. Most of this day was
writing, not coding. Also spent 30 minutes fighting a Python PATH issue when
reconcile.exe wasn't found. The fix was python -m reconcile instead of
reconcile. Six minutes of work, 30 minutes of not realizing it was six
minutes of work.

Day 5 (2.5 hours): reconcile-demo. FastAPI wrapper, HTML form, Render
deploy. First deploy failed. Second deploy worked. Total: 12 hours, five
calendar days, most of it in 2-hour sessions.

The honest part: every single day had a "this isn't working" moment. Day 2's
was the zero-match. Day 3's was a test failing because the date math was off by
one day — weekends don't post the same way weekdays do, and I hadn't thought
about that. Day 4's was the PATH issue. Day 5's was ModuleNotFoundError for
the reconcile package because the temp files were being read incorrectly.

Every day, something didn't work on the first try.

The reason I finished anyway: each of those moments was a specific error message
pointing at a specific line. None of them were "I don't know where to start."
They were all "this one thing is wrong." And each one was fixable in under 20
minutes once I stopped trying variations of the same command and actually read
what the error was saying.

What's still not done, and why

Things I deliberately cut because they weren't part of the finish line:

  • No support for Xero or QuickBooks exports. Only the two CSV formats in the README.
  • No CLI flags for tolerances. Change FuzzyMatchConfig in code if you need different tolerances.
  • No JSON output. Text only.
  • No CI test for the pip installed command. Works locally; not tested on CI.
  • No partial refunds, chargebacks, or multi-currency settlement.

Each one is in the README's "Limitations" section. The point of that section
isn't to apologize — it's to make the boundary visible. Anyone using the tool
knows exactly what it does and doesn't do.

What I'd do differently

One thing. I'd write the fixture files before the matcher.

I wrote models.py first, then the matcher, then the fixtures. Then on Day 2 I
discovered the fixtures needed a specific format the matcher didn't expect. If
the fixtures had come first, they'd have shown me the reference-extraction
problem before I wrote code that assumed it away. The matcher would have been
written correctly on the first try.

The rule I'll carry forward: write the test data before the code that uses it.
The data tells you what the code needs to handle, and it's cheaper to change the
data than to change the code.

If you're doing this in Excel right now

Everything above is about a specific 12-hour project. The part that matters to
you is whether Excel is the right tool for your situation.

The honest answer: for fewer than 20 transactions a month with fewer than 3
clients, Excel is fine.
The manual process takes 20 minutes and the
discrepancies are rare enough that a tool is not the bottleneck.

For 20+ transactions a month with 3+ clients, no bookkeeper, and no
spreadsheet discipline — Excel is losing money for you.
Not visibly. Not in a
way you can point to a line and say "that's what Excel cost me." But every
₦15,000 missed payment that goes uncaught is real, and the same missed payment
every month for a year is ₦180,000 that went to a bank's reconciliation queue and
never came back.

You can try the tool in the browser
without installing anything. Upload any two CSVs and see what it does. If it
doesn't help, you've lost nothing. If it does, the source is at
github.com/Agenda-X-PR/reconcile
and you can run it locally.

Either way, you now know why your Excel is lying to you.