Provably Fair
Every game vertical can run with provably-fair RNG: a SHA-256 commitment + HMAC-SHA512 chain that lets a player independently verify that no round's outcome was manipulated.
The certified rng-fortuna service owns the secret serverSeed and runs the derivation. The engine opens a session, draws values through rng-fortuna's PF endpoints, records each batch into a persisted audit trail, and closes the session at round end — at which point rng-fortuna reveals the seed for client-side verification.
The protocol in 30 seconds
Four pieces of state per session:
| Name | Who controls it | When the player sees it |
|---|---|---|
serverSeed | rng-fortuna (32 random bytes) | Revealed after round end |
serverSeedHash | rng-fortuna (SHA-256(serverSeed)) | Shared before play |
clientSeed | Client (player chooses, or generated) | Always public |
nonce | rng-fortuna, increments per value | Always public |
Because serverSeedHash is published before the player bets, the seed cannot be retroactively changed. Because clientSeed is the player's, outcomes cannot be precomputed.
PF mode is opt-in per game config via config.rng === 'pf' (defaults to plain fortuna). The engine never holds serverSeed.
Wire shape
The pf block on the placeBet response:
{
"sessionId": "…",
"serverSeedHash": "…",
"clientSeed": "…",
"revealedServerSeed": "…", // only on the closing step
"rngData": [
{ "id": "pf:0-2", "values": [0.42, 0.18, 0.91], "nonces": [0, 1, 2], "type": "double" },
{ "id": "pf:3-3", "values": [16], "nonces": [3], "type": "int" }
]
}rngData is the per-round audit trail: every value drawn from rng-fortuna, in order, with its nonce. type distinguishes calls returning floats in [0, 1) (double) from calls returning integers in a game-defined range (int).
Setting the client seed
The player's clientSeed is supplied as a top-level field on the placeBet request body:
// placeBet request
{
"stake": 1,
"clientSeed": "my-lucky-seed"
}Only the first placeBet of a round consumes the supplied seed — that is the call that opens the PF session against rng-fortuna. On continuation steps (blackjack hit, mines reveal, fixed-odds wager-feature collect, etc.) the round is already bound to the seed it opened with, so any clientSeed field on follow-up bodies is ignored.
If the field is omitted or empty on the opening call, rng-fortuna generates a random clientSeed for the round. Whatever seed the round ends up bound to is always echoed back on the pf block of every placeBet response, so the client can record it for verification without having to remember what it sent.
Reveal timing per vertical
- Slots, blackjack, single-step rounds. placeBet is terminal;
revealedServerSeedlands on the placeBet response. - Multi-step rounds with a collect step (mines, fixed-odds wager features, frogger). The seed reveals on the closing step's response (collect, or whichever step ends the round).
- Crash. placeBet opens the round but does not close it (the timer does), so the seed must stay secret until then — otherwise the player could pre-derive
maxMultiplierfrom the first nonce. The placeBet response carriessessionIdandserverSeedHashonly;revealedServerSeedbroadcasts on theROUND_ENDEDwebsocket event so every observer can verify the committed multiplier.
Player verification
The minimum a player has to remember is the serverSeedHash from the first placeBet response. Everything else lands in the closing response (or the ROUND_ENDED broadcast for crash):
1. Save serverSeedHash from the first placeBet response.
2. Play freely.
3. At round close, receive { serverSeedHash, clientSeed, revealedServerSeed, rngData }.
4. Verify locally:
- Commitment: SHA-256(revealedServerSeed) === serverSeedHash.
- Values: recompute each rngData entry from (revealedServerSeed,
clientSeed, nonces[]) and check that the recomputed values
match values[]. 'double' rows verify by direct comparison.
'int' rows are bracket-checkable when the verifier knows
the call's min/max out-of-band (e.g. from the game's
rules); the commitment check is the strong proof either way.The verification can run entirely in the browser using Web Crypto — no third-party libraries needed.
Server-side verify
The engine also exposes pfVerify — POST the revealed seeds and the player's action list, the engine re-runs the exact game logic the round ran and returns the full rngData audit and game result. Useful when the client doesn't want to re-implement the HMAC chain, or for cross-checking against the player's locally-computed result. Every game-type has its own request shape; see the Provably Fair section in each per-game guide under Games.
Audit trail
Every round's rngResult.pf.rngData is persisted to gameState. Each entry carries id, values, nonces, and type — everything a verifier needs. PF entries use id = pf:start-end; raw-Fortuna entries (non-PF code paths) keep the per-call UUID. Operator audit consumers that join on the legacy UUID shape need to handle the pf:start-end id format on PF rounds.