Octo
Event ticketing for venues where the network drops exactly when you need it
A ticketing platform for Zambian events. It sells through mobile money and admits people through QR gates that keep working when the venue's mobile network saturates. Most of the engineering lives in the two places you can't trust the network: taking money, and letting someone through a door.

StatusServer-side sale, payment and reconciliation paths built; scanner app still a scaffold.
- Built for
- Ballo Innovations
- My role
- Architecture and backend implementation covering payments, ticket integrity, and the offline scan reconciliation model.
- When
- 2026 — present
- TypeScript
- tRPC
- Hono
- Prisma
- PostgreSQL
- Firebase Cloud Functions
- DPO Pay
- HMAC-SHA256
The longer version
The problem
Octo sells tickets for events in Zambia, mostly open fields and large halls around Lusaka, where a few thousand people arriving at once will saturate the mobile network or sink it entirely. That single fact drives most of the architecture. Payment happens on a connection that drops mid-checkout, and admission happens on devices that might have no connection at all for the whole length of the event.
In both cases the easy implementation is quietly wrong rather than visibly broken, which is exactly what made them worth writing down.
What I built
The sale and payment paths, the ticket integrity scheme, and the server side of offline scan reconciliation. Tickets get minted only after the server has independently asked the gateway whether money actually arrived, and each one carries a signed payload so a scanner at the gate can verify it without a network call.
What is not built yet
The scanner application is a scaffold. The on-device database, the local signature verification, the clock discipline, the background sync queue and device enrolment are specified and still on the backlog, so what I can defend in detail is the reconciliation policy rather than a shipped end-to-end offline gate.
The same goes for the payment side. Orders carry an expiry, and the sweep that acts on it, reconciling anything the gateway never confirmed, is designed and scheduled rather than built. Both are work I'd finish before this carried event-day volume, and both are why the status above says in-build.
Screens and demos
What it looks like in use, not just what it was designed to do.

Public web: browse by category 
Organizer sign-in 
Organizer analytics: aggregates only, and it says where attribution isn't wired up yet
What made it hard
The problems worth reading about. Everything else in this system was ordinary work.
- 01
Gate scanners have to admit or reject with no connectivity at all, so the server can't assume it sees scans in the order they happened. Scans get re-sorted by the device's own clock and resolved first-scan-wins. But a decision a gate already acted on never gets reversed, because someone who is already inside the venue can't be un-admitted by a database write.
- 02
Nothing marks an order paid except a server-to-server confirmation with the payment gateway. A browser redirect is client-controlled, and the client is exactly who has the incentive to lie about having paid.
- 03
A ticket carries its own proof: a payload plus an HMAC signature a scanner can verify offline. That requirement is also why the signing key lives in Postgres rather than a KMS, because you can't hand a wrapped key to a phone.
What it measures
Each figure says where it came from, so you can judge how much weight to give it. Some are measurements and some are chosen thresholds; the basis line tells you which.
- 0
- Oversell under concurrent reservation
- 3
- Independent guards against double payment
- 500
- Maximum entries per sync batch
200 parallel single-seat reservations against a capacity of 100 yielded exactly 100 successes and 100 sold-out errors, 20 runs out of 20, measured against live Postgres.
An early already-completed check, an atomic conditional update on the order's status, and a final check for tickets that were already minted.
Schema bound on the sync request; sequential resolution is part of why it is capped.
The close calls, written up
Where a choice was genuinely arguable, I wrote it up the way a team records a design decision internally: the constraint, the options I turned down and why, and what it would cost to reverse.
- ADR-006acceptedPayments19 Jul 2026
Trusting only server-to-server payment confirmation
A payment gateway redirect is client-controlled and trivially spoofable, so the only thing allowed to mark an order paid and mint tickets is a server-to-server confirmation call that gets retried, deduplicated at three separate points, and still leaves one real gap I have not closed: nothing yet re-checks an order that the gateway never called back about.
3 options evaluated
- ADR-005acceptedTicket Integrity26 Jul 2026
Where the ticket signing key lives
A ticket is a plain payload plus an HMAC signature, and the obvious place to keep the signing key is a secrets manager. I put it in a database row instead, because the same key eventually has to be provisioned onto a scanner device, and a wrapped KMS key cannot be handed to a phone. That trade is real, I flagged it in the code the day I made it, and it is still open.
3 options evaluated
- ADR-003acceptedDistributed Systems27 Jul 2026
Offline gate scan reconciliation
Two gates scan the same ticket fifteen seconds apart, neither is online, and both let the holder through. When the batches finally reach the server they arrive in the wrong order. The server re-sorts by the device's own clock and resolves first-scan-wins. What it will not do is reverse a decision a gate already acted on, because someone already inside cannot be un-admitted by a database write.
3 options evaluated