FAIRNESS
Every pull is provably fair
Before you bet, the server commits to a secret server seedby publishing its SHA-256 hash, and it can't change the outcome after the fact. Your own client seed and a per-round nonce mix in, so neither side alone decides the result. When you rotate your seeds the server reveals the seed it committed to, and you can recompute every roll it produced. No trust required, just math you can run.
- 1
Commit
Ahead of your bet the server hashes its server seed and shows you
serverSeedHash = sha256(serverSeed). The seed stays secret; the hash is the promise it can't take back. - 2
Play
The roll is bytes from
HMAC-SHA256(serverSeed, "clientSeed:nonce:cursor"), rejection-sampled into a uniform ticket in0…999,999. That ticket lands in exactly one item's range. - 3
Verify
Rotate your seeds and the server reveals the original
serverSeed. Confirm it hashes to the commit you were shown, then recompute the ticket yourself and check it matches your item.
The actual verification code
This is the core of @edgeplays/gleipnir-sdk, the same fairness engine the server runs, published to npm so you can install it and check any round for yourself.
// Lootwin's fairness is the exact code the server runs, shipped as an npm
// package you can audit and run yourself:
// npm i @edgeplays/gleipnir-sdk
// import { ByteStream, nextInt, commit } from "@edgeplays/gleipnir-sdk/fair";
// The recipe below is that library's core, verbatim — HMAC-SHA256, nothing else.
import { hmac } from "@noble/hashes/hmac";
import { sha256 } from "@noble/hashes/sha256";
// A deterministic byte stream KEYED BY THE SERVER SEED. Each 32-byte block is
// HMAC-SHA256(serverSeed, "clientSeed:nonce:cursor"); ":rep" is appended when a
// single draw needs more than one block. Same inputs -> same bytes, forever.
function block(serverSeed, clientSeed, nonce, cursor, rep) {
const msg = rep === 0
? `${clientSeed}:${nonce}:${cursor}`
: `${clientSeed}:${nonce}:${cursor}:${rep}`;
return hmac(sha256, serverSeed, msg); // 32 bytes
}
// Your winning ticket: a uniform int in [0, 999999] by rejection-sampling four
// bytes at a time (no modulo bias). It maps to exactly one item's ticket range.
function ticket(serverSeed, clientSeed, nonce) {
const span = 1_000_000, MAX = 0x1_0000_0000, cut = MAX - (MAX % span);
for (let rep = 0, cursor = 0; ; rep++) {
const b = block(serverSeed, clientSeed, nonce, cursor, rep);
for (let i = 0; i + 4 <= b.length; i += 4) {
const v = b[i] * 2 ** 24 + b[i + 1] * 2 ** 16 + b[i + 2] * 2 ** 8 + b[i + 3];
if (v < cut) return v % span;
}
}
}
// Before the bet you were shown serverSeedHash = commit(serverSeed) = sha256(serverSeed).
// After you rotate seeds the server reveals serverSeed: check the hash matches,
// then recompute ticket() and confirm it lands on the item you actually got.Odds are published, to the ticket
Every box page lists its exact per-item odds in parts-per-million (1,000,000 tickets per box), and each item's detail shows the precise ticket range it owns. The winning ticket from step 2 falls into exactly one of those ranges, nothing is hidden or rounded.
Per-open verification (paste a round's seeds and recompute it in the browser) ships on your account.
Case battles, and ties
A case battle draws every seat's boxes from the same committed seed chain, so the whole board verifies exactly like a single open. When two or more seats finish on the same winning total, the tie is broken by one extra provably-fair draw, with no earliest-seat rule and no house call. Sudden death: the drawn seat takes the entire pot.
That draw reads the same revealed server seed at a reserved nonce that can never collide with a round draw: nonce = baseNonce + roundCount × seatCount, the first index past the last box open. The winner is tied[roll % T] over the tied seats in seat order. Every settled battle shows the tied seats, the roll, and the resulting winner on its own page, and you can recompute all of it from the revealed seed. The same math, one more time.