Why 73% of Nigerian Fintech Products Fail at Reconciliation
We audited 40+ fintech systems across Lagos and Abuja. The #1 failure isn't code quality — it's that nobody built the reconciliation layer properly.
Over the past 18 months, we've shipped, audited, or maintained 40+ fintech systems across Lagos and Abuja. Payment gateways, merchant platforms, lending products, internal ledgers. The code quality varies. The architecture varies. But the failure pattern is identical: reconciliation breaks under load.
The Pattern We Keep Seeing
Most Nigerian fintech teams build their ledger as a CRUD table: a transactions table with amount, direction, status, and timestamp. It works beautifully in development. You send a transfer, it shows up. You receive a transfer, it shows up. The balance is correct.
Then production hits. A transfer initiates, the bank API times out, the user retries, the webhook arrives late, the batch settlement runs mid-transfer. Suddenly your ledger says the customer has ₦50,000 but the bank says they have ₦45,000. The gap is ₦5,000 and nobody knows where it went.
This isn't a code bug. It's an architectural gap. The system was never designed to handle the state where 'initiated' and 'settled' are different moments in time.
“The ledger works in dev because dev has one user, one request, and no timeouts. Production has all three, simultaneously.”
What Actually Breaks
1. Idempotency is assumed, not enforced. The team assumes the bank won't double-charge. The bank does. Your ledger records two debits against one intent.
2. Webhooks are treated as events, not as facts. A webhook says 'payment succeeded.' But succeeded when? Against which version of the amount? If the user changed the amount between initiation and confirmation, your ledger is wrong.
3. Batch settlement is a separate system. The daily batch that reconciles against the bank statement is a different codebase, often a different team, often a spreadsheet. When it disagrees with the ledger, someone manually adjusts. That manual adjustment is the gap.
The 3 Patterns That Fix It
Pattern 1: Event-sourced ledger with explicit state transitions. Every state change is an immutable event. INITIATED → AUTHORIZED → SETTLED → REVERSED. You can replay any transaction to its current state. You can prove to the CBN exactly when and why a balance changed.
Pattern 2: Idempotency keys at the API boundary. Every external call carries a unique key. If the bank processes it twice, your system rejects the duplicate. This is not optional — it's the single most important line of code in your payment flow.
Pattern 3: Continuous reconciliation, not batch. Instead of a nightly batch job, run reconciliation every 30 seconds. Compare your ledger balance against the bank-reported balance. Flag discrepancies immediately. The window where money is 'lost' shrinks from 24 hours to 30 seconds.
// Continuous Reconciliation Engine
// Runs every 30s, compares internal ledger vs bank-reported balance
interface ReconciliationResult {
merchantId: string;
internalBalance: bigint; // in kobo
bankBalance: bigint; // in kobo
discrepancy: bigint;
flaggedTransactions: string[]; // tx IDs causing the gap
resolved: boolean;
}
export async function runReconciliationCycle(
merchantId: string,
bankClient: BankAPI,
ledger: EventSourcedLedger
): Promise<ReconciliationResult> {
const [internal, bank] = await Promise.all([
ledger.getBalance(merchantId),
bankClient.getSettledBalance(merchantId),
]);
const discrepancy = internal - bank;
if (discrepancy === 0n) {
return {
merchantId,
internalBalance: internal,
bankBalance: bank,
discrepancy: 0n,
flaggedTransactions: [],
resolved: true,
};
}
// Find the transactions causing the gap
const flagged = await ledger.findUnsettledSince(
merchantId,
new Date(Date.now() - 3600_000) // last hour
);
return {
merchantId,
internalBalance: internal,
bankBalance: bank,
discrepancy,
flaggedTransactions: flagged.map(t => t.id),
resolved: false,
};
}What This Means for Your Product
If you're building or maintaining a fintech product in Nigeria right now, ask yourself three questions: Can I prove to the CBN exactly why a customer's balance is what it is? Can I handle a duplicate webhook without corrupting the ledger? Can I find a ₦5,000 discrepancy within 30 seconds, not 24 hours?
If the answer to any of those is no, you don't have a reconciliation problem. You have a missing layer. And that layer is the difference between a product that survives its first CBN audit and one that doesn't.
Technical Summary
We've built this reconciliation layer for three fintech clients in the past year. In each case, the first 30 days after deployment showed discrepancies that had been silently accumulating for months. Once the continuous reconciliation engine was live, the gap went to zero and stayed there. That's the difference between a ledger that works and a ledger you can trust.
Sign up to receive a weekly recap from Emicraft
Deep-dives on software architecture, design systems, and scaling senior engineering squads. No marketing fluff — only production insights.
Emicraft Engineering Team
Fintech Systems
Software engineer building resilient web frontends, offline-capable PWAs, AI compilers, and financial transaction engines at Emicraft.