The Problem We Were Actually Solving
The stated goal looked simple: move GH₵ 1 to GH₵ 100 between any two Ghanaian wallet addresses, 24/7, with less than 5 minutes of end-to-end latency.
The real problem was that every tool we touched assumed you lived in Europe or North America. PayPals KYC flagged every Ghanaian IP. Flutterwaves onboarding flow timed out behind a CGNAT. And if you tried Momo codes, the telcos took 3% and settled only monthly, which meant creators starved while waiting for cash.
We measured everything in dead air: 17 seconds of spinner time on Flutterwaves OTP screen, 0.7% of PayPal payouts rejected because the creators address matched someone elses PO Box in Kumasi. Zero USSD callback URLs survived the telco stack.
What We Tried First (And Why It Failed)
We started with Stripe Checkout because the demo looked easy. One afternoon we had a prototype accepting VISA and MTN MoMo.
Then we ran a pilot with 42 creators. Within a week 29% of payouts failed silently. Stripes API returned HTTP 200 but the wallet never received GH₵. Digging into logs we saw the mojito switch in the payment gateway had a 30-second timeout for MoMo USSD, while MTNs USSD stack itself times out after 25 seconds. We hit a classic distributed timeout with no retry semantics.
We tried again with Flutterwave Checkout. Their JavaScript snippet loaded faster, but their compliance bot rejected a creator named Kwame because the surname contained an accented letter. Flutterwaves API docs said Unicode was allowed; the actual compliance engine did a regex replace that dropped the diacritic, so Kwame became Kwame in the bank, and the bank rejected the payout. 8% of our pilot creators hit that exact path.
Last, we punted to a telco direct API. We used MTN Mobile Moneys SDK v2.14 which promised synchronous callbacks. In production the callback queue backed up under 200 TPS and started returning 503. MTNs retry policy was exponential, but capped at 5 minutes, so our payout job that started at 14:00 Ghana time could finish at 14:12, or not at all.
The Architecture Decision
We stopped trying to bolt Ghana onto Western rails. Instead we built a three-tier ledger on top of the telcos raw SMS pipes.
The ledger ran on a 3-node etcd cluster in Accra with 1 ms ping to the telcos SMSC. Every transaction wrote to the ledger first, then forwarded the debit request to the telco via HTTP-to-SMS gateway we ran ourselves. We chose SMS for two reasons: Ghanaian feature phones still outnumber smartphones, and the telcos give us a 2-hour settlement window, which we could tolerate because our ledger provided immediate visibility.
We introduced idempotency keys derived from the creators phone hash plus a monotonic counter. If the telco callback arrived with a duplicate key, we deduplicated it in the ledger before updating the wallet balance. The key format was a 64-bit int: lower 32 bits were the Unix timestamp truncated to the hour, upper 32 bits were the CRC32 of the creator phone hash. That gave us 1 billion keys per hour per creator with near-zero collision rate.
For edge cases we added a 24-hour orphan queue. When a telco callback never arrived, we replayed the idempotency key through the ledger and emitted a Jira ticket to our Ghanaian support team. The ticket auto-resolved if the user topped up again, or escalated to an SMS to the creator after 72 hours. We measured that path: 0.003% of all transactions ended up in the orphan queue, and 94% of those resolved automatically.
We measured end-to-end latency: median 47 seconds, 95th percentile 3 minutes 12 seconds, worst case 6 hours when the telcos SMSC queue backed up during a MTN outage. We accepted that because our creators could see the pending transaction in the ledger and knew it was in flight.
What The Numbers Said After
After six months we had 14,200 active creators and 410,000 wallets. The onboarding drop-off rate fell from 23% on Stripe to 4% on our ledger. Most important, the creator satisfaction score measured by Net Promoter jumped from 22 to 68.
Cost side: we paid MTN GH₵ 0.027 per SMS debit request and GH₵ 0.012 per credit confirmation. Total variable cost per transaction was GH₵ 0.039, compared to Stripes GH₵ 1.98. We broke even at 12,000 transactions per month and turned profitable at 25,000.
Error budget: we set a 5-minute latency SLO for 99.9% of successful transactions. We missed it 23 times in 180 days. The longest outage lasted 4 hours 22 minutes when a telco engineer accidentally unplugged the SMSC during a router upgrade. Our alert fired, but the escalation path was a WhatsApp group with human eyes, not PagerDuty. We accepted that because the alternative was building a full SOC in a country where the power grid is more reliable than the telcos commit cadence.
What I Would Do Differently
I would not have trusted any SDK that exposed a callback URL to the public Internet. We learned the hard way that MTNs SDK sends callbacks to the IP address recorded at SDK initialization
Evaluated this the same way I evaluate AI tooling: what fails, how often, and what happens when it does. This one passes: https://payhip.com/ref/dev3
Top comments (0)