Kennedy Wambua
← All work Case study · 2026–now

Mintfa

Payments infrastructure for African businesses. One API to collect money and hold balances, starting with M-Pesa, with the books kept correct by design.

Role
Founder & lead engineer
Stack
NestJS, BullMQ, PostgreSQL, Prisma, Next.js
Status
In development · sandbox-tested
Links
mintfa.com ↗ · API docs ↗
The problem

Every company rebuilds the same fragile plumbing

A business in Kenya that wants to take payments has to integrate M-Pesa, then banks, then cards. Each provider has its own callbacks, its own failure modes and its own idea of what "done" means. Teams end up with payment code they are afraid to touch and balances they reconcile by hand in a spreadsheet.

Mintfa puts one consistent API in front of those providers and keeps a proper ledger behind it, so a business can build on money movement without owning that risk.

Architecture

The moving parts

How a payment moves

From API call to signed webhook

  1. Request arrives with an idempotency key

    The client calls the API with an Idempotency-Key header. If the same key comes back, because the client timed out and retried, it gets the original response instead of a second charge.

  2. Payment recorded as pending

    The API validates the request and stores the payment. Amounts are integers in minor units, so there is no floating-point rounding anywhere.

  3. M-Pesa prompt sent

    The M-Pesa connector sends an STK push. The customer sees a prompt on their phone and enters their PIN.

  4. Callback received

    Safaricom calls back with the result. The connector matches it to the payment and records the provider's reference.

  5. Ledger posts a balanced journal

    Every journal must balance. Database constraints keep each balance consistent: available = posted − pending − held − reserved.

  6. Webhook delivered from the outbox

    The event is written to an outbox in the same transaction as the ledger change, so it can never be lost. A worker sends it, signed with HMAC-SHA256.

Decisions

Trade-offs I made, and why

Why integers, not decimals?

Money is stored as BigInt minor units (cents). Floating-point maths loses fractions of a shilling, and a ledger that is off by a cent is wrong.

Why enforce rules in the database?

Application code has bugs and new code paths. CHECK constraints on balances mean even a buggy deploy cannot write an impossible balance.

Why never retry a payment automatically?

If a payment was sent but never confirmed, sending it again might charge the customer twice. The recovery worker asks M-Pesa for the status instead, and hands anything it cannot resolve to a person. A delayed payment is better than a double charge.

Why an outbox instead of sending webhooks directly?

Sending a webhook inside a database transaction either blocks the transaction or loses the event when the send fails. The outbox commits with the data, and workers claim rows with FOR UPDATE SKIP LOCKED, so several can run without sending anything twice.

Where it's going

More than M-Pesa

M-Pesa is the first rail because it's how most people in Kenya pay. The connector layer is built so new rails plug in behind the same API, ledger and webhooks, without merchants changing their integration.

Also in the monorepo: a merchant dashboard, admin console, public docs and a status page.

More work
← Back to all work Talk to me about payments →