Skip to content

Dice

PropertyValue
Slugdice
Round typeSingle-step
RTPConfigurable 80%–99% (default 96%)
ModesClassic (over/under), Range (inside/outside)
Roll range0–9999
VolatilityLow-High (player-controlled)
DevicesDesktop, Mobile
Provably FairYes

Dice is a betting game where the player bets on where a random roll (0–9999) lands. The operator picks one of two UI modes, and the player controls their own risk by choosing how wide their winning window is — the narrower the window, the higher the multiplier.


The operator selects the game mode in the configurator (it changes the slider UI the player sees):

ModePlayer picksBet shape
ClassicA single target with one slider handleRoll lands over or under the target
RangeA [min..max] window with two handlesRoll lands inside or outside the range
  1. The operator has configured a mode (Classic or Range) and an RTP
  2. The player sets their winning window and side
  3. The player places a bet
  4. A random number (0–9999) is rolled
  5. If the roll lands on the player’s winning side, they win at the computed multiplier

The multiplier is derived from the player’s winning chance and the configured RTP:

chance = winning_window / 10000
multiplier = RTP / chance

The narrower the window, the higher the multiplier. The maximum window the player may pick is derived from RTP (maxWindow = floor(RTP × 10000) − 100), which keeps the minimum payout strictly above 1×.

Example (RTP 96%):

  • Window 5000 (chance 50%) → multiplier ~1.92x
  • Window 2500 (chance 25%) → multiplier ~3.84x
  • Window 1 (single number) → multiplier ~9600x (the maximum)

Dice uses single-step rounds — one bet, one result:

sequenceDiagram
    participant Player
    participant Game as Dice Game
    participant Beexar
    participant Wallet as Operator Wallet

    Player->>Game: Choose target=75, mode=over, bet=10
    Game->>Beexar: Place bet
    Beexar->>Beexar: Roll dice → result=82
    Note right of Beexar: 82 > 75 → Win!
    Beexar->>Wallet: POST /betwin (bet=10, win=39.60, finished=true)
    Wallet-->>Beexar: { balance: "529.60", transactions }
    Game-->>Player: Show result: 82, You won 39.60!

Operators configure dice in the game configurator:

ParameterTypeDefaultDescription
rtpfloat0.96Return-to-player, range 0.80–0.99
modestringrangeUI layout: "classic" or "range"
bet_limits.mindecimalConfigurableMinimum bet amount
bet_limits.maxdecimalConfigurableMaximum bet amount

The roll range (0–9999) and the window bounds are derived from RTP — operators do not set them directly.


The result returned after each round:

FieldTypeDescription
rollintRandom roll result (0–9999)
targetintPlayer’s chosen target (Classic mode)
range_minintWindow lower bound (Range mode)
range_maxintWindow upper bound (Range mode)
modestring"over", "under", "inside", or "outside"
wonboolWhether the player won
multiplierfloatApplied multiplier

Dice uses a per-bet server seed with a pre-commitment (“commit-reveal”):

  1. Before the bet, the player fetches the SHA-256 commitment of the reserved server seed via GET /api/v1/dice/seed/next (server_seed_hash). This call is idempotent — polling it does not rotate or reveal the seed.
  2. The player provides a client seed on the bet.
  3. The bet reveals the raw server_seed it used (hash it to verify against the commitment from step 1) and returns next_server_seed_hash — the commitment for the next bet, forming a verifiable chain.
  4. The seed rotates on every bet attempt, including a rejected one (e.g. insufficient funds): the rejected bet’s error carries next_server_seed_hash in params, so a revealed seed is never reused.

The roll is a deterministic function of (server_seed, client_seed, nonce, cursor)nonce and cursor are 0 for dice (each bet uses a fresh, single-use seed). All four inputs are recorded so a settled bet can be reproduced from analytics. The exact derivation a verifier must reproduce byte-for-byte:

In practice it is a one-liner — the first 8 bytes of one HMAC, big-endian, mod 10000:

roll = uint64_be( HMAC_SHA256( key = hex_decode(server_seed), msg = client_seed + ":0:0" )[0:8] ) mod 10000

Exactly (the spec a byte-for-byte verifier follows):

commitment = SHA256(server_seed) // server_seed as the 64-char lowercase hex STRING, verbatim
key = hex_decode(server_seed) // 32 RAW bytes — NOT the hex characters
block(j) = HMAC_SHA256(key, client_seed + ":0:" + j) // nonce = 0; j = 0, 1, 2, … (each block is 32 bytes)
keystream = block(0) ‖ block(1) ‖ block(2) ‖ … // concatenated bytes
limit = 2^64 − (2^64 mod 10000) // 2^64 mod 10000 = 1616 → limit = 18446744073709550000
// read 8-byte big-endian samples from the keystream at offsets 0, 8, 16, 24, 32, …
// take the FIRST sample < limit (rejection sampling removes modulo bias):
roll = sample mod 10000 // 0..9999 — in practice the very first sample (block(0)[0:8])

Three details a correct verifier must get right (the common mistakes):

  • the HMAC key is the hex-decoded raw bytes of server_seed (32 bytes), not the hex characters;
  • exactly 8 bytes are read per sample, big-endian;
  • the mapping uses rejection sampling against 2^64 − (2^64 mod 10000) (taking consecutive 8-byte windows of the keystream until one is below the limit), not a plain mod 10000 — this removes modulo bias. The rejection probability is ≈ 9·10⁻¹⁷, so the first sample is used in every real bet.

The commitment hashes the server_seed hex string (verbatim, lowercase): to check a reveal, compute SHA256(server_seed) and compare it to the server_seed_hash you held before the bet.

A complete, runnable verifier (Node.js — node:crypto):

import { createHmac, createHash } from 'node:crypto'
// Recompute the dice roll from the revealed seed + your client seed.
function diceRoll(serverSeed, clientSeed) {
const key = Buffer.from(serverSeed, 'hex') // 32 raw bytes — NOT the hex string
const N = 10000n
const limit = (1n << 64n) - ((1n << 64n) % N) // = 18446744073709550000n
// keystream = block(0) ‖ block(1) ‖ …; read 8-byte big-endian samples until one < limit
for (let block = 0; ; block++) {
const buf = createHmac('sha256', key).update(`${clientSeed}:0:${block}`).digest() // 32 bytes
for (let off = 0; off + 8 <= buf.length; off += 8) {
const sample = buf.readBigUInt64BE(off)
if (sample < limit) return Number(sample % N) // 0..9999 (returns on the very first sample in practice)
}
}
}
// Check the revealed seed matches the commitment you saw before the bet.
function verifyCommitment(serverSeed, commitment) {
return createHash('sha256').update(serverSeed).digest('hex') === commitment.toLowerCase()
}