1. Scope of verification
Once a server seed has been revealed, two independent checks are available for any GrowBit Original bet:
- The commitment.
sha256(serverSeed)must equal theserverSeedHashpublished before the outcome existed. Agreement establishes that the server seed was not substituted after the bet was placed. - The outcome. Re-running the game's published algorithm on
(serverSeed, clientSeed, nonce)must reproduce the result exactly. Agreement establishes that the result was not altered.
Both checks are necessary. Neither is sufficient alone, and there is a further question that neither addresses.
1.1. The limitation of a commitment
A commitment establishes that a server seed was not substituted afterwards. It does not establish how that seed was selected beforehand.
An operator holding both the server seed and the client seed while neither is published could generate candidate server seeds, calculate the outcome each would produce for the next several bets against the client seed already in its possession, and publish the commitment of whichever candidate it preferred. Every published commitment would still verify, and every outcome would still re-derive correctly. The scheme would remain internally consistent while the player was disadvantaged.
This is known as pre-commitment seed selection, and it is not addressed by stronger hashing. It is addressed by ordering: the operator must be committed to a server seed before it is able to see the other input.
1.2. Ordering of the two inputs
Seed chain games (Coinflip, Dice, Limbo, Keno, Mines, Towers, Blitz, War, Blackjack and Cases) use a two-stage seed lifecycle for every seed generation created by rotation:
1. GrowBit generates a server seed, records it with no client seed, and publishes sha256(serverSeed). The seed is now committed and cannot be changed.
2. The player supplies the client seed. The browser generates 32 bytes using the platform cryptographic random number generator by default; any other value may be substituted from Your seeds. The submission carries back the commitment the page was displaying, and GrowBit applies it as a compare-and-set condition, so a commitment altered between those two moments causes the submission to be refused rather than accepted.
3. Only then will the seed generation issue a nonce. A bet placed against a generation with no bound client seed is refused rather than assigned one automatically.
Stage 1 is a genuinely earlier event, not merely an earlier step within one request. A client seed submitted for a game whose commitment has not yet been published is refused, rather than the commitment being created in order to accommodate it. Generating the server seed within the request that already carries the client seed would place both inputs in GrowBit's possession at the moment of selection, which is the condition this ordering exists to prevent. For a generation created by rotation, that earlier event is the rotation itself: the rotation request carries no client seed and returns the new commitment, which Your seeds then displays, and the client seed follows in a separate request.
Once these stages have completed, no candidate set remains for GrowBit to search. The property relied upon is the ordering rather than the unpredictability of the client seed: a memorable phrase chosen by the player is equally effective, provided GrowBit did not know it while its own input remained unfixed.
Each bet records which ordering produced it. In the Provably Fair panel, clientSeedSource: player indicates that the sequence above applied. clientSeedSource: server identifies a seed generation in which both inputs were generated together by GrowBit, and which therefore carries a weaker guarantee. The first seed generation of every seed chain is created this way, so that a new account can bet without first supplying a client seed. Rotating the seed replaces such a generation with one created under the sequence above. Bets settled on such a generation continue to report it accurately and are not reclassified.
Crash rounds are shared by every participant and therefore cannot use a per-player chain. The equivalent property is obtained from the participants themselves:
clientSeed = "crash:" + gameIndex + ":" + roundSalt
roundSalt = sha256( "<betId>:<entropy>" joined by "," , bets ordered by betId )Each entropy value is 32 hexadecimal characters generated in a participant's browser and submitted with their bet, during the betting phase, after the commitment has already been broadcast. GrowBit is therefore committed before any of that input exists. A participant may choose their own value freely, but evaluating its effect would require the server seed, which remains undisclosed until the round settles. The crash point is not calculated when the round is seeded; it is written in the same atomic update that closes betting.
Every contribution is broadcast to all connected clients, so a round settled on a client seed that does not correspond to the bets observed would be detectable. The verification script for Crash rebuilds the input from the bet list rather than relying on the published value; the contributions for any round remain available afterwards through the endpoint documented in section 4.4.
Case Battles are committed in the battle record at creation and are visible to every participant before any roll. The client seed is derived from the exact roster and slot order, so neither can be altered after the commitment.
1.3. Limitations of the system
Two limitations remain, and GrowBit does not represent otherwise.
Coordination between the operator and a participant in a Crash round. An operator with knowledge of the server seed could, in principle, direct a participant acting with it to submit a particular value as the final contribution, and thereby influence the round. No commitment and reveal scheme prevents this, since an operator able to do so could equally place the bet itself. Preventing it would require an input controlled by neither party, which means dependence on an external randomness source and on that source remaining available. This is a recognised option and it is not implemented.
Time of issue. A commitment receipt, described in section 1.4, establishes that GrowBit issued a given commitment. It does not establish when the commitment was issued, because the time recorded within a receipt is GrowBit's own assertion. A receipt is evidence of a point in time because the holder possessed it at that point.
1.4. Commitment receipts
Each seed generation, and each Crash round a player joins, is accompanied by a detached Ed25519 signature over its commitment:
{
"v": 1,
"subject": "chain:<userId>:<gameKind>:<generation>" | "crash:<roundId>",
"serverSeedHash": "<64 hex>",
"issuedAt": "<ISO-8601>",
"kid": "<key id>",
"signature": "<base64url>"
}The signed bytes are the first four fields, prefixed with growbit-commitment and joined by newline characters in that fixed order. Fixed-order concatenation is used in preference to JSON because JSON key ordering is not standardised, and a verifier implemented in another language could otherwise reject a valid receipt.
The public key is published without authentication:
curl -s https://growbit.net/api/v1/fairness/commitment-keyWhat a valid receipt establishes. That GrowBit issued this commitment for this subject. Only the holder of the corresponding private key could have produced the signature, so GrowBit cannot subsequently deny having issued it. Two valid receipts naming the same subject but different commitments would, taken together, evidence inconsistent statements by GrowBit. The subject includes the seed generation number precisely so that the several commitments a seed chain legitimately acquires over its life are not mistaken for such an inconsistency.
What a valid receipt does not establish. The time of issue, as set out in section 1.3. Receipts should be saved before play, from Your seeds, and may be independently timestamped by any means the holder chooses.
Measure not implemented. Combining each period's commitments into a Merkle tree and publishing the root through a party independent of GrowBit would remove the need to rely on GrowBit's signing key and would establish time of issue. This requires an external witness and the corresponding availability dependency. It is recorded here as outstanding rather than represented as delivered. Per-bet blockchain records are not proposed: they provide no material assurance beyond a periodic root at substantially greater cost.
1.5. The verification scripts
Four scripts are published: one for each derivation scheme, and one for commitment receipts. Each is a single file with no dependencies beyond the runtime's built-in cryptographic library, and none contains GrowBit code. That independence is what allows a script to detect a defect in the GrowBit implementation rather than reproduce it.
Each script:
- accepts the values displayed in the Provably Fair panel;
- prints every intermediate calculation rather than a verdict alone;
- checks the commitment when
--server-seed-hashis supplied; - recomputes the outcome, within the scope stated in the section for its scheme, and compares it with the published result;
- exits with a non-zero status on any mismatch; and
- includes a
--self-testcovering both positive and negative cases, including that altering a single character of the seed changes the result and that an incorrect expected value causes the run to fail.
Each accepts --help for its full option list. They require Node 18 or later and no installation step.
Two schemes are versioned, because their derivation has changed and settled bets must continue to re-derive under the version that produced them. The scripts therefore require the version as an explicit argument, and the Provably Fair panel displays the version applicable to each bet. Inferring the version would re-derive a historical bet under a mapping that did not exist when it settled, and report a mismatch on a correct result.
1.6. Verify a commitment receipt yourself
node verify-receipt.cjs --receipt receipt.json --public-key <base64> \
[--expect-hash <64 hex>] [--expect-subject <string>]The script prints the exact bytes covered by the signature, so the subject and the commitment can be confirmed to fall within them. Its --self-test includes the case in which a signature over the same fields without the domain prefix must be rejected; without that check, a signature produced by GrowBit for an unrelated purpose could be presented as a receipt.
2. Scheme A: seed chain games
Games: Coinflip, Dice, Limbo, Keno, Mines, Towers, Blitz, War, Blackjack
These games share one per-player, per-game seed generation, consisting of a serverSeed of 32 random bytes in hexadecimal, a clientSeed, and a nonce that advances by one for each bet. Only serverSeedHash is published while the generation is active. The server seed is disclosed when the generation is rotated.
2.1. The byte source
Message:
message(cursor) = clientSeed + ":" + nonce + ":" + cursorDigest:
bytes(cursor) = HMAC_SHA256(key = serverSeed, message = message(cursor))serverSeed is used as the HMAC key, as its UTF-8 string bytes. It is not hex-decoded first.
Byte extraction and float conversion. Each digest is 32 bytes, which is eight draws. Draws are taken left to right, four bytes each. When a digest is exhausted, cursor increments and the next digest is produced:
u32 = uint32_big_endian(bytes[offset .. offset + 4])
u = u32 / 2^32The divisor is 2^32, not 2^32 - 1, so u ranges over [0, 1). It can be exactly 0 and can never be 1.
cursor starts at 0 and only exists to extend one nonce past eight draws. Games needing eight or fewer draws never leave cursor = 0.
2.2. shuffleRange
Games that need a permutation, such as a deck, a board, or a number pool, use a Fisher-Yates shuffle driven by the same stream:
shuffleRange(n):
arr = [0, 1, ..., n - 1]
for i = n - 1 down to 1:
u = nextDraw()
j = floor(u * (i + 1))
swap(arr[i], arr[j])
return arrThe precise form matters and is the most common source of error in a re-implementation: descending i, exactly one draw per iteration, and the loop terminating at i = 1. A 52-card deck therefore consumes 51 draws, which is seven digests.
2.3. Per-game outcome mapping
The algorithms are not identical past the byte source. Each game maps draws differently.
Coinflip, one draw
u = draw[0]
outcomeSide = u < 0.5 ? "heads" : "tails"The house edge is applied in the payout rather than in the draw: a win pays a fixed 19200 basis points (1.92x), giving a theoretical return of 0.5 x 1.92 = 96%.
Dice, one draw
u = draw[0]
roll = min(9999, floor(u * 10000)) an integer in [0, 9999]
won = direction == "under" ? roll < target : roll > targetOffered multiplier, with edgeBips = 400:
winCount = direction == "under" ? target : 9999 - target
multiplierBips = floor((10000 - edgeBips) * 10000 / winCount)Limbo, one draw
u = draw[0]
edge = edgeBips / 10000 (= 0.04)
multiplier = u <= edge ? 1.0 : (1 - edge) / (1 - u)
rolledMultiplierBips = clamp(floor(multiplier * 10000), 10000, 1e14)The house edge is applied in the u <= edge branch. The remainder is the standard inverse-uniform curve, giving P(multiplier >= M) = (1 - edge) / M.
Keno, shuffleRange(40)
permutation = shuffleRange(40)
drawn = first 10 entries, each + 1 1-based numbers in [1, 40]
hits = |spots and drawn|The displayed list is sorted ascending; the shuffle order is the draw order. Payout comes from the per-(risk, spots) paytable indexed by hits.
Mines, shuffleRange(25)
shuffled = shuffleRange(25)
minePositions = first mineCount entries, sorted ascendingCashout multiplier after k safe reveals on a 25-cell board with m mines. This is a fair cumulative product with the house edge applied once, at the end, not per step:
fair_k = PRODUCT over j = 1..k of (25 - j + 1) / (25 - m - j + 1)
multiplierBips = floor(fair_k * (10000 - edgeBips))A zero-reveal cashout pays exactly 10000 bips (1.00x). No wager was at risk, so no edge applies.
Towers, 9 draws, one per row
Nine rows. Each difficulty has either exactly one trap or exactly one safe tile per row, so a single draw fully specifies a row:
u = draw[row]
idx = floor(u * tiles)
traps == 1 -> idx is THE trap column; every other column is safe
safe == 1 -> idx is THE safe column; every other column is a trapdifficulty tiles safe traps
easy 4 3 1
medium 3 2 1
hard 2 1 1
extreme 3 1 2
master 4 1 3fair_k = (tiles / safe) ^ k
multiplierBips = floor(fair_k * (10000 - edgeBips))Edge applied once, same as Mines.
Blitz, a variable number of draws
Cards are drawn independently from an infinite 52-face deck until the round is decided:
face_i = min(51, floor(u_i * 52))
stop when a face repeats (loss) OR when `target` distinct faces are seen (win)The draw count is not fixed in advance, so Blitz is the one chain game that cannot be verified by taking a fixed prefix of the stream. You must replay the stopping rule. Hard cap 53 draws, by the pigeonhole principle.
War, shuffleRange(52)
One fresh 52-card deck. All seven relevant cards come from one nonce:
playerCard = deck[0]
houseCard = deck[1]
burn = deck[2], deck[3], deck[4]
warPlayerCard = deck[5]
warHouseCard = deck[6]Card encoding is the raw deck index c in [0, 51]:
rank = c mod 13 (0 = '2' ... 12 = 'A', ace HIGH)
suit = floor(c / 13) (0 = hearts, 1 = diamonds, 2 = clubs, 3 = spades)Suits 0 and 1 are red, which is what the Coloured Tie side bet resolves on. The engine always derives the war cards so they can be verified, but the server withholds them until you commit to going to war.
Blackjack, shuffleRange(312)
A six-deck shoe, freshly shuffled per round. Every card of the round is fixed by one nonce. The engine is a pure function of (shoe, bet, actions), so a round is replayed by re-running it with the recorded action list.
player[0] = shoe[0]
dealerUp = shoe[1]
player[1] = shoe[2]
dealerHole = shoe[3]
cursor = 4Thereafter cards are consumed strictly in resolution order: player hands left to right, where each hit or double takes shoe[cursor++]. A split moves the second card into a new hand inserted immediately after the current one, and both resulting hands immediately draw one replacement card, left first then right. Once every player hand is terminal the dealer reveals the hole card and draws until standing on all 17s (S17). If every player hand busted, the dealer draws nothing.
Shoe indices decode via c mod 52 and then the 52-card rule above.
2.4. Verify a chain-game bet yourself
node verify-chain-game.cjs --game <game> \
--server-seed <hex> --client-seed <str> --nonce <int> \
[--server-seed-hash <hex>] [--expect '<json>']Per-game options: dice takes --target and --direction, keno takes --spots, mines takes --mines and optionally --reveals, towers takes --difficulty and optionally --picks, blitz takes --target. Coinflip, Limbo, War and Blackjack need no extra options.
For Blackjack the script recomputes the shoe and the initial deal, and prints the first twelve cards of the shoe as shoeHead. It does not replay player actions or the dealer's play: cards drawn after the deal are checked against that list using the order in section 2.3, and a round that draws beyond the twelfth card cannot be checked in full with this script.
Worked example, Mines with three mines:
node verify-chain-game.cjs --game mines \
--server-seed 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--client-seed parity-client-seed --nonce 5 --mines 3
#0 cursor=0 bytes=0x928bcf37 u32=2458636087 u=0.5724458226468414
#1 cursor=0 bytes=0x2c3976e5 u32=741963493 u=0.1727518376428634
...
shuffleRange(25) -> first 3 entries are the mines
minePositions = 1, 12, 23Worked example, Limbo:
node verify-chain-game.cjs --game limbo \
--server-seed 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--client-seed parity-client-seed --nonce 3
u = 0.4216531964484602
edge = 0.04
multiplier = (1 - edge) / (1 - u) = 1.659903701558971
rolledMultiplierBips = floor(multiplier * 10000) = 16599Worked example, War, where the same nonce fixes every card:
node verify-chain-game.cjs --game war \
--server-seed 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--client-seed parity-client-seed --nonce 8
player = 47 (Ts), house = 17 (6d)
rank 8 vs 4 -> win
war cards = 7 (9h) / 11 (Kh)Altering a single character of any of those server seeds changes the recomputed result. This is the negative case that demonstrates the check is meaningful.
3. Scheme B: case rolls
Games: Cases, Case Battles
Cases do not use the draw stream. There is no float conversion. One roll comes from one u32.
Two versions exist and both are frozen. A case roll determines a payout, so an opening that has already settled must continue to re-derive the value on which it settled. Altering the mapping in place would reinterpret historical results and cause previously settled openings to fail verification. The version is therefore recorded on every opening and every battle at creation, displayed in the Provably Fair panel, and required by verify-case-roll.cjs as the --algo argument.
Version 1 (identifier case-roll-v1), legacy and biased:
digest = HMAC_SHA256(key = serverSeed, message = clientSeed + ":" + nonce)
u32 = uint32_big_endian(digest[0 .. 4])
roll = (u32 mod 100000) + 1 an integer in [1, 100000]Note the message has two parts, not three. There is no :cursor suffix, and a roll derived with the Scheme A message will not match.
Statistical bias in v1. 2^32 is not a multiple of 100000. floor(2^32 / 100000) = 42949 complete cycles account for 4,294,900,000 values, leaving 67,296 values unaccounted for. Those fall on rolls 1 through 67,296, so each of those rolls has 42,950 preimages out of 2^32 while rolls 67,297 through 100,000 have 42,949. The relative bias is 1 / 42949, approximately 2.33e-5, in favour of the lower portion of the range.
Whether this operated to a player's advantage or disadvantage depended on where a given case placed its lower-value items. It is disclosed as a defect in the mapping rather than as an intended characteristic. The figures above are verified by a test that counts preimages directly rather than by sampling, and verify-case-roll.cjs --self-test asserts the same arithmetic.
Version 2 (identifier case-roll-v2), current and exactly uniform:
message(cursor) = "case:v2:" + clientSeed + ":" + nonce + ":" + cursor
digest(cursor) = HMAC_SHA256(key = serverSeed, message(cursor))
stream = 8 big-endian u32 per digest, left to right,
cursor incrementing when a digest is exhausted
limit = floor(2^32 / 100000) * 100000 = 4294900000
draw u32 from the stream until u32 < limit (rejection sampling)
roll = (u32 mod 100000) + 1Rejection sampling makes the mapping exactly uniform: every roll in [1, 100000] has precisely 42,949 of the 4,294,900,000 accepted values. A draw is rejected with probability 67296 / 2^32, about 1.57e-5, so the first u32 is accepted essentially always. The loop exists to make the algorithm exact, not because it runs often.
The case:v2: prefix provides domain separation. Without it the message would be byte-identical to the Scheme A message clientSeed:nonce:cursor. No seed chain is currently shared between the two schemes, and the prefix ensures that this remains safe if that changes.
Every case's items partition [1, 100000] into contiguous [rangeFrom, rangeTo] windows summing to exactly 100000, and the item is whichever window contains roll. That mapping is identical in both versions.
3.1. Case Battles
Same primitive, with a derived client seed and a positional nonce.
clientSeed = sha256("casebattle:" + battleId + ":" + tokens)where tokens are the slot-ordered participant tokens, comma-joined: "{slot}:{userId}" for a human, "{slot}:bot:{botId}" for a bot. Because the client seed commits to the exact roster and slot order, neither can be changed after the seed is fixed.
For round r (0-based) and slot index s (0-based, meaning slot - 1):
nonce = r * totalSlots + s
roll = caseRoll(serverSeed, clientSeed, nonce)Jackpot battles pick the winner with extra draws taken past the last round nonce, so the draws they use are provably unused by any round.
Each team's weight is the total value its slots unboxed, in minor units, or in Crazy mode the pot less that total. W is the sum of the weights, and the windows are laid out in ascending team order. If W is 0, every team has a weight of 1.
Under v1 the ticket was built from two [1, 100000] rolls at N = totalRounds * totalSlots and N + 1:
ticket = (roll1 - 1) * 100000 + (roll2 - 1) an integer in [0, 1e10)
winner = the team whose cumulative weight window contains
floor(ticket * W / 1e10)That mapping is biased by approximately W / 1e10, because 1e10 is not a multiple of W and W is a prize pool expressed in minor units. On a large battle this reached the 1e-3 range, materially greater than the roll bias described above.
Under v2 the draw is two full u32 halves of one digest at nonce N, forming a 64-bit ticket:
digest = HMAC_SHA256(serverSeed, "case:v2:" + clientSeed + ":" + N + ":0")
ticket = (uint32(digest[0..4]) << 32) | uint32(digest[4..8]) in [0, 2^64)
winner = the team whose window contains floor(ticket * W / 2^64)The same multiply-and-floor mapping is then biased by at most W / 2^64, approximately 5e-14 for a prize pool of one million minor units. A battle created before this change continues to use v1, including a battle already in progress at the time of deployment, because the version is fixed on the battle record at creation rather than read from the current setting.
The battle's serverSeed is revealed once the battle ends or is cancelled, never while it is rolling.
3.2. Verify a case roll yourself
node verify-case-roll.cjs --algo v1|v2 \
--server-seed <hex> --client-seed <str> --nonce <int> \
[--server-seed-hash <hex>] [--expect-roll <int>] \
[--ranges '<json>'] [--expect-item <itemId>]--algo is required. The Provably Fair panel displays the version under which a given bet settled. Inferring it would re-derive a historical opening under a mapping that did not exist at the time and report a mismatch on a correct result.
Worked example, v1:
node verify-case-roll.cjs --algo v1 \
--server-seed 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
--client-seed parity-client-seed --nonce 42
digest = db9a1723758599c77d272dcc30d8d05a878d3cdc63ae4c49e342bea4c522d181
first 4 B = 0xdb9a1723
u32 = 3684308771
roll = (3684308771 mod 100000) + 1 = 8772The same inputs under --algo v2 produce a different roll. This is expected: the two versions are separate frozen mappings.
Pass --ranges with the case's item windows and the script also resolves the item, and checks that the windows sum to exactly 100000.
For a battle, pass --battle-id and --slots to derive the client seed, and --round, --slot and --total-slots to derive the nonce, instead of computing them yourself.
For a jackpot battle the script derives the draws the ticket is built from: run it at nonce N and N + 1 under v1, or take the first 16 hexadecimal digits of the digest it prints for nonce N under v2. It does not compute the ticket or the winning team; those follow from section 3.1.
4. Scheme C: Crash rounds
Crash is not chain-based and uses no nonce. Each round has its own server seed, committed when the round opens and revealed when it settles. One HMAC decides the whole round:
digest = HMAC_SHA256(key = serverSeed, message = clientSeed)There is no cursor, no nonce, and no draw stream. What goes into clientSeed depends on the round's version, which is published on the round and taken by verify-crash-round.cjs as --algo.
Version 1 (identifier crash-v1), frozen:
clientSeed = "crash:" + gameIndexThis value was fully determined before the round existed, and the crash point was calculated when the round was seeded. Both inputs were therefore available to GrowBit while the server seed remained unfixed, so v1 rounds were subject to the limitation described in section 1.1. They remain verifiable exactly as they settled and the script reproduces them, but a successful v1 check does not establish how the seed was selected.
Version 2 (identifier crash-v2), current:
clientSeed = "crash:" + gameIndex + ":" + roundSalt
roundSalt = sha256( "<betId>:<entropy>" joined by "," , bets ordered by betId )Each entropy value is 32 hexadecimal characters produced by a participant's browser using the platform cryptographic random number generator, submitted with their bet during the betting phase and after the commitment was broadcast. Ordering is by betId, an identifier assigned at insertion, rather than by arrival time, so the salt is a function of the committed set of bets and any party holding those bets recomputes it identically. A round with no bets uses the SHA-256 of the empty string.
A v2 round is seeded with a commitment only. crashPointBips remains unset throughout the betting phase and is written in the same atomic update that moves the round to its running state. A round therefore cannot be observed running without an outcome, and an outcome cannot subsequently be recomputed differently.
Section 1.3 records the limitation that remains for Crash rounds.
4.1. Instant bust
With edgeBips = 400, mod = floor(10000 / edgeBips) = 25. The hex digest is reduced modulo 25 by folding it in 16-bit blocks, left to right, with any leading partial block folded in first:
val = 0
for each 4-hex-digit block of the digest, left to right:
val = ((val << 16) + parseInt(block, 16)) mod 25
divisible = (val == 0)If divisible, the round busts instantly at 1.00x. This happens with probability about 1/25 = 4%, and that branch is the house edge. The curve below carries no additional bias.
4.2. The curve
Otherwise:
h = parseInt(first 13 hex digits of the digest, 16) [0, 2^52)
e = 2^52
multiplier = ((100 * e - h) / (e - h)) / 100
crashPointBips = floor(10000 * multiplier)
floored at 10000, capped at 1e12 bipsWhy 13 hex digits. 13 x 4 = 52 bits, and 2^53 - 1 is the largest integer a JavaScript double represents exactly. A 52-bit h and the arithmetic above therefore stay entirely inside exact integer precision. No BigInt is required and there is no rounding behaviour to argue about. Fourteen hex digits would be 56 bits and would silently lose precision.
The 1e12 bips cap (1e8x) is duration-bounded, not economic. The multiplier is time-driven, so a higher ceiling only lengthens a round. The economic ceiling is the per-currency max-win, applied at settlement.
4.3. What timing evidence does and does not prove
The crash point is fixed when betting closes, before the round begins running, and nothing about the duration of the round can alter it. Timing therefore forms no part of the fairness proof. On v1 rounds the crash point was fixed earlier still, when the round was seeded.
Timing may nevertheless be checked for consistency, using GrowBit's own two timestamps rather than a local clock:
expected running duration (ms) = ln(crashPoint) / 0.00006The round loop evaluates every 150 ms, so the point at which the multiplier reaches the crash point is detected up to one interval late, together with scheduling variation and the settlement transaction. A settlement timestamp is therefore expected at or shortly after the calculated duration, and not materially before it. verify-crash-round.cjs prints this window when supplied with --running-started-at and --settled-at.
A multiplier displayed in a browser ahead of the settled value is a client-side rendering artefact and carries no implication for the round.
4.4. Verify a crash round yourself
node verify-crash-round.cjs --server-seed <hex> --game-index <int> \
--algo v1|v2 [--round-salt <hex>] \
[--bet <betId>:<entropy> ...] [--bets-json <file>] \
[--server-seed-hash <hex>] [--expect-multiplier <2.34>] \
[--running-started-at <iso>] [--settled-at <iso>]For a v2 round, supply the bets rather than the salt. The script rebuilds the salt and confirms that the round's published client seed is the one those bets produce. This is what makes an omitted contribution detectable: removing a single --bet causes the run to fail.
The contributions for any round are available without authentication, so a round may be checked by a person who was not connected while it was running:
curl -s https://growbit.net/api/v1/games/crash/rounds/<roundId>/contributionsEach entry gives betId and entropy. That betId is the identifier of the bet record, which is what the salt is computed over, and it is not the identifier returned to a player when they join a round. No account identity is included, because the salt does not depend on one. If truncated is true the list was capped and the salt cannot be rebuilt from it; that field exists so a short list is never mistaken for a tampered round.
Worked example, a settled v1 round at 1.97x:
node verify-crash-round.cjs --algo v1 \
--server-seed 9b1fad17ed8499aa9588fd947652388b278ffae5292d70427f044e76fe002474 \
--game-index 206353 \
--server-seed-hash 0e702afb690337e811dbce48919f06b1fa665106a013583e546b8e7833f7f26e \
--expect-multiplier 1.97 \
--running-started-at 2026-08-24T12:00:10.000Z \
--settled-at 2026-08-24T12:00:21.400Z
commitment : MATCH
digest = 7eb5843202db71c096c9549ce37ea6271e00eefe114f277294544ddc0d0024e4
instant-bust test: mod = 25, val = 16, divisible = false
h = parseInt("7eb5843202db7", 16) = 2229089152806327
e = 2^52 = 4503599627370496
multiplier = (100*e - h) / (e - h) / 100 = 1.9702299839709994
crashPointBips = 19702 (1.9702x)
timing (advisory, does NOT decide the outcome):
expected running duration = ln(1.9702) / 0.00006
= about 11302 ms
server-reported duration = 11400 ms
consistent: detected 98 ms after the curve reached the crash point.
VERIFIED. The settled crash point is exactly what this seed forced.Altering a single character of the server seed causes the run to fail. This is the negative case that demonstrates the check is meaningful.
5. Outcomes and configuration
Everything above determines an outcome: a roll, a board, a card order, or a crash point. An outcome is determined entirely by the seeds and re-derives from published values indefinitely.
A payout is that outcome applied to a stake and then subject to configuration. One input to a payout is not contained in the seeds: the maximum single-win limit. Where a win would exceed that limit, the amount credited is the limit rather than the value of the multiplier. A party recomputing the uncapped payout would therefore obtain a figure that differs from the amount paid, without any means of distinguishing a properly applied limit from an error.
Two measures make that difference checkable:
- Each bet records the economy configuration version in force at the moment it settled, together with the currency conversion rate fixed at the same moment.
- Each version of that configuration is retained in an append-only record and cannot be rewritten, so a historical version number continues to identify the values it identified at the time. A limit raised subsequently cannot be applied retrospectively to justify a payout made under the previous one.
The Provably Fair panel for a bet shows the ceiling that applied to it, reconstructed from the retained configuration version and the rate fixed on that bet, and not from the current configuration. The same values are available to the account holder from GET /api/v1/me/bets/:betId/fairness, which requires authentication because a bet belongs to one player. A bet whose version predates the record reports no ceiling, which is stated as unverifiable rather than populated with current values.
This measure does not prevent an operator with database access from deleting retained records. What it removes is the ability to alter historical values while every published surface continues to appear consistent, because an absent version is apparent as a gap.
Two parameters are, by contrast, not configurable at all:
- The house edge of each GrowBit Original is fixed within that game's outcome logic rather than held as a configuration value. No operational interface supplies an edge override to any game.
- Paytables and multipliers are functions of the parameters selected for that bet, such as target, risk level, mine count, or row count, and those parameters are recorded on the bet.
6. Third-party casino games
Third-party casino games, including slots, are not provably fair in the sense used on this page.
They are licensed titles delivered through an aggregator and generated within each provider's own random number generator, which may be independently tested and certified under that provider's compliance framework. GrowBit does not generate, influence, or reconstruct their outcomes. There is no GrowBit server seed, client seed, or nonce for such a round, and the Provably Fair panel accordingly displays no seed material for them. Their fairness rests on the provider's certification and is not verifiable using the scripts published here.
7. Seed rotation and historical verification
Rotating a seed chain retires the current generation, discloses its server seed, and immediately commits a replacement. The disclosed seed and the new commitment are returned together; the new server seed is not disclosed.
The replacement generation is created without a client seed. It holds a commitment only and will not issue a nonce until a client seed is supplied. This is the ordering described in section 1.2, and rotation is how a seed chain reaches it: the first seed generation of every seed chain is created with a client seed generated by GrowBit. The browser supplies a new value automatically on the next bet, and Your seeds allows one to be set deliberately instead.
Two properties are material to verification:
- Rotation is refused while a round remains open. Mines, Towers, Blackjack and War may hold an unfinished round whose outcome still depends on the committed seed. Disclosing that seed during the round would reveal the outcome in advance, so rotation is refused until the round concludes. This is enforced within the rotation transaction and not by the interface alone.
- Retired generations remain verifiable indefinitely. Every retired generation is written to an append-only disclosure record identified by its own commitment. A bet placed on generation 3 resolves to the seed of generation 3 regardless of how many rotations follow.
Each disclosure records the generation number, the server seed, its commitment, the client seed, the source of that client seed, and the highest nonce the generation reached. Those values are sufficient to reproduce every bet the generation produced and to establish which of the two orderings described in section 1.2 applied to it.
8. Statistical bias in the remaining games
Section 3 records a bias in the legacy case roll and the version boundary that replaced it. This section states the corresponding figure for every other mapping in use, so that no reader infers from section 3 that all remaining mappings are exactly uniform.
The seed chain games do not use a modulo mapping. They map a draw usingfloor(u32 / 2^32 * N), which is likewise not exactly uniform: whereverN does not divide 2^32, exactly 2^32 mod N of the outputs receive
one additional preimage out of 2^32.
mapping N extra-preimage relative
outputs bias
dice roll 10000 7296 2.328e-6
blackjack six-deck shoe 312 256 7.264e-8
blitz card face 52 48 1.211e-8
keno pool 40 16 9.313e-9
mines grid 25 21 5.821e-9
towers, 3 tiles 3 1 6.985e-10
towers, 2 or 4 tiles 2, 4 0 exactly uniform
case roll v1, for comparison 100000 67296 2.328e-5
(modulo, not multiply-and-floor)Two characteristics distinguish these figures from the case roll bias
recorded in section 3.
The distribution of the surplus differs. Under a modulo mapping the
additional preimages fall on the lowest 2^32 mod N outputs as a single
contiguous block. That is why the v1 case roll favoured whichever items
occupied the lower part of a case's contiguous item ranges. Under the
multiply-and-floor mapping the wider intervals are distributed across the
range rather than concentrated at one end, and a Fisher-Yates shuffle
distributes them further, so no contiguous set of outcomes is
systematically favoured.
The magnitudes differ materially. The largest figure above is the dice
roll at 2.33e-6, approximately four orders of magnitude below the house
edge. Distinguishing it from exact uniformity would require on the order
of a trillion observations.
These mappings are accordingly retained without a version change.
Introducing one would require freezing a v1 and adding a v2 across six
games, with a corresponding verification path and compatibility boundary
for each, in exchange for a difference that cannot be observed in play.
The figures above are verified by a test and therefore cannot diverge from
the implementation. Any new mapping onto a range wide enough to approach a
bias of 1e-4 will use rejection sampling from the outset.
9. Precision and rounding
Three rules apply throughout and account for most apparent discrepancies of a single unit:
- Multipliers are integers expressed in basis points.
10000basis points equals 1.00x. No multiplier is stored as a floating-point value. - Displayed values are rounded down, never up. A crash point of
19999basis points is displayed as1.99xrather than2.00x, because automatic cash-out settles on integer basis points and a20000target genuinely loses against a19999crash point. Rounding the display upwards would present a legitimate loss as a win. - The house edge is applied once. Mines and Towers multiply a fair cumulative product by
(1 - edge)at the end rather than at each step, so the theoretical return is a constant1 - edgeat every cash-out point regardless of how far a player progresses.
GrowBit is an independent platform and is not affiliated with, endorsed by, or sponsored by Ubisoft or Growtopia, or by any of their respective owners, publishers, developers, affiliates, officers, employees, licensors, or representatives. References to Growtopia, World Locks, Diamond Locks, Blue Gem Locks, DLS, or related names are used solely to identify supported virtual items, virtual currencies, or user-facing gameplay integrations.
Questions? Email legal@growbit.net