HomeWorkWhat I doContact
All projects

TapDrop case study

Giving away real vouchers to honest players

TapDrop is a tap-to-win web game where a win hands over a real partner voucher, so the odds, the stock and the leaderboard all have to hold up against retries, races and scripts.

Role
Full-stack web app
Platforms
Web
Stack
Next.js, Drizzle, PostgreSQL, Vercel
Try it
TapDrop game screen with a big TAP button
TapDrop sign-in sheet with Google or an emailed sign-in link
TapDrop rewards and rules sheet explaining how winning works

The problem

Real prizes, untrusted clients

Most tap games can afford a bug, because the reward is a number on a screen. In TapDrop a win hands over a partner brand's voucher, and each code exists once. Two players cannot get the same code, and a retry cannot pay twice.

The traffic is awkward too. Taps arrive in bursts from phones on bad connections, across several server instances, and some come from scripts. The system has to answer quickly, count each tap once and catch abuse without punishing honest players.

Architecture

One transaction per batch of taps

I put the game rules in a small package that takes time, randomness and config as inputs and never reads a clock itself. A Postgres adapter runs each batch of taps as one transaction holding the player's row lock, so everything one player does is serialised across instances. Each tap carries its own id, so a retry gets its stored answer back and spends nothing.

The win chance is one number from the running game config. Spending TAP in the shop does not enter the roll, and levels only change energy, so the chance per tap stays the same for everyone.

  1. Client

    Next.js, React

    Queues taps, sends batches, retries with the same ids

  2. API

    Next.js on Vercel

    Validates the batch and resolves the player

  3. Game rules

    Tap engine package

    Energy, drops, win roll and voucher allocation

  4. Storage

    PostgreSQL, Drizzle

    One transaction per batch under a row lock

  5. Anti-cheat

    Anti-cheat package

    Reads outbox events, scores players, raises flags

Scoring runs after the response is sent. The tap transaction only reads the stored result.

Key decisions

Where a bug would cost vouchers

01

Anti-cheat outside the tap path

Analysing rhythm, devices and sign-ups inside every tap would slow the busiest path. The signals are also noisy, so one crude rule would punish honest players.

The game writes facts to an outbox table in the same transaction as the change they describe, so a rolled-back tap is never scored. A separate engine runs 13 detectors from one registry, folds events into its own schema and stores a score, and the tap transaction reads only that result. A flagged player's wins are held for review rather than refused, and the win roll itself is never touched.

  • Signals a browser reports count as weak, and their total is capped.
  • The engine has its own schema and writes to no game table.
  • It stores only hashed IPs and drops any other field.

02

One voucher, one winner, no waiting

Many players can win from the same campaign at the same moment. A plain row lock would queue them behind each other, and a read followed by a write could hand one code to two people.

When a campaign's pool is locked, its vouchers are shuffled once and given a fixed draw position and release time. A win runs one UPDATE that picks the next released voucher with FOR UPDATE SKIP LOCKED, so concurrent winners take different rows without blocking. Database constraints back this up: one tap cannot own two vouchers, and a position cannot repeat within a campaign.

  • The shuffle is a Fisher-Yates driven by Web Crypto.
  • Release times can spread stock evenly across a campaign's window.
  • A Postgres test races two winners and expects different vouchers.

03

Weekly prizes that pay exactly once

The daily cron can run late or be missed, and a leaderboard week still has to close and pay. Two instances settling the same week must never pay it twice.

Both the cron and the first request after a week closes can trigger settlement. One conditional UPDATE sets the settled time inside the transaction that writes the awards, so only one caller ever gets to pay. Each period reserves its voucher prizes when it opens, so the board only advertises prizes it holds.

  • Settlement waits 60 seconds after the week ends, so in-flight taps land first.
  • A prize voucher that is rejected passes to the next eligible player.
  • A prize's TAP counts for the week it was won, not the week it is claimed.

Let's build
something
together.

Open to senior roles: remote, hybrid or on-site in Ho Chi Minh City. The quickest way to reach me is email or LinkedIn.