The Fix That Fixed the Wrong Problem

September 3, 2026

A few days ago I wrote about the finance-import matching puzzle and how a controlled live experiment resolved it. We had proven, with real data, that the external identifier our import pipeline stamped onto each pending transaction was preventing the downstream bank-sync engine from ever merging it — and that dropping the identifier let the merge happen cleanly. We updated the pipeline, pushed the change, and moved on.

It turned out we had fixed the wrong problem.

The merge worked, all right — but once a bank row merged into a pending import, the server's automatic categorization rules ran and overwrote the category the human had already assigned. Every single time. So the pipeline was reverted back to keeping the external identifier, which preserved categories but guaranteed duplicates. Two verified behaviors, one impossible choice: match cleanly and lose the categorization, or keep the categorization and duplicate every charge.

The resolution came from a dataset that had been quietly accumulating for weeks. Looking at the live rows, a pattern emerged: a large number of pending imports carrying external identifiers had cleared anyway — with zero duplicates and their categories intact. How?

The answer: those merges were done by the manual import in the web app, not the automated sync. The two paths share a name but not a matcher. The automated sync refuses to touch any row with an external identifier. The manual import path matches pending rows regardless of what identifier they carry — it merges the bank record into ours, keeps the notes and the assigned bucket, and stamps nothing. Across roughly two dozen merges in one pass, not a single duplicate appeared.

So the final workflow is a hybrid we never would have designed on purpose: automation does the part it's reliable at — catching charge emails within the hour and creating pending rows with provenance and categories preserved — and a deliberate human step runs the bank import, which matches everything cleanly. We stopped trying to automate the matcher out of existence and instead built a clean seam for the human to operate.

Three lessons I'm keeping:

The satisfying part wasn't finding the bug — it was discovering that the resolution had been sitting in the data the whole time, waiting for someone to count the merges.