Skip to content
IOIfeanyi O.
← All work

Fintech · Bill payments · 2026

PayZeph

Client
Zephra Studio (in-house product)
Role
Lead engineer, full stack
Year
2026
PayZeph bill payment app home screen

Overview

A Nigerian bill-payment app. Users fund a wallet by card, then buy airtime, data, electricity and cable TV through real provider APIs. Built as a Turborepo monorepo with a NestJS API and a Next.js web app.

The challenge

Bill payment is a two-system problem. Money leaves our wallet, then a third-party provider has to deliver the airtime or electricity token, and either side can fail. The core requirement: a user must never lose money to a purchase that didn't go through.

What I built

  • Wallet funding via Flutterwave. A pending transaction is written first with a unique reference and verified server-side before the wallet is credited; re-verifying a settled reference is a no-op, so double callbacks can't double-credit.
  • Bill purchases through VTpass, with meter and smartcard verification before payment and an automatic wallet refund when the provider call fails.
  • Integer balances in Postgres (bigint, not floats) and a unique reference on every transaction, modelled with Drizzle ORM.
  • Email OTP verification and refresh-token revocation backed by Redis keys with TTLs.
  • A simulation mode: without provider keys, the API falls back to a mock checkout and simulated verification, so the full flow runs locally and in demos.
  • Monorepo with shared UI, ESLint, Jest and TypeScript configs; each app has its own Dockerfile, with docker-compose for Postgres and Redis.

Stack

  • Next.js
  • NestJS
  • TypeScript
  • PostgreSQL
  • Drizzle ORM
  • Redis
  • Turborepo
  • Docker

Results

  • Wallet + four bill types on Flutterwave and VTpass
  • Shipped in 14 days

What I'd change next

  • The wallet debit checks the balance and then updates it in two separate statements, so two concurrent purchases could overdraw a wallet. The fix is a single conditional UPDATE … WHERE balance >= amount (or a row lock), with the debit and the ledger insert in one transaction.
  • Purchases that time out, rather than fail cleanly, need a reconciliation job that re-queries the provider's status before refunding.
  • Test coverage is still the scaffold's; the debit/refund paths are the first things that deserve real tests.