Verify a round yourself
Fetch a finished round, rebuild its SHA-256 lock from the revealed seed and salt, and check it against the lock published before picks opened.
Every Basement round publishes a SHA-256 lock before it takes a single pick, and reveals the seed and salt behind it when it ends. You can check any finished round with a one-line shell command or a few lines of JavaScript. This page shows both, on a real round. The in-app version of the same check is in how rounds stay fair.
What's locked, and when
When a round is announced, the server draws a random 32-bit seed and a random salt of 32 hex characters. It publishes the round's lock right away, before the round accepts a pick. The lock is the SHA-256 of this text:
textarena:round:version:seed:salt
| Part | Field in the API | Example |
|---|---|---|
| arena | round.arena | skyball |
| round | round.n | 100 |
| version | round.kernelVersion | skyball-3 |
| seed | round.reveal.seed | 1426668468 |
| salt | round.reveal.salt | ff6ae4bd664563b967c173074f57f5a2 |
The example is from Skyball round 100, which ran on skyball-3, the one-on-one version. Skyball rounds today show skyball-4. The version in the hashed text is always the one the round itself ran on.
The lock is round.commit, as 64 lowercase hex characters. A round is announced the moment the one before it goes live, and takes picks until it goes live itself. The seed and salt stay private until the round has a result. Then they appear in round.reveal.
Arena ids are rush-hour, clash, knockout (Neon Knockout), broadside, skyball and turf-war.
Check a finished round
Fetch the round
GET https://basement.fun/api/arenas/<arena>/rounds/<n>. The response is { round, drops }. If round.reveal is missing, the round hasn't finished yet.
Build the text
Join arena, round number, version, seed and salt with colons, exactly as the API gives them. Don't add spaces or a trailing newline.
Hash it
Take the SHA-256 of that text as UTF-8 and write it as lowercase hex.
Compare
The hash should equal round.commit. If you saved the lock before the round played, it should equal that too.
Worked example: Skyball round 100
bashcurl https://basement.fun/api/arenas/skyball/rounds/100
The response, with the entrants and timestamps trimmed:
json{ "round": { "arena": "skyball", "n": 100, "kernelVersion": "skyball-3", "phase": "result", "commit": "640c01c179e8b0a14105f05a27d757e163b267507eb1434e86309070282f612f", "reveal": { "seed": 1426668468, "salt": "ff6ae4bd664563b967c173074f57f5a2" }, "result": { "placings": [1, 0], "winner": 1, "endTick": 640 } }, "drops": []}
Hash it in a shell. Use printf '%s', because echo adds a newline and changes the hash. On Linux, sha256sum works in place of shasum -a 256.
bashprintf '%s' "skyball:100:skyball-3:1426668468:ff6ae4bd664563b967c173074f57f5a2" | shasum -a 256# 640c01c179e8b0a14105f05a27d757e163b267507eb1434e86309070282f612f -
That's the same value as commit. Or run the whole check in Node 18 or later, which has fetch and WebCrypto built in:
js// verify-round.mjs// Usage: node verify-round.mjs <arena> <round> [lock you saved before it played]const [arena, n, saved] = process.argv.slice(2);const res = await fetch(`https://basement.fun/api/arenas/${arena}/rounds/${n}`);if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);const { round } = await res.json();if (!round.reveal) throw new Error("No seed yet. The round hasn't finished.");const text = `${round.arena}:${round.n}:${round.kernelVersion}:${round.reveal.seed}:${round.reveal.salt}`;const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));const hash = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");console.log(text);console.log(hash);console.log(hash === round.commit ? "Matches the published lock" : `Does not match the published lock ${round.commit}`);if (saved) console.log(hash === saved ? "Matches the lock you saved" : "Does not match the lock you saved");
text$ node verify-round.mjs skyball 100skyball:100:skyball-3:1426668468:ff6ae4bd664563b967c173074f57f5a2640c01c179e8b0a14105f05a27d757e163b267507eb1434e86309070282f612fMatches the published lock
The hashing lines work unchanged in a browser. The fetch doesn't work from another site's page, because the API sends no CORS headers.
Save the lock before the round plays
Matching commit in the same response that reveals the seed shows the numbers agree. It doesn't show the lock came first. For that, record the lock while the round is still taking picks, then check it after the round ends.
The round taking picks is the one whose phase is upcoming or lineup. In GET /api/arenas/<arena> it's next while a match is live or showing its result, and round during the lineup.
js// save-lock.mjs// Usage: node save-lock.mjs <arena>const arena = process.argv[2];const summary = await (await fetch(`https://basement.fun/api/arenas/${arena}`)).json();const open = [summary.round, summary.next].find((r) => r && (r.phase === "upcoming" || r.phase === "lineup"));console.log(open.arena, open.n, open.commit);
Once that round has a result, usually within two minutes, pass the saved lock as the third argument to verify-round.mjs.
If a game update ships while a round is still taking picks, that round gets a fresh seed, salt and lock under the new version. Picks on it stand. A lock you saved before the update won't match, and the round's kernelVersion shows the new version. A round that had already closed for picks or gone live when the update landed is voided instead, and every pick on it is refunded.
Re-running the match
The hash shows the seed was fixed before picks opened. It doesn't show that this seed produces the result the round reported. Checking that means running the game code, and the game code isn't public, so outside the app you can check the hash but you can't re-run the match yourself.
The app can. The replay at https://basement.fun/r/<arena>/<n> loads the same game code the live round ran, feeds it the revealed seed and the round's Drops, and plays the whole match in your browser. If its winner or finish tick differs from the round's, it refuses to play and says "This round can't be replayed on this version". More in replays.
Drops are published, not locked
drops in the round response lists every Drop the server accepted in that round, in order:
json{ "seq": 1, "tick": 212, "kind": "chaos", "target": 0, "by": "RowdyCorgi37" }
tick is when it lands, counted at 20 ticks a second from GO. kind is speed, shield or chaos, target is an index into round.entrants, and by is the handle of the player who dropped it. The replay uses them as input.
The server draws a Drop's kind and target from its own randomness at the moment someone Drops. They don't come from the seed and aren't in the lock, so the hash can't vouch for them. What you can check is that they're published with the round and that the replay, fed those Drops, gives the reported result.
Older rounds after an update
Each round records the game version it ran on in kernelVersion, and the version is part of the hashed text. The hash check keeps working for every round. Replays don't. Once a game's version changes, replays of its older rounds stop with "This round ran on an earlier version of the game".