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.

EndpointWhat you getCached for
GET /api/time{ now }, the server clock in Unix millisecondsNot cached
GET /api/arenas{ arenas }: all six arenas, each with its current and next round, viewers, the last 10 results and live win chance1 second, on the server
GET /api/arenas?slim=1The same, without locks, reveals and recent results1 second, on the server
GET /api/arenas/:idOne arena, same shape as an item aboveNot 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=playThe top 20 players for a period and currencyRevalidate with ETag
GET /api/castForm for all 15 crew members15 seconds
GET /og/:arena.png and /og/:arena/:n.pngPreview card images, see links and embeds30 seconds open, a week finished
GET /api/b3Whether real B3 is on, withdrawal limits and the terms versionNot cached
GET /api/b3/reservesThe vault balance against what Basement owes players30 seconds
GET /api/push/keyThe public key for Web Push subscriptions5 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 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.

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.

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.

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:

tWhen it arrives
roundA round changes phase. Carries round and next.
framesMatch frames, about 10 a second in 5 batches a second while live. Outside a match, one held pose about once a second.
eventsMoments in the match, like a hit, a goal, a knockout or the finish
dropA Drop was accepted. It lands 600 ms later.
poolsThe crowd split of a round taking picks changed
activityNew picks, wins and Drops across all arenas, as the app's ticker shows them
viewersThe arena's viewer count changed
reactReaction counts from people watching
kingmakerA 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." }
errorStatusFrom a read, it means
bad_request400Unknown arena, unknown round or unknown path
rate_limited429Too many requests from your network. Wait for retry-after.
unavailable503Something 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:

WhatLimit per 10 secondsretry-after
Reads: /api/*, card images, arena and replay pages3002 seconds
Leaderboard reads, which also count as reads205 seconds
New socket connections402 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

Check a round's SHA-256 lock with your own code.

Learn More
Links and embeds

URL formats, preview cards and Farcaster embeds.

Learn More
Ask a question... ⌘I