Pay for a pack → open it → keep or claim the cards. No cash-out. Every card's odds are published before you buy, and every result is mathematically verifiable after.
A complete Next.js application built around one idea: a pack opening should be something you can check, not something you have to trust. The server commits to a result before you see it, publishes the exact probability of every card on the buy screen, and reveals the seed afterwards so anyone can re-run the arithmetic.
1 · The result is committed before you open it.
A SHA-256 commit-reveal scheme. Before the open, the server generates a secret
serverSeed and publishes only its hash — a commitment it cannot change
afterwards. After the open it reveals the seed, so anyone can hash it, compare it
to the pre-shown commitment, and re-derive the exact card. The
/verify page does this for you; every opening stores the
seeds needed to reproduce it independently.
2 · The odds on the buy screen are the odds the server rolls against.
Both the displayed percentages and the server's weighted pick are derived from
the same weight values, so they cannot disagree. A pack whose odds don't sum to
100% is refused rather than served — see assertValidPool in
lib/odds.ts. The pack page also runs a live simulator: it
executes the real pickCard thousands of times in your browser and charts
observed against published frequency.
3 · The reveal matches what you actually pulled.
The animation is rarity-scaled: a Common gets a clean flip, a Mythic gets the
full screen-flash. The intensity is derived from the card's revealTier, never
from a fake near-miss — the buildup can only reflect the result the server had
already committed to.
serverSeedHash = sha256(serverSeed) // published BEFORE the open
roll = parseInt(sha256(`${serverSeed}:${clientSeed}:${nonce}`).slice(0, 13), 16) / 16 ** 13
card = pickCard(pool, roll) // walk the weighted pool13 hex characters is exactly 2⁵², so the roll is representable in a JavaScript
double without loss. Multi-card packs use nonce, nonce+1, … — one roll per
slot, each independently verifiable. Your clientSeed is mixed into every roll
and you can change it at any time, which is what stops the house from having
pre-picked a seed to your disadvantage.
npm install # installs deps and generates the Prisma client
# point DATABASE_URL / DIRECT_URL at a Postgres instance, then:
npm run setup # prisma generate + db push + seed
npm run dev # http://localhost:3000Every visitor is issued an anonymous cookie session and 1000 demo coins. There is no sign-up, no password, and no personal data.
| Script | What it does |
|---|---|
npm run dev / build / start |
Next dev server / production build / serve |
npm test |
Vitest — odds validation, weighted-pick distribution, fairness reproducibility |
npm run db:push |
Apply the Prisma schema to Postgres |
npm run db:seed |
Seed the cards and the four packs (validates odds = 100%) |
npm run setup |
All three of the above in order |
| Pack | Price | Cards per pack |
|---|---|---|
| Poké Ball | 50 | 5 |
| Great Ball | 120 | 7 |
| Ultra Ball | 300 | 9 |
| Master Ball | 750 | 11 |
122 cards across six rarity tiers — Common, Uncommon, Rare, Epic, Legendary, Mythic. Each pack publishes its full per-card odds table, summing to a verified 100%, on the same screen as the buy button.
app/
(marketing)/ landing
store/ pack grid, and [packId] detail with odds + simulator
open/[openingId]/ the reveal, and its share image
collection/ wallet/ owned cards and completion; the demo wallet
verify/ history/ the verifier and an append-only opening log
pulls/ wall/ your luck vs expected; recent rare pulls
fairness/ account/ how it works; username + client seed
api/ all endpoints
components/ PackCard, OddsTable, Reveal, HoloCard, VerifyForm, …
lib/
fairness.ts hash, roll, recompute
odds.ts weighted pick + 100% validation
catalog.ts packs, odds, and the card dataset
queries.ts server-side Prisma → DTO mapping
db.ts Prisma singleton with cold-start retry
session.ts anonymous cookie session
prisma/ schema.prisma, seed.ts
tests/ odds and fairness specs
TypeScript · Next.js 15 (App Router) · Tailwind · Framer Motion · Prisma ·
Postgres · Node crypto (SHA-256) · Vitest · Vercel.
Deployment is covered in DEPLOYMENT.md.
- Demo currency only. Balances are clearly-labelled demo coins with no monetary value, and there is no cash-out anywhere — by design. Claiming a physical copy is the only "exit", and it is a stub in this prototype: there is no fulfillment, address handling, or shipping.
- The payment seam is isolated.
app/api/wallet/deposit-demo/route.tsis the single place a real processor would slot into; the open → collect → verify flow would not change. - Rate limiting is in-memory, which is adequate for a single-instance demo but would need a shared store in a real deployment.
- Pokémon names, card images and set names are © The Pokémon Company / Nintendo / Game Freak. They are used here only to populate a non-commercial demonstration of the fairness and odds engine, which is deliberately game-agnostic — the card source can be replaced without touching the engine. Any public or commercial use would require licensing.
MIT for the source code — see LICENSE. What that licence does not
cover (the Pokémon card data and images) is set out in NOTICE.