Appearance
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:
| 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 it, or the system generates it) | Always public |
nonce | rng-fortuna, increments per value | Always 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
maxMultiplierfrom the first nonce. The placeBet response carries onlysessionIdandserverSeedHash. The engine broadcastsrevealedServerSeedon theROUND_ENDEDwebsocket 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.