LOOTWIN

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. 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. 2

    Play

    The roll is bytes from HMAC-SHA256(serverSeed, "clientSeed:nonce:cursor"), rejection-sampled into a uniform ticket in 0…999,999. That ticket lands in exactly one item's range.

  3. 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.

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.