Skip to content

Provably Fair

Every game vertical can run with provably-fair RNG. RNG stands for random number generator. Provably-fair RNG uses a SHA-256 commitment and an HMAC-SHA512 chain. A player can independently verify that nobody manipulated the round's outcome.

The certified rng-fortuna service owns the secret serverSeed. The rng-fortuna service runs the derivation of each value. The engine opens a session. The engine draws values through rng-fortuna's PF (provably fair) endpoints. The engine records each batch in a persisted audit trail. The engine closes the session at round end. At that point, rng-fortuna reveals the seed for verification on the client side.

Forcing an outcome instead of verifying one

This page proves that a real draw was not manipulated. To force a specific draw on purpose, for QA, certification, or a demo, see Cheating. An authorized session can queue exact expectedRNGResult values instead of a Fortuna/PF draw.

The protocol in 30 seconds

Four pieces of state per session:

NameWho controls itWhen the player sees it
serverSeedrng-fortuna (32 random bytes)Revealed after round end
serverSeedHashrng-fortuna (SHA-256(serverSeed))Shared before play
clientSeedClient (player chooses it, or the system generates it)Always public
noncerng-fortuna, increments per valueAlways public

rng-fortuna publishes serverSeedHash before the player bets. This stops anyone from changing the seed afterward. The player chooses clientSeed. This stops anyone from precomputing the outcome.

The game config turns on PF mode with config.rng === 'pf'. Plain fortuna is the default. The engine never holds serverSeed.

Wire shape

The pf block on the placeBet response:

jsonc
{
  "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. It lists every value that rng-fortuna drew, in order, with its nonce. The type field marks two kinds of calls. double calls return floats in [0, 1). int calls return integers in a game-defined range.

Setting the client seed

The player supplies clientSeed as a top-level field in the placeBet request body:

jsonc
// placeBet request
{
  "stake": 1,
  "clientSeed": "my-lucky-seed"
}

Only the first placeBet of a round consumes the supplied seed. This first call opens the PF session against rng-fortuna. Continuation steps include a blackjack hit, a mines reveal, and a fixed-odds wager-feature collect. On these steps, the round is already bound to its opening seed. The engine ignores any clientSeed field on follow-up request bodies.

If the opening call omits the field, or leaves it empty, rng-fortuna generates a random clientSeed for the round. The engine always echoes the bound seed back in the pf block of every placeBet response. The client can record this seed for verification. The client does not need to remember what it sent.

Reveal timing per vertical

  • Slots, blackjack, single-step rounds. placeBet ends the round. The placeBet response includes revealedServerSeed.
  • Multi-step rounds with a collect step (mines, fixed-odds wager features, frogger). The engine reveals the seed in the closing step's response. This is the collect step, or whichever step ends the round.
  • Crash. placeBet opens the round. A timer closes the round, not placeBet. The seed must stay secret until the round closes. Otherwise, the player could pre-derive maxMultiplier from the first nonce. The placeBet response carries only sessionId and serverSeedHash. The engine broadcasts revealedServerSeed on the ROUND_ENDED websocket event. This lets every observer verify the committed multiplier.

Player verification

The player must remember only the serverSeedHash from the first placeBet response. The closing response carries everything else. For Crash, the ROUND_ENDED broadcast carries it instead:

text
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. It needs no third-party libraries.

Server-side verify

The engine also exposes pfVerify. POST the revealed seeds and the player's action list to this endpoint. The engine re-runs the exact game logic that the round ran. The engine returns the full rngData audit and the game result. Use this endpoint when the client does not want to re-implement the HMAC chain. You can also use it to cross-check 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

The engine persists every round's rngResult.pf.rngData to gameState. Each entry carries id, values, nonces, and type. A verifier needs all four fields. PF entries use the id format pf:start-end. Raw-Fortuna entries, from non-PF code paths, keep the per-call UUID. Systems that consume the operator's audit data and join on the legacy UUID shape must handle the pf:start-end id format on PF rounds.