October 20th, 2026

{ Engineering }
CoinQuest: what happened when we turned a conference into a game

Obi Uchenna David
On 16 October 2026, 708 people at CX Con ’26 played CoinQuest, a game we built for the day. Together they made 16,013 connections in about six hours.
CX Con is a customer experience conference, held this year at Landmark Centre in Lagos. CoinQuest ran on attendees’ phones from 9 AM to 5 PM. You caught virtual coins around the venue, scanned other people’s badges to connect, and chose whether to spend your points or save them in a vault that earned interest. The top of the leaderboard won vouchers, T-shirts and professional headshots.
This is what we built, what people actually did with it, and what we changed while the game was running.
How the game worked

CoinQuest was a web app, opened from a link or a QR code on the stage screen. People signed in with the email they registered with and a one-time code. There were three ways to earn points.
| Action | Points | Rule |
|---|---|---|
| Scan someone’s badge | 1,000 to each person | Once per pair; a speaker’s badge was worth 2,000 |
| Catch a coin on the venue map | A few hundred | Tripled during a golden bonus, 11:20 AM to 12:00 PM |
| Keep points in the vault | 4% every 30 minutes, compounding | 8% once you had met 5 people; vault locked until 4 PM |

Each new connection came with an ice breaker question, such as “What is the most Lagos thing that happened on your way here?”. The idea was to start a real conversation, not just collect a scan.

The wallet and the vault were the Cowrywise part of the game. Points in the wallet were safe and simple. Points in the vault grew, but you couldn’t touch them until the afternoon.
Prizes went by final rank:
- 1st: ₦100,000 voucher
- 2nd and 3rd: ₦50,000 voucher each
- 4th to 8th: Cowrywise T-shirt
- Top 50: a professional headshot, on top of any other prize

The day in numbers
By the 3:40 PM freeze, 698 people were on the leaderboard. They had made 16,013 connections and caught 882 coins. 708 people signed in over the whole day.
What the freeze was: at 3:40 PM the leaderboard froze. The app saved everyone’s final rank, and that snapshot decided the winners. Players could keep scanning after that, but nothing they earned moved the rankings.

Most people joined in the first hour after the doors opened: 285 new sign-ins between 10 and 11 AM. The 1 PM hour then had 10,518 badge scans, three times as many as the four hours before it combined. In that hour 452 different people scanned, and the typical player scanned 7 badges.
The 10 AM rush worked out to one new player every 13 seconds, on top of 52 people who were already playing before the doors opened. The busiest single minute came at 1:30 PM: 241 badge scans in 60 seconds, four every second. The connection count went from 2,338 at 11 AM to 3,499 at 1 PM, then to 16,013 by the freeze.
Nine in ten players made at least one connection, and the median player met 23 people.
| What players did | Players | Share |
|---|---|---|
| Made at least one connection | 638 of 708 | 90% |
| Answered a trivia question correctly | 94 of 112 who answered | 84% |
| Caught at least one coin | 183 of 708 | 26% |
| Saved points in the vault | 120 of 708 | 17% |
What players actually did
People came to meet each other, and the points followed. Meeting people produced 96% of all points earned by the 3:40 PM freeze. Vault interest added 4%, and coins under 1%.
| Source | Points | Share |
|---|---|---|
| Meeting people | 32.0 million | 96% |
| Vault interest | 1.28 million | 4% |
| Catching coins | 0.15 million | 0.5% |
Coins barely mattered. We expected the map to be the main game. By the 3:40 PM freeze, players had caught 882 coins in total. Even the leaders earned fewer than 3,000 points from coins, against 230,000 or more from connections.
The vault decided first place. All of the top 8 met between 230 and 350 people. The winner met 318, fewer than three of the players below them. What set them apart was saving early. Compounding at 8% every 30 minutes added about 190,000 points and gave them a lead of 137,000 over second place. Three players in the top 8 kept almost everything in their wallets and earned under 1,000 points of interest.
Scanning turned into a race. Some players made over 300 connections, about one a minute for five hours. In the busiest hour, the top five scanned 144 to 169 badges each, roughly one every 21 seconds. That speed doesn’t leave time for an ice breaker. It was spread across hundreds of real attendees at the venue, so we saw it as the game’s incentives at work, not cheating.
What we changed during the day
We changed the rules four times while the game was live. Every change was a database setting or a small deploy, and nobody had to reinstall anything.
| When | Change | Why |
|---|---|---|
| Morning | Sign-in limited to people who had checked in at the registration desk | Keep the game to people physically at the venue |
| Midday | Prize shop removed; prizes go by final rank only | One player had already spent 70,000 points on a T-shirt. We refunded them in full, and they went on to rank in the top 8 |
| Afternoon | Sign-in opened to anyone, with walk-ins asked for their name | People without a registration wanted to play |
| 3:40 PM | Leaderboard frozen, in-app prize codes cancelled | Winners were announced on stage at the close instead of collecting codes from a booth |
The last change came late. At the freeze, the app automatically issued 58 prize codes to the top 50. We cancelled them all before anyone collected one, and switched in-app redemption off.
We also added 20 new ice breakers at midday, taking the pool to 25, because people were seeing the same five questions.
The tech stack, and why we chose it
We had one event day and no second chance, so every choice favoured fewer moving parts. CoinQuest is one Next.js app on Vercel talking to one Postgres database, and every game rule runs on the server.
| Layer | What we used | Why |
|---|---|---|
| Delivery | Progressive web app (PWA) at coinquest.cowrywise.com | Attendees started from a QR code on the stage screen. No app store download or review, one codebase for iPhone and Android, and it can still be added to the home screen. |
| App framework | Next.js 16 (App Router), React 19, TypeScript | One deployment serves the player app, the staff prize desk, the admin console and the JSON API. Server route handlers hold every game rule. |
| Hosting | Vercel, Node runtime | A deploy takes minutes, which is how we shipped rule changes during the event. Vercel signs in to AWS with OIDC, so no AWS keys are stored. |
| Database | Postgres on Supabase | Points are money-like, so we needed real transactions and row locks (SELECT … FOR UPDATE). Every balance change writes a ledger row, and a CHECK constraint stops any balance going below zero. |
| Money maths | decimal.js on the server, numeric(20,4) in Postgres | Vault interest compounds every 30 minutes. Floating-point numbers would drift; the phone’s own figure is for display only. |
| Sign-in | Email one-time codes via Amazon SES, sent from hello@cowrywise.com | Every registered attendee had an email. The session is a random 32-byte token in an httpOnly cookie, stored hashed (SHA-256). |
| Live updates | Polling: map and leaderboard every 10 s, player state every 20 s | Coins spawn on a server timetable, so a 10-second refresh felt live. Nothing extra to run or scale. |
| Map | Google Maps, with open-source MapLibre and Esri satellite tiles as a fallback | The game kept working if Google’s key failed. |
| Badge QR codes | qrcode for drawing; the browser’s BarcodeDetector for scanning, with jsQR as a fallback | BarcodeDetector is fast where it exists. jsQR covers iPhones, where Safari doesn’t support it. |
| Phone state and styling | TanStack Query, Zustand, Tailwind v4 with Cowrywise design tokens | Query handles polling and caching. Zustand holds small screen state. Tailwind kept the Cowrywise look consistent. |
| Rules and settings | Stored in the database, edited in the admin console | Prize times, point values and sign-in rules changed mid-event without a deploy. |
| Checks | zod on every request; Vitest and Playwright tests | The server trusts no amount sent by a phone. Playwright ran the game on emulated iPhone 14 and Pixel 7 screens. |

A phone never reaches the database. It calls the app’s API, the app applies the rules inside a locked transaction, and only then does a balance change.
How the engine works
All the rules live in one server-side folder, the game engine. Each player action is one short database transaction: lock the player’s row, check the rules, write to the ledger, commit.
A badge scan. Each badge shows a QR code that links to the app with that attendee’s token. Scanning with the phone camera or in the app sends the token to the server.
- The server rejects a scan of your own badge or an unknown token.
- It stores the pair with the lower attendee id first. A unique index on the pair means two people can only connect once, whoever scans first.
- Both players get 1,000 points in the same transaction.
- On your fifth connection the vault rate doubles. The server settles the interest earned so far at the old rate first.
A coin catch. This takes three calls, and nothing is paid until the last one.
- Start: the server checks you are within 45 m of the coin by GPS (plus up to 25 m for GPS accuracy) or scanned a nearby zone QR code in the last 30 minutes. It rejects a position that moved faster than 12 m/s since your last one.
- Throw: the phone sends how long you held the ring. The server recomputes the throw quality from that time and rejects throws under 450 ms.
- Decide: wallet or vault. Only now are points credited. A unique index on player and coin makes a double catch impossible.
Vault interest. Interest compounds continuously until the vault unlocks:
vault(t) = balance × (1 + r/100) ^ (Δt / 30 minutes)
The server only writes interest to the ledger just before a balance or rate changes, so each stretch compounds at one rate. Between writes, the phone works out the same formula to animate the number; it never saves it.
The ledger. Every change to a wallet or vault writes a ledger row with the amount, the reason and the balance after it. A database rule stops any balance going below zero. A transaction that hits a deadlock or a serialization conflict is retried automatically. Because of the ledger, the T-shirt refund on the day was one entry, not a manual fix.
Retries on venue Wi-Fi. Every action that pays points carries an idempotency key. If a phone retries after a dropped connection, the server returns the stored first response instead of paying twice.
The leaderboard. Rank is wallet plus vault, including interest earned so far. Results are cached for 5 seconds, so hundreds of phones polling every 10 seconds rarely reach the database.
Rate limits. Sign-in allowed 300 attempts a minute per IP address, because the whole venue Wi-Fi shared one. Catches, throws and connections are also limited per player.
What we left out on purpose
- The Supabase client in the browser. Phones never talk to the database. Row-level security is on for every table with no public policies, so the public Supabase API returns nothing, and the service key exists only on the server.
- Supabase Auth. The attendee list was already our identity source. A small session table was simpler than mapping attendees onto another user system.
- Websockets and Supabase Realtime. Polling was enough for a game that changes every few seconds, and it meant one less service to watch on the day. Realtime can be added later without touching the game engine.
- A native app. Two app store builds and reviews for a one-day event wasn’t worth it. The trade-off: iPhones have no vibration API, and we can’t count home-screen installs on the server.
- Cron jobs. Coin waves are created by the first request after their start time, with seeded positions, so every player sees the same map and nothing runs on a schedule.
The app held up. In a mid-morning check across all requests we found no server errors, and the health check answered in about 0.4 seconds. The busiest hour, 1 to 2 PM, handled 10,518 badge scans.
What we’d change next time
The game did its main job: hundreds of people met each other. Most of what we’d change is about balance and about deciding the rules before the doors open.
- Reward the conversation, not the scan. Cap connections per hour, or award the points only after both people answer the ice breaker.
- Make coins worth chasing. At under 1% of points, the map was decoration. Coins need to be worth a meaningful share of a typical connection count.
- Tone down compounding. 8% every 30 minutes turned one early decision into a 190,000-point lead. A lower rate or a cap would keep saving rewarding without deciding the game.
- Settle sign-in and prizes in advance. We changed who could sign in twice and the prize model twice on the day. Each change worked, but players noticed.
- Plan the ending. Freeze the leaderboard at the time winners are announced, and show a game-over screen afterwards. At 10:30 PM, 12 people were still opening the app to check their rank.
Next article


Chima Ataman;
{ Team Lead Infrastructure } Cowrywise