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.
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.
The moving parts
From API call to signed webhook
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.
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.
M-Pesa prompt sent
The M-Pesa connector sends an STK push. The customer sees a prompt on their phone and enters their PIN.
Callback received
Safaricom calls back with the result. The connector matches it to the payment and records the provider's reference.
Ledger posts a balanced journal
Every journal must balance. Database constraints keep each balance consistent: available = posted − pending − held − reserved.
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.
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.
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.
- More payment railsOther mobile money networks, cards and bank transfers.
- PayoutsSending money to suppliers, staff and customers. Designed, not built yet.
- Settlement and reconciliationMoving collected funds to merchants and matching every provider record. Built for test mode; live use waits on a licensed partner.
- BillingSubscriptions and invoices on the same ledger.
Also in the monorepo: a merchant dashboard, admin console, public docs and a status page.