June 8, 2026
Daily reconciliation
The boring job that catches what monitoring misses
What monitoring is actually good at
Dashboards and alerts are built to catch things that break loudly: a spike in error rates, latency crossing a threshold, a worker falling behind its queue. They’re good at that because those failures announce themselves in the metrics you’re already watching. What they’re structurally bad at catching is the quiet kind of wrongness — a ledger entry recorded with the wrong amount because of a rounding difference between your system and a processor, a payout that got marked complete in your database but never actually landed at the bank, an event that was processed twice by a handler that wasn’t as idempotent as it was supposed to be. None of that trips an alert. Everything looks healthy. The system is just quietly wrong.
Reconciliation is the process built specifically to catch that category of failure: compare your own records against an independent source of truth and surface every difference, on a schedule, whether or not anything else suggested a problem.
What gets compared, and against what
In a regulated wallet’s daily reconciliation, the internal ledger — the system of record for every balance and money movement inside the platform — is compared against the actual state of the bank accounts and processor settlements it’s supposed to represent:
interface ReconciliationRun {
date: string;
internalLedgerBalance: Money;
externalBankBalance: Money;
processorSettlementTotal: Money;
discrepancies: Discrepancy[];
}
interface Discrepancy {
type: "missing_in_ledger" | "missing_externally" | "amount_mismatch" | "duplicate";
internalRecordId?: string;
externalReference?: string;
amountDifference?: Money;
}
The job pulls the day’s transactions from the internal ledger, pulls the corresponding settlement and balance data from the bank and from each processor, and matches them up by reference. Anything that doesn’t match — a transaction present on one side and absent on the other, or present on both sides with a different amount — becomes a discrepancy record, and the run doesn’t just log a count, it produces a specific, actionable list.
async function reconcileDay(date: string) {
const internal = await ledger.getEntriesForDate(date);
const external = await bank.getSettlementsForDate(date);
const byRef = new Map(external.map((e) => [e.reference, e]));
const discrepancies: Discrepancy[] = [];
for (const entry of internal) {
const match = byRef.get(entry.externalReference);
if (!match) {
discrepancies.push({ type: "missing_externally", internalRecordId: entry.id });
continue;
}
if (match.amount !== entry.amount) {
discrepancies.push({
type: "amount_mismatch",
internalRecordId: entry.id,
externalReference: match.reference,
amountDifference: subtract(match.amount, entry.amount),
});
}
byRef.delete(entry.externalReference);
}
for (const unmatched of byRef.values()) {
discrepancies.push({ type: "missing_in_ledger", externalReference: unmatched.reference });
}
return { date, discrepancies };
}
Why this has to run every day, not just when something looks wrong
The value of reconciliation comes specifically from its regularity. A discrepancy caught the day it happens is a quick fix: check the one transaction, find the cause, correct the record. A discrepancy that accumulates silently for weeks because nobody ran the comparison is a much harder problem — by the time it surfaces, dozens of small differences may have compounded, the context around any individual one is gone, and the question stops being “what happened to this transaction” and becomes “how long has this been wrong.”
Running it daily also means the discrepancy list itself becomes a leading
indicator. A day with zero discrepancies is unremarkable. A day where the
same discrepancy type starts appearing repeatedly — say, amount_mismatch
against one specific processor — is a signal that something upstream
changed, whether that’s a fee schedule update, a rounding behavior change on
the processor’s side, or a bug in how your own system records that
processor’s settlements. That pattern is invisible in a single day’s run and
obvious across a week of them.
What happens when a discrepancy is found
A discrepancy doesn’t get silently auto-corrected — that would just replace one unverified number with another. It gets surfaced to whoever owns reconciliation as a specific, bounded question: this internal record and this external record disagree by this amount, here’s the reference to look up on both sides. Resolving it is a human decision informed by the specific transaction’s history, and the resolution — along with who made it and why — gets recorded, so the same discrepancy can’t quietly reappear unaddressed.
The relationship to everything else in the system
Reconciliation doesn’t replace the transactional outbox, idempotent handlers, or careful ledger design — it’s the check that exists because none of those guarantees anything about external systems outside your control. A bank’s settlement file can be late, a processor’s API can report a status that turns out to be wrong, a fee can be calculated differently than expected on their end. Your internal consistency guarantees don’t extend past your own boundary; reconciliation is what extends the check across it.
What this means for you
If your system moves money and the only thing watching it is dashboards and alerts, you have a blind spot for exactly the failures that don’t announce themselves — quiet mismatches between what your ledger says and what actually happened externally. A daily reconciliation job, comparing your system of record against an independent external source and surfacing every difference as a specific, resolvable discrepancy, is unglamorous work that catches what monitoring structurally cannot.
30 minutes with a senior engineer.
Tell us what you're building. You'll leave with an honest opinion, even if it's "you don't need us."
Reference calls with past clients are available under NDA during evaluation.