Case study · Fintech · Web app
Buydair
Pay smarter, stay safer.
Buydair lets people top up a prepaid wallet through Paystack, by card, bank transfer or USSD, and buy airtime, data, electricity tokens and TV subscriptions for themselves or family in seconds. Alongside payments, free short courses teach digital literacy and how to recognise fake alerts, phishing and SIM-swap scams.
- Role
- Solo: product, design and engineering
- Type
- Fintech · Web app
- Status
- Live and in use
- Stack
- React · Vite · Tailwind CSS · TanStack Query · Node.js · Express · MongoDB (Mongoose) · Zod · Paystack · Socket.IO · Netlify


THE PROBLEM
Buying airtime or paying a bill should take seconds. The hard part isn’t the happy path; it’s what happens when the network drops mid-payment, the user taps Pay twice, or a provider takes too long to answer. Each of those moments can turn into a double charge or money that seems to disappear.
The goal was a wallet where money is never charged twice, never lost halfway and never guessed at, plus a learning side that helps people avoid the scams that target anyone paying online.
HOW IT WORKS
KEY DECISIONS
- 01
A payment never happens twice
The risk · Networks drop, users double-tap and apps retry. Every repeated request could charge the user again.
- Every payment carries a unique idempotency key, created when the user taps Pay and sent with the request.
- The server stores each key with a unique index per user. A repeat of a finished request gets the saved reply back instead of a second charge; a repeat of one still in progress is refused.
- If a payment fails on a wrong PIN, its key is released so the user can try again properly.
- A retry test proves it: send the same payment again and the charge count stays at one.
- 02
Top-ups are credited exactly once
The risk · Paystack confirms a payment through a webhook, and the app can also ask whether it went through. Both can arrive at the same moment.
- Both paths run the same settle step, which credits with a conditional update that only succeeds while the top-up is still uncredited.
- One path wins; the other finds nothing left to update and stops.
Funding.findOneAndUpdate( { _id, credited: false }, { $set: { credited: true } } ) - 03
Money can’t vanish halfway
The risk · If the server fails between debiting one wallet and crediting another, money could leave without arriving. Two payments at once could both spend the same balance.
- The debit, the credit and the record run inside one database transaction: all of it happens, or none of it does.
- The balance check is part of the debit itself, so two simultaneous payments can’t both spend the same ₦1,000.
Wallet.findOneAndUpdate( { user, balance: { $gte: amount } }, { $inc: { balance: -amount } } ) - 04
Never guess the outcome
The risk · A bill provider that times out hasn’t necessarily failed. Calling it failed invites a second payment; calling it successful can lose money.
- Every purchase ends in one of three clear states: successful, failed and refunded, or pending.
- Money is taken first and recorded as pending. A clear failure is refunded automatically; an unclear answer stays pending and is never guessed.
- A background job re-checks pending purchases every few minutes using each purchase’s own request ID. It settles them, refunds ones the provider never received after ten minutes, and hands anything older than two days to a human.
- Unpaid top-ups are marked abandoned after 30 minutes but still re-checked, so a late bank transfer is still credited.
- If the phone loses connection mid-payment, the app shows a “we’re confirming this” screen instead of “failed”, so nobody is tempted to pay again.
- 05
Trust nothing from the outside
The risk · Anyone can try to send a fake “payment successful” message, or edit a request to pay less.
- Every Paystack webhook is verified with an HMAC-SHA512 signature, and the amount and currency must match what was expected before anything is credited.
- Prices are calculated on the server, never taken from the app.
- Purchases are refused early if the bill-payment float can’t cover them.
- The main balance is deliberately capped at ₦2,000, a risk limit while the product is young.
- Practice money can’t buy anything; it only works for demo transfers between users.
- 06
Accounts and admin built for real money
The risk · A wallet is only as safe as its accounts, and the people who manage it.
- Passwords are hashed with bcrypt, login errors never reveal whether an email exists, every request is validated with Zod, and sensitive routes are rate-limited.
- Access tokens are short-lived; refresh tokens live in an http-only cookie and only their hash is stored.
- A hashed transaction PIN guards every payment, locks after repeated wrong attempts, and weak PINs like 1234 are refused.
- Admin rights are checked against the database on every request. Refunds and suspensions need the admin’s PIN and a written reason, and every admin action is recorded in an audit log.
- Balances are never edited by hand: money only moves through the same settle and refund steps the app itself uses.
BEYOND PAYMENTS
- Three courses with scam drills, read-aloud and adjustable text size
- Certificates anyone can verify
- Practice money for safe demo transfers
- Buying for saved family contacts, with birthday reminders
- Live balance updates over Socket.IO
- Admin dashboard: user search, stuck-transaction recovery and growth metrics
- Landing page with an interactive purchase demo, branded emails and an installable mobile web app
OUTCOME
Buydair is live, handling real top-ups and real airtime, data, electricity and TV purchases.
Retries, double taps and dropped connections are safe by design: a payment is charged once, credited once, and every purchase ends in a clear, recoverable state.
Built solo, covering product, design and engineering.
WHAT’S NEXT
Launch the “Grown-ups & elders” learning track, already marked as coming soon in the app.
Raise the balance cap gradually as monitoring and fraud checks mature.
Have something that needs to work in the real world?