> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basement.fun/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/games/provably-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:

```text
arena: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

```bash
curl 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`.

```bash
printf '%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 100
skyball:100:skyball-3:1426668468:ff6ae4bd664563b967c173074f57f5a2
640c01c179e8b0a14105f05a27d757e163b267507eb1434e86309070282f612f
Matches 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](/social/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".

## Next steps

**[Read-only endpoints](/developers/read-endpoints)**

Arenas, rounds and the live socket.
  **[How rounds stay fair](/games/provably-fair)**

The same check inside the app.