A multi-vendor deals marketplace, built for a market that pays by phone.
Most checkout code assumes a card. In Ghana that assumption fails: people pay with mobile money, from a handset, and the flow is nothing like a card charge.
Each one is a separate set of screens over a shared schema, so a change to how an order works shows up correctly in all three rather than being reimplemented.
Browse deals, pay, then follow the order without calling anyone.
An order queue and a payout view, scoped so a vendor sees only their own.
The operator’s view: everything that needs a person to decide.
This is where most of the engineering went. The rules below exist because each one has a failure it prevents.
Every amount is an integer. Prices, totals and balances are held in pesewas, never in floating point. Exchange rates are stored scaled by a million so a manually entered rate keeps its precision instead of drifting a pesewa at a time.
Two gateways sit behind one interface. Which one collects is a setting an operator can change. But a payment records the gateway that started it, and every later step — the OTP, the status poll, the callback — is routed by that stored name rather than the current preference.
So switching the default never strands a prompt already sitting on somebody’s handset.
Webhook deliveries are recorded before they are acted on. Providers retry. Without a record of what has already been seen, a retry credits an order twice.
A scheduled sweep does the jobs nothing else can. It chases payments still pending past a timeout and asks the gateway directly what happened, catching the sales whose callback was lost. It also retires prompts stuck waiting on an OTP the payer walked away from, so they stop being carried forward as if they might still succeed.
A marketplace grows more moving parts than a shop. The schema carries all of it in one place rather than spreading it across services.
One TypeScript codebase, server and client, on TanStack Start. Drizzle keeps the schema and the queries in the same language as the interface, so a column rename fails at compile time instead of at runtime in front of a customer.
Vendor integrations go through an adapter, with a mock implementation used in development — the fulfilment flow can be exercised end to end without touching a live vendor system.
Tell us what you’re trying to build in your own words. We’ll reply within a working day.