Subscribe to get high-signal insights on how modern fintech is built.

engineering

Reconciliation Is Where the Money Hides

Every fintech has a ledger and a bank account, and the numbers between them disagree more often than anyone admits.

By Alex Kugell ·

Your ledger says $84,219.47. Your bank says $84,103.24. The difference is $116.23.

Is that a rounding error across 50,000 transactions? A fee your processor deducted that your system didn't record? A refund that posted to the bank but never hit your ledger? Or is it the first symptom of a bug that has been silently misattributing money for six weeks?

You can't tell from the number. That's the problem with reconciliation. The gap between two systems never explains itself.

Why reconciliation is harder than it looks

Most engineers encounter reconciliation as a matching problem: take two lists of transactions, find the pairs, flag the mismatches. An afternoon of SQL and a cron job, maybe a weekend project.

But matching is the easy part. The hard part is what happens after matching fails. A $0.01 difference across a million transactions could be one missing penny or a thousand offsetting errors that happen to net to one cent. A transaction that appears in your ledger but not in your bank statement could be a timing issue that resolves tomorrow, or it could be a payment that never actually settled.

The engineering challenge is building a system that distinguishes between a break that will fix itself and a break that is hiding real money. Synapse Financial identified a $13 million discrepancy in September 2023, classified it as something that would resolve, and kept operating. By the time the bankruptcy trustee examined the records, the gap was $85 million.

The three-way problem

Two-way reconciliation compares your ledger against one external source. It confirms whether your records agree with someone else's records. But two wrong systems can agree just as easily as two right ones.

Three-way reconciliation compares your internal ledger, the processor's settlement file, and the bank statement. Each one sees the same money through a different lens.

Your ledger records intent. When a customer initiates a $500 transfer, you write an entry immediately. The ledger knows what you told the payment rail to do.

The processor's settlement file records acknowledgment. Stripe, Adyen, or your ACH originator sends a file confirming which transactions they accepted, which they rejected, and what fees they deducted. This file might arrive hours after the API call.

The bank statement records reality. The actual dollars that landed in or left your account. This is the ground truth, and it arrives last, sometimes days after the transaction.

When all three agree, the transaction is clean. When two agree and one doesn't, you know which system has the problem. When all three disagree, you have a different kind of problem entirely.

Three-Way Reconciliation
Internal Ledger
Records intent
$500.00
Processor File
Records acknowledgment
$485.50
Bank Statement
Records reality
$485.50
Compare all three ↓
3/3
All agree
Clean
2/3
Two agree
One system wrong
0/3
None agree
Investigate
The ledger says $500.00 but the processor deducted a 2.9% fee. Without separate fee entries, matching breaks on the first pass.

What breaks look like

Breaks cluster into patterns, and each type points to a different root cause and a different fix.

Timing breaks are the most common. A card authorization hits your ledger on Tuesday, the processor settles it on Wednesday, and the bank posts it on Thursday. On Tuesday night, your reconciliation sees a ledger entry with no matching settlement. On Wednesday night, it sees a settlement with no matching bank deposit.

By Thursday, everything aligns. These breaks are noisy but self-resolving, and most reconciliation systems handle them with a configurable waiting window.

Timing Break
Tue
Wed
Thu
Ledger
Records intent
✓
✓
✓
Processor
Records acknowledgment
no record yet
✓
✓
Bank
Records reality
no record yet
no record yet
✓
On Tuesday night, the ledger has an entry with no matching settlement. On Wednesday, the settlement exists but the bank hasn't posted. By Thursday, all three agree. The break was real but temporary.

Fee breaks are subtler. Your ledger records a $100 charge. The processor's settlement file shows $97.10 after deducting a 2.9% processing fee. The bank deposit reflects $97.10.

If your system matches on amount, the $100 ledger entry will never match the $97.10 deposit. You need to either record the fee as a separate ledger entry at the time of the transaction, or your matching logic needs to understand fee structures per processor.

Fee Break
Ledger records
$100.00
Gross amount at time of charge
→
Processor deducts
−$2.90
2.9% processing fee
→
Bank receives
$97.10
Net deposit after fees
Matching on amount fails. The ledger's $100.00 will never equal the bank's $97.10. Without separate fee entries, this break recurs on every transaction.

One-to-many breaks appear when a processor batches multiple transactions into a single bank deposit. You have 847 individual ledger entries. The processor's file lists 847 settlements. The bank shows one deposit of $412,309.15.

Your matching engine needs to group the 847 entries, sum them, subtract expected fees, and compare the total against the single bank line item. If the totals don't match, you know there's a break somewhere in those 847 transactions, but not which one.

One-to-Many Break
847 ledger entries
txn_4a1$47.50
txn_4a2$215.00
txn_4a3$12.99
...
txn_4n7$89.30
settles as
→
1 bank deposit
$412,309.15
Single line item on the bank statement
If the sum of 847 entries minus expected fees doesn't equal the deposit, the break is somewhere in those 847 transactions. Finding which one requires decomposing the batch.

Missing transaction breaks are the most dangerous. A transaction exists in one system but not in another, and it's not a timing issue. The processor accepted a payment and sent a webhook, but your system never recorded it. Or your system recorded a refund, but the processor never processed it. Every missing transaction is real money that one system knows about and another doesn't.

Missing Transaction Break
Ledger
txn_881
txn_882
txn_883
txn_884
Processor
txn_881
txn_882
txn_883
txn_884
Bank
txn_881
missing
txn_883
txn_884
txn_882 exists in the ledger and the processor's file, but the bank never received it. This is real money that one system says moved and another says didn't.

Penny breaks seem harmless. A $0.01 difference on a single transaction is usually a rounding artifact from fee calculations or currency conversions. But penny breaks compound. If your fee computation rounds differently than your processor's, every transaction is off by a fraction of a cent, and over a month of high volume, those fractions add up to real dollars.

Penny Break Accumulation
Period
Transactions
Variance
Cumulative
Day 1
2,400
$0.24
$0.24
Week 1
16,800
$1.68
$1.68
Month 1
72,000
$7.20
$7.20
Month 6
432,000
$43.20
$43.20
Year 1
864,000
$86.40
$86.40
At $0.01 per transaction from a rounding mismatch between your fee computation and the processor's, the variance is invisible on day one and material by year end.

The matching engine

Most reconciliation systems run matching in layers, each one catching what the previous layer couldn't.

Matching Engine Layers
Unmatched records entering
10,000
Exact Match
Transaction ID, payment reference, idempotency key
Matched
9,200
↓
800 remain
One-to-Many Grouping
Sum subsets by date range against batch deposits
Matched
500
↓
300 remain
Fuzzy Match
Amount within tolerance, date window, metadata hints
Matched
200
↓
100 remain
Exception Queue
Manual investigation required
100
1% of original
Each layer exists because the one before it can't handle every case. What survives all three is the work that determines whether your books are right.

The first layer does exact matching on a unique identifier: a transaction ID, payment reference, or idempotency key. In a well-instrumented system, this catches the vast majority of records.

If exact matching catches significantly less, the problem is usually inconsistent identifiers. Your system calls it txn_abc123, the processor calls it PAY-abc123, and the bank statement truncates it to abc12. Identifier normalization, stripping prefixes, uppercasing, removing dashes, is often the hardest part of the matching pipeline.

The second layer handles one-to-many and many-to-one groupings. A single bank deposit might cover hundreds of processor settlements, and multiple customer payments might clear a single invoice. This layer groups records by date range and amount, then checks whether subsets sum to a known total.

The combinatorial problem grows fast. Ten unmatched transactions on each side produce over a thousand possible groupings to check.

The third layer applies fuzzy matching for records that lack clean identifiers: amount within a tolerance, date within a window, and any available metadata like merchant category or customer reference.

Fuzzy matching is necessary but risky. A tolerance of $0.02 seems safe until two unrelated transactions happen to be $0.01 apart and get paired incorrectly. Every false match hides a real break.

What's left after all three layers is the exception queue.

The exception queue is the product

For a mature reconciliation system, the exception queue matters more than the matching algorithm. This is where ops teams spend their time, and how the queue is designed determines whether breaks get resolved or accumulate.

A raw list of unmatched transactions is useless at scale. An ops analyst looking at 200 items needs to know which ones matter. A $0.01 timing break that will auto-resolve tomorrow and a $50,000 missing settlement need different levels of urgency.

Break categorization at creation is what makes the queue navigable. Each break gets tagged: timing, fee, partial, missing reference, amount mismatch, duplicate.

The tag determines the routing. Timing breaks get a 48-hour auto-resolve window, fee breaks route to the processor integration team, and missing transactions escalate immediately.

Ownership matters as much as categorization. A break between your ledger and the processor's file is your engineering team's problem. A break between the processor's file and the bank statement is the processor's problem, or your treasury team's. A break between your ledger and the bank that the processor's file can't explain is everyone's problem.

Aging is the third dimension. A break open for 24 hours is probably a timing issue, but one that lingers for 14 days is likely a bug, and anything unresolved past 90 days becomes a compliance exposure. Aging-based escalation, where unresolved breaks automatically move up the severity ladder, prevents the slow accumulation that turned Synapse's $13 million discrepancy into an $85 million hole.

The ops teams that handle this well treat the exception queue as the system's primary output, not something tacked on after the matching engine ships.

Batch versus streaming

Most reconciliation runs overnight. A cron job pulls the processor's settlement file, downloads the bank statement, loads both into a staging table, and runs the matching engine. Results land in a dashboard by morning.

The problem with batch reconciliation is the delay. A transaction that settles at 9am Tuesday won't be reconciled until Wednesday morning. If the transaction failed, silently, because the processor accepted it but the bank rejected the ACH, you won't know for at least 12 hours. In that window, your ledger says the customer has money that the bank never received.

This is exactly how fail-open event systems create reconciliation problems. The system assumed success, showed the customer a balance, and now you're discovering the failure half a day later.

Streaming reconciliation matches each event as it arrives. A processor webhook fires, and within seconds your system compares it against the ledger entry and any available bank notification. Breaks surface in near real-time instead of the next morning.

But streaming reconciliation introduces its own complexity. Events arrive out of order. A bank notification for a Tuesday settlement might arrive before the processor's Wednesday settlement file that explains the individual transactions within it. Your matching engine needs to handle partial matches, where one side of the pair exists but the other hasn't arrived yet, without generating false breaks.

Idempotency becomes critical. If a processor sends the same webhook twice, or the bank notifies you of a deposit that your streaming matcher already paired, the system needs to recognize duplicates without double-counting. In 2012, Nationwide Building Society processed a batch of debit card transactions twice, double-debiting 704,426 accounts because their reconciliation layer didn't catch the duplicate batch before it posted. This is the same idempotency problem that payment systems face, applied to the reconciliation layer.

The practical answer for most fintechs is hybrid: streaming reconciliation for high-priority flows like card authorizations and instant payments, batch reconciliation for ACH settlements and end-of-day bank files. The two systems need to agree on which transactions they've each processed, or you end up reconciling your reconciliation systems.

What the architecture reveals

Reconciliation is often treated as an ops function, something that happens after the real engineering work is done. But the quality of your ledger design determines how hard reconciliation will be.

If your ledger records a single timestamp per transaction, your matching engine has to guess which clock to match against: when the customer initiated, when the processor acknowledged, or when the bank settled. Recording all three gives the matcher unambiguous join keys.

The same principle applies to fees. A gross-amount-only ledger forces the matching engine to reverse-engineer fee structures from processor documentation, while a ledger that records gross, fee, and net as separate entries makes fee breaks trivially detectable.

Identifiers matter even more. If your ledger generates its own transaction IDs with no correlation to processor references, you're stuck in fuzzy matching territory. Storing the processor's transaction ID alongside your own gives the exact matcher a clean join on the first pass.

The most reliable reconciliation systems are the ones where the ledger was designed with reconciliation as a first-class constraint. The matching algorithm is a downstream consequence of that decision. Get the ledger right, and most of your breaks never happen in the first place.

Sources

Alex Kugell
Alex KugellAI/ML Engineer, Fintech Infrastructure

Writes Ledger Drift. Engineer building AI systems and financial infrastructure. Open source contributor to x402, ACK, and MPP.

Frequently Asked Questions

What is three-way reconciliation in fintech?
Three-way reconciliation compares three independent records of the same money: your internal ledger, the payment processor's settlement file, and the bank statement. Each source sees the same transactions through a different lens. The ledger records intent at the moment of the API call. The processor confirms what it accepted and settled. The bank confirms what actually arrived. Discrepancies between any two reveal timing gaps, fee mismatches, or missing transactions that two-way matching would miss entirely.
Why do small reconciliation breaks matter?
A one-cent break can indicate a rounding error, or it can indicate a systematic bug that affects every transaction. Synapse Financial identified a $13 million discrepancy in September 2023 and kept operating through it. By the time of the bankruptcy filing, the gap had grown to $85 million across $265 million in customer deposits. Small breaks that go unresolved compound silently, and by the time the aggregate variance is large enough to trigger an alert, the root cause may span months of transactions.
How does break matching work in payment reconciliation?
Break matching runs in layers. The first pass does exact matching on transaction identifiers, catching the majority of records in a well-instrumented system. The second pass handles one-to-many grouping, where a single bank deposit covers multiple processor settlements. The third pass applies fuzzy matching on amounts within a tolerance window for records missing clean identifiers. Whatever remains after all three passes lands in an exception queue for manual investigation. Each layer exists because the one before it cannot handle every case.
What is the difference between batch and real-time reconciliation?
Batch reconciliation runs overnight. You pull the processor's settlement file and the bank statement at end of day, match them against your ledger, and discover breaks the next morning. The gap between when money moved and when you detect a problem can be 12 to 24 hours. Real-time reconciliation matches each transaction as it settles, using streaming infrastructure to compare ledger entries against processor webhooks and bank notifications within seconds. The trade-off is infrastructure complexity: streaming reconciliation requires event-driven architecture, idempotent matching, and careful handling of out-of-order events.
How many reconciliation breaks does a typical fintech ops team handle per day?
The number varies widely by transaction volume and matching quality. Most exception items are timing breaks that auto-resolve when delayed records arrive within a day or two. The dangerous subset is the items that require manual investigation, because each one represents money that your system cannot account for. Ops teams that lack clear escalation paths for these items accumulate unresolved breaks that compound over weeks.

Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.

Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.

Keep reading

engineeringThreat-Model a Fintech System, Not a Web AppOWASP catches injection and broken auth, but it won't catch the retry that donates twice from someone's bank account.
fintechFraud Detection 101Your fraud model has 45 milliseconds, 99.99% legitimate transactions, and a customer who will leave if you get it wrong.
analysisPayPal Wants Developers to Build on PYUSDPYUSDx lets developers issue custom stablecoins backed by PayPal's dollar, and the real play is making PYUSD the reserve layer.
View more ›