The verifier, in the open.

This is the exact math your browser runs to re-check every box, Evolve, and Mystery open. It uses only public inputs and standard Web Crypto — no PackLab server, no trust in us. Read it, copy it, run it against the test vectors or any completed box.

Boxes

// packages/fairness/src/verify.ts — verifyLot(commitments, reveal)
// Re-derives a whole box in your browser. No PackLab server involved.

// 1. The published seed matches its pre-committed fingerprint
sha256hex(reveal.serverSeed) === commitments.seedCommitment

// 2. The published card list + nonce match the box fingerprint
//    (manifest v2 binds beaconRef, so a swapped round breaks this hash)
sha256hex(canonicalJson(reveal.manifest)) === commitments.manifestCommitment
reveal.manifest.beaconRef === commitments.beaconRef

// 3. The beacon value is the real drand round it claims to be
(await drand.get(commitments.beaconRef)).randomness === reveal.beaconValue

// 4. Reproduce every pack, byte for byte
finalSeed = HMAC_SHA256(hexDecode(serverSeed), utf8(beaconValue))
order     = shuffledOrder(finalSeed, packCount)   // Fisher-Yates
// pack at draw position p = canonical pack order[p]; its cards are that
// pack's slots in manifest order → compare to what was minted.

Evolve & Mystery Pack

// src/lib/evolveVerify.ts + src/lib/mysteryVerify.ts
// The SAME roll for Evolve and Mystery — recomputed from public inputs only.

const hashOk = sha256hex(serverSeed) === serverSeedHash;

const sig  = HMAC_SHA256(hexDecode(serverSeed), utf8(drandRandomness));
const roll = parseInt(hex(sig).slice(0, 13), 16) / 2 ** 52;   // 52 bits → [0,1)
const rollOk = Math.abs(roll - recordedRoll) < 1e-9;

// Evolve: you won iff roll < winProbBps / 10000
// Mystery: the winning set is a cumulative bucket-select of roll over the
//          committed odds table (whose SHA-256 you also re-check):
const tableOk = sha256hex(JSON.stringify(weightTable)) === weightTableHash;
let acc = 0, target = roll * sum(weightTable.map(w => w.weight));
for (const w of weightTable) { acc += w.weight; if (target < acc) return w.setId; }

The shipped source

The snippets above mirror these files in the public repository. The full, runnable verifier is a few hundred lines total.

  • packages/fairness/src/verify.tsverifyLot — the full box re-derivation
  • packages/fairness/src/shuffle.tsshuffledOrder — the Fisher-Yates permutation
  • packages/fairness/src/beacon.tsdrand resolution + round timing
  • apps/web/src/lib/evolveVerify.tsverifyEvolveInBrowser — the Evolve roll
  • apps/web/src/lib/mysteryVerify.tsverifyMysteryInBrowser — the Mystery roll + bucket-select
Full method: technical spec · inspect a real box · how it works