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

# Read-only endpoints

The JSON and WebSocket endpoints the Basement app reads, covering arenas, rounds, leaderboards, crew form and B3 reserves. Unversioned and may change.

These are the endpoints the Basement app itself reads to show arenas, rounds, boards and the live match. Anyone can call them with a plain GET from a server, without a key or a sign-in. They aren't a supported API.

**Unversioned**

None of these endpoints are versioned or covered by any stability promise. Fields, shapes and URLs can change or disappear with any release, without notice. Build so that a missing field doesn't break you.

## Endpoints

All paths are on `https://basement.fun`. Every response is JSON except the card images.

| Endpoint | What you get | Cached for |
| --- | --- | --- |
| `GET /api/time` | `{ now }`, the server clock in Unix milliseconds | Not cached |
| `GET /api/arenas` | `{ arenas }`: all six arenas, each with its current and next round, viewers, the last 10 results and live win chance | 1 second, on the server |
| `GET /api/arenas?slim=1` | The same, without locks, reveals and recent results | 1 second, on the server |
| `GET /api/arenas/:id` | One arena, same shape as an item above | Not cached |
| `GET /api/arenas/:id/rounds/:n` | `{ round, drops }` for any round. `round.reveal` appears once it has a result. | Not cached |
| `GET /api/leaderboard?period=day&currency=play` | The top 20 players for a period and currency | Revalidate with ETag |
| `GET /api/cast` | Form for all 15 crew members | 15 seconds |
| `GET /og/:arena.png` and `/og/:arena/:n.png` | Preview card images, see [links and embeds](/developers/links-and-embeds#preview-cards) | 30 seconds open, a week finished |
| `GET /api/b3` | Whether real B3 is on, withdrawal limits and the terms version | Not cached |
| `GET /api/b3/reserves` | The vault balance against what Basement owes players | 30 seconds |
| `GET /api/push/key` | The public key for Web Push subscriptions | 5 minutes |

`:id` and `:arena` are arena ids: `rush-hour`, `clash`, `knockout`, `broadside`, `skyball` or `turf-war`. `:n` is a round number.

## Arenas and rounds

`GET /api/arenas/knockout` during a live match, trimmed:

```json
{
  "arena": "knockout",
  "now": 1791812159803,
  "round": {
    "arena": "knockout",
    "n": 12366,
    "kernelVersion": "clash-4-ring",
    "phase": "live",
    "announcedAt": 1791812113263,
    "lineupAt": 1791812147413,
    "liveAt": 1791812155583,
    "maxEndAt": 1791812190583,
    "entrants": [
      { "index": 0, "cast": { "id": "toxic", "name": "Toxic", "skin": "hazmat" }, "color": "#ff4d6d" },
      { "index": 1, "cast": { "id": "orbit", "name": "Orbit", "skin": "astronaut" }, "color": "#3d9bff" }
    ],
    "multiplierBps": 19000,
    "commit": "4c640dd8b99ba822c04371fc77aaa0b01e3e7b6da8e30c06fe73e1060018a079"
  },
  "next": { "n": 12367, "phase": "upcoming", "commit": "ea30e496780183dba7fc23406c0cf06aa4a6383e68eae1985b1672f90cef3540" },
  "viewers": 1,
  "recent": [{ "n": 12365, "winner": 1, "winnerName": "Orbit", "color": "#3d9bff" }],
  "chance": [0.428, 0.572]
}
```

- Times are Unix milliseconds on the server clock. Use `GET /api/time` or the `now` field to correct for your own clock.
- `phase` runs `upcoming`, `lineup`, `live`, `result`. A round takes picks from `announcedAt` until `liveAt`, so the round taking picks is `next` while a match is live or showing its result, and `round` during the lineup. `next` is `null` during the lineup.
- `maxEndAt` is the latest the match can end. The real end is `result.endedAt` once there's a result.
- `entrants` are the sides you can pick, by `index`. In Turf War each entrant is a squad, with its crew in `members`.
- `multiplierBps` is the fixed payout in basis points, so `19000` pays 1.9x, stake included.
- `result.winner` is an entrant index, or `null` when the round was void and every pick refunded. `placings` lists every entrant, best first.
- `result.voidReason` appears only on a void round and says why: `kernel-changed` (a game update landed while the round was locked or live), `ledger-unreachable` (the round couldn't be locked in the ledger), `timeout` (time ran out with nobody ahead) or `other` (anything else, such as a game error).
- `chance` is the live win chance per entrant from what's visible on the stage. It appears only while a match is live.
- `viewers` counts open connections that have this arena on screen right now.
- `unsettled` appears only while a finished round is still waiting for the ledger to settle it: `{ count, n, endedAt }`, with how many rounds are waiting and the oldest one's number and end time. It's normally absent.

`/api/arenas/:id/rounds/:n` gives one round in the same shape plus its `drops`, with `reveal` once it's over. A finished round never changes, so cache it for good. [Verify a round](/developers/verify-a-round) walks through a full response and the Drop fields.

## Leaderboards and crew form

`GET /api/leaderboard` takes two parameters:

- `period`: `day` (since 00:00 UTC), `week` (since Monday 00:00 UTC) or `streak` (today's longest runs of rounds that paid back more than was staked)
- `currency`: `play` or `b3`

```json
{
  "period": "week",
  "currency": "play",
  "rows": [{ "rank": 1, "handle": "RowdyCorgi37", "net": 790 }],
  "me": null
}
```

That's trimmed, with the handle replaced. `net` is payout minus stake over settled picks in the period, and `km`, when present, counts the player's Kingmaker moments. The `streak` board has `best` in place of `net`. `me` is always `null` without a session. Rows can include players at zero or below; the app shows only rows above zero. Boards recompute at most every 30 seconds. Send the `ETag` back as `If-None-Match` and an unchanged board costs you a 304. More on how boards work in [leaderboards](/social/leaderboards).

`GET /api/cast` gives `{ cast: [...] }`, one entry per crew member, from settled rounds with voids left out. One entry, trimmed:

```json
{
  "id": "valk",
  "wins": 4125,
  "losses": 8339,
  "par": 4114.3,
  "streak": 1,
  "best": "clash",
  "arenas": { "knockout": [681, 706], "skyball": [515, 512] },
  "recent": [{ "arena": "knockout", "n": 10556, "place": 1, "at": 1791745169119 }]
}
```

`par` is how many wins an even share would have given, `arenas` holds wins and losses per arena, and `recent` has the last 8 results, newest first. In Turf War `place` is the squad's place. The crew are covered in [the crew](/games/the-crew).

## Real B3 status and reserves

`GET /api/b3` says whether real B3 is on (`enabled`), whether withdrawals are open (`withdrawals`), the current `termsVersion`, and the withdrawal limits `minWithdraw`, `dailyWithdrawLimit` and `reverifyAbove`, all in whole B3. Its account fields are empty without a session. `allowedHere` describes where the request came from, so a server's answer says nothing about any player's region.

`GET /api/b3/reserves` compares the B3 in the vault with what Basement owes players. Every field is explained in [where your B3 is held](/b3/vault-and-reserves#check-the-reserves).

## Live socket

`wss://basement.fun/ws/:arena` streams one arena as JSON text messages, each with a type in `t`. The first message is always `snap`, with the current and next round, recent frames and events, Drops, recent results, viewers and recent activity. After that:

| `t` | When it arrives |
| --- | --- |
| `round` | A round changes phase. Carries `round` and `next`. |
| `frames` | Match frames, about 10 a second in 5 batches a second while live. Outside a match, one held pose about once a second. |
| `events` | Moments in the match, like a hit, a goal, a knockout or the finish |
| `drop` | A Drop was accepted. It lands 600 ms later. |
| `pools` | The crowd split of a round taking picks changed |
| `activity` | New picks, wins and Drops across all arenas, as the app's ticker shows them |
| `viewers` | The arena's viewer count changed |
| `react` | Reaction counts from people watching |
| `kingmaker` | A Drop decided the round, named at the finish |

The frame format differs per game and isn't documented. A `frames` batch can repeat ticks you already have from `snap`, so skip ticks you hold. Your connection counts as a viewer and gets frames until it sends `{"t":"focus","on":false}`. To check the connection, send `{"t":"ping","id":1}` and you get a `pong` with the same `id`. Don't send reactions; they're for people watching.

An unknown arena answers 404, a request without a WebSocket upgrade 426, and an arena that's still starting 503 with `retry-after: 1`.

## Errors and rate limits

API errors are JSON with an HTTP status:

```json
{ "error": "bad_request", "message": "Unknown round." }
```

| `error` | Status | From a read, it means |
| --- | --- | --- |
| `bad_request` | 400 | Unknown arena, unknown round or unknown path |
| `rate_limited` | 429 | Too many requests from your network. Wait for `retry-after`. |
| `unavailable` | 503 | Something failed on the server. Try again shortly. |

The other codes come from app actions, never from the reads on this page: `unauthorized` (401), `insufficient_funds` (402), `betting_closed`, `round_not_open`, `drops_closed` and `limit_reached` (409), and `geo_blocked` (451).

An unknown round is a 400 from the API and a 404 from `/og/`. Card and socket errors are plain text.

Limits count per network, meaning one IPv4 address or one IPv6 /64, over 10-second windows:

| What | Limit per 10 seconds | `retry-after` |
| --- | --- | --- |
| Reads: `/api/*`, card images, arena and replay pages | 300 | 2 seconds |
| Leaderboard reads, which also count as reads | 20 | 5 seconds |
| New socket connections | 40 | 2 seconds |

Poll a running round every few seconds at most, or hold one socket per arena instead. Finished rounds and finished cards never change, so fetch them once.

## Next steps

**[Verify a round](/developers/verify-a-round)**

Check a round's SHA-256 lock with your own code.
  **[Links and embeds](/developers/links-and-embeds)**

URL formats, preview cards and Farcaster embeds.