W-06 · Case study
Ember
Payments in Nigeria assume connectivity. Ember asks what still works when it's gone — and is careful about what a prototype on mock balances can and cannot prove.
- Prototype on simulated balances. Not a production payment system.
- Four contributors
- Private repository
- Ed25519 (tweetnacl) on client and server
- TypeScript · React + Vite (PWA) · Express · tweetnacl · Ed25519 · QR
Thesis
Offline, you can't prevent a double spend. You can bound it and detect it.
§ 01
The problem
Bank transfers, POS and USSD all need a live connection. When mobile data or Wi-Fi drops — routinely, in markets and transport parks — commerce stops, even for people holding digital money.
§ 02
The idea
While online, a customer pre-funds an offline wallet and receives a bank-signed authorisation with a spending limit. Offline, the merchant shows a payment request as a QR code; the customer scans it, signs the payment with their device key and shows a second QR with the signed packet. The merchant checks both signatures locally, against a bank public key built into the app.
When either phone reconnects, queued payments sync and the server settles them into a mock ledger — or flags them, if a packet is replayed or spending goes past the authorised limit.
§ 03
The trade-off at the centre
Preventing an offline double spend requires secure hardware that ordinary phones don't guarantee. So Ember doesn't claim to. It detects the double spend at reconnect and bounds the possible loss to the customer's offline limit — the same trust model used by offline-capable chip cards.
Diagram 01
An offline payment, and what happens later
₦2,000 at a market stall, both phones offline.
Customer pre-funds; the bank signs an authorisation certificate with an offline limit.
Every stage, in words
- 01Fund (online) (server · bank signer) — Customer pre-funds; the bank signs an authorisation certificate with an offline limit.
- 02Request (merchant QR) — Merchant shows amount, payee and a one-time nonce. Expires after 120 s.
- 03Sign (customer device · Ed25519) — Customer signs the payment packet with their device key and shows it back as a QR.
- 04Verify offline (merchant device) — Merchant verifies the bank's and the customer's signatures with no connection.
- 05Sync (POST /api/sync) — Whichever phone reconnects first uploads its queued packets. Sync is serialised.
- 06Settle or flag (mock ledger) — Validate first, then credit. Replays and over-limit spends are flagged, not settled.
- 07Durable ledger (Postgres · atomic, planned) — Atomic, constraint-backed settlement and a hardware-backed signer.
§ 04
What I worked on
Settlement was the part most likely to be wrong in quiet ways, so I moved it to validate-before-mutation: a packet is fully checked before any balance changes, transaction ids and request nonces must be unique, and sync requests are serialised. Payment requests now expire after 120 seconds and carry a one-time nonce.
On the transport side I worked on QR reliability — an animated multi-frame encoding with parity recovery, scanner frame reassembly, and a matte anti-glare display that keeps the screen awake — and researched NFC as a secondary offline transport.
§ 05
Actual versus planned
What exists is a React/Vite PWA and an Express server, with keys in the browser's local storage and an in-memory ledger that resets on restart. The target architecture in the team's diagrams — a native app with a hardware-backed signer and a Postgres ledger with atomic settlement — is a plan, not an implementation. A review of that design concluded it isn't ready to handle real money until settlement atomicity, the funds-reservation model and the device trust model are specified.