EmicraftEngineering Firm

Appearance

Start a project
Emicraft Engineering TeamAugust 15, 2026
FINTECH, RECONCILIATION, LEDGER, CBN, ARCHITECTURE, NIGERIA

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.

Key Engineering Takeaway: The reconciliation gap isn't a bug to fix. It's a missing layer. You need a reconciliation engine that treats the ledger and the bank statement as two sources of truth that must converge.

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.

reconciliation-engine.ts
// 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.

Key Engineering Takeaway: Reconciliation isn't a feature you add later. It's the foundation. Build it first, and everything else sits on solid ground.

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.

✓ Delivered every Tuesday✓ Unsubscribe anytime
TA

Emicraft Engineering Team

Fintech Systems

Software engineer building resilient web frontends, offline-capable PWAs, AI compilers, and financial transaction engines at Emicraft.