№ XII · 2026 · play-money, parked
Provably Fair Casino
A play-money web casino — blackjack, roulette, slots, and an even-money bet —
where every outcome is derived from HMAC-SHA256 over a seed the
house committed to before the player wagered. Anyone can recompute any
result. FastAPI + async SQLAlchemy + Postgres behind an Astro frontend, 196
tests, eleven subagent-driven plans. A front-end over a friends' Discord
economy; built for the build.
What it is
A friends' Discord server runs a fake-currency economy through the
UnbelievaBoat bot — the usual collect-income, buy-roles loop. This
is a polished web casino bolted onto that economy: log in with Discord, move
cash from the bot into casino chips, play four games, move
chips back out. No real money touches it at any point, and that is the entire
design premise — with the stakes fake, the only interesting problem left is
proving the games are honest.
Four games ship: Bet (a single even-money gamble), Slots, Roulette, and Blackjack — the last one fully interactive, with a deal / hit / stand / double lifecycle. A public verifier page lets anyone, no login, recompute an outcome from its seeds.
The provably-fair guarantee
Every player holds a three-part seed: a secret 256-bit server seed the house generates, a client seed the player can set themselves, and a nonce that increments by one with every bet. The trick is the order of reveal:
- Commit first. Before the player bets, they are shown only
SHA-256(server_seed)— the commitment. It pins the server seed down completely, but reveals nothing about it. - Derive deterministically. The outcome is
HMAC-SHA256(server_seed, "client_seed:nonce:cursor"), sliced into 32-byte blocks and then into floats in[0, 1). The cursor advances to produce as many bytes as the game needs — one float for a coin flip, a full Fisher-Yates shuffle for a deck of cards. - Reveal on rotation. When the player rotates their seed, the old server seed is disclosed. They paste it into the public verifier, which re-runs the exact same HMAC math. Because the commitment was published before any bet, the house provably could not have swapped the seed to dodge a payout.
How a bet is sealed — and checked
Every instant game runs through one lifecycle function. It reads the player's
active seed, computes the outcome at the current nonce, writes an immutable
bets row recording the seed id, nonce, client seed, wager, payout
and the full outcome JSON, then debits the wager and credits any winnings
against the chip ledger — all in a single transaction — and finally increments
the nonce. The bet row is the permanent receipt.
Blackjack needed one extra rail. Because it is interactive, the server holds an in-progress deck between requests — and the naive implementation returned the whole deck object in the API response, which meant a player could read the next card in their browser's dev tools. The fix strips the undealt deck and the cursor from every blackjack response; the client only ever sees cards already face-up. It was caught in a review pass, not in production.
The money model
Two ledgers, never allowed to drift. UnbelievaBoat owns the "cash" balance; the casino's own Postgres owns the "chips" balance. The bot's API is touched on exactly two operations — deposit and withdraw — and never during play. A bet only ever moves chips inside the casino's own ledger.
That ledger is the most defensive piece of the backend. Balance changes go
through one function that locks the wallet row FOR UPDATE, refuses
any debit that would push the balance negative, and appends an immutable
chip_transactions row recording the signed delta and the resulting
balance. Deposits and withdrawals carry idempotency keys, so a double-clicked
button or a retried request can never move money twice.
Architecture
┌──────────────────┐ ┌────────────────────┐
│ Astro + TS │ ←──→ │ FastAPI │
│ static frontend │ │ Discord OAuth · │
│ lobby · 4 games │ │ sessions · CORS │
└──────────────────┘ └─────────┬──────────┘
│
┌────────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ provably-fair │ │ chip wallet │ │ UnbelievaBoat API │
│ HMAC-SHA256 │ │ FOR UPDATE · │ ←→│ deposit / withdraw │
│ + commitments │ │ append-only ledger │ │ only │
└───────┬────────┘ └─────────┬──────────┘ └────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────┐
│ PostgreSQL — users · seeds · chip_wallets · │
│ chip_transactions · cash_transfers · bets │
└──────────────────────────────────────────────────┘
The frontend is a static Astro site that talks to the backend cross-origin with
credentialled fetches; the backend is a FastAPI app with Discord OAuth2,
cookie sessions, and a CORS allowlist of exactly one origin. All dynamic DOM on
the frontend is built through small el() / setChildren()
helpers that set textContent — never innerHTML — so
backend-supplied strings can't become script. A security hook enforced that
rule mid-build when an early lobby draft interpolated a username into markup.
How it was built
Same discipline pattern as Mintly and Odvolanie. One design spec, then the work decomposed into eleven sequential plans — provably-fair core, backend + Discord auth, chip wallet, deposit/withdraw, the four game engines, seed rotation and the verifier, then the frontend foundation and pages. Each plan executed subagent-driven: a fresh implementer per task following test-first development, a spec-compliance review, then a code-quality review, fixes folded back, and a whole-plan review before merge.
The reviews earned their place. They caught a missing CORS middleware that would have blocked every frontend request, the blackjack deck leaking through the API, and a fistful of smaller issues — missing CHECK constraints, an unlinked reference id, a 32-bit column where a 64-bit one belonged. 196 tests passed at the end, every one written before the code it covers.
Why it isn't launched
The casino is feature-complete and the test suite is green. What stands between it and the friend group is purely operational: a Discord application has to be registered for OAuth, and the live UnbelievaBoat economy needs an API token wired in. Ten minutes of clicking, none of it code.
And it was never meant to earn — it is a play-money toy for a specific group of
friends, with no revenue path by design, so it doesn't compete for the launch
energy the income projects get. It got a clean park instead. A
DEV_MODE flag was added on the way out: with it on, the casino
auto-creates a local player with a starting chip stack and skips the bot
entirely, so the whole thing is fully playable solo without any Discord setup
at all.
Stack
- Backend: Python 3.14, FastAPI, async SQLAlchemy 2.0 over asyncpg,
PostgreSQL, Pydantic Settings, Starlette session + CORS middleware,
httpxfor the Discord and UnbelievaBoat clients - Provably-fair core: standard-library
hmac/hashlib— an HMAC-SHA256 byte stream, SHA-256 commitments, and a Fisher-Yates deck shuffle, all pure and golden-vector tested - Frontend: Astro 5, TypeScript, hand-written CSS — a static site of a
lobby, four game tables, wallet, bet history, seed settings, and a public
verifier; all dynamic DOM via safe
textContenthelpers - Persistence: six tables — users, seeds, chip wallets, an append-only
chip_transactionsledger,cash_transferswith idempotency keys, and immutablebetsreceipts - Tests: 196 passing — pytest, pytest-asyncio, respx for mocking the Discord and UnbelievaBoat HTTP calls; engine golden vectors, ledger concurrency, the full game lifecycles, and the verifier round-trip
- Process: spec → eleven plans → subagent-driven execution with two-stage review per task; local Postgres via docker-compose
What's actually here
A complete, runnable reference for the one problem worth solving in a casino: making "trust us" unnecessary. The commitment scheme means the house cannot lie about an outcome after the fact, the ledger means it cannot lose track of a balance, and the verifier means none of that has to be taken on faith. The play-money framing isn't a limitation — it's what keeps the project a pure study of verifiable randomness instead of something with a licence problem.
If the friends ever want it wired into their server, the ten operational minutes are all that's left. Until then it runs solo in dev mode, and it stands as a study in commitment schemes, transactional ledgers, and shipping a provably-honest system end to end.