Skip to content

POST /api/tables

Since: 2026-04-19

The single authoritative table-creation endpoint. Browser-facing; evm-bank validates the settings blob via Zod, writes the tables row with status: 'active', and returns. Replaces the prior lazy-create path that used to live inside POST /api/deposit and inside game-server's upsertRoom helper.

Request

POST /api/tables
Authorization: Bearer <JWT>
Content-Type: application/json

{
  "tableId": "ABC123",
  "gameType": "poker",
  "settings": {
    "smallBlind": 1,
    "bigBlind": 2,
    "minBuyIn": 50,
    "maxBuyIn": 200,
    "maxPlayers": 6,
    "rakePercent": 5,
    "rakeCap": 4,
    "rakeMinPot": 0,
    "rakeRequiresFlop": true,
    "turnTimeSec": 30,
    "turnTimerEnabled": true
  }
}

Auth: JWT bearer. Verified against the same JWKS game-server uses. Authorization: Bearer <token>; invalid / missing token → 401.

Body fields:

FieldTypeNotes
tableIdstringThe short-string identifier. The bytes32 hash for on-chain calls is computed inline at contract-call time (keccak256(toBytes(tableId))) and is never persisted.
gameTypestring"poker" or "arena". The Zod dispatcher in @numero/types/table-settings routes to the right schema.
settingsobjectPer-game-type settings. Validated by pokerTableSettingsSchema or arenaTableSettingsSchema. Extra fields are stripped; missing required fields reject.

Settings schemas

Poker (gameType: "poker")

Defined in packages/types/src/table-settings.ts as pokerTableSettingsSchema.

FieldTypeRange / constraint
smallBlindint>= 0
bigBlindint> 0, > smallBlind
ante?int>= 0, optional
minBuyInint> 0
maxBuyInint> 0, >= minBuyIn
maxPlayersint2 <= n <= 9
rakePercentnumber0 <= n <= 100
rakeCapint>= 0
rakeMinPotint>= 0
rakeRequiresFlopbooleanindustry "no flop, no drop"
turnTimeSecint> 0
turnTimerEnabledbooleanfalse disables auto-fold
hostedOnly?booleantrue = only hosted agents can sit
showHands?booleantrue = all hole cards visible to spectators (requires hostedOnly)
deckSeed?stringtest mode only (ENABLE_TEST_AUTH) — deterministic deck

Arena (gameType: "arena")

Defined as arenaTableSettingsSchema.

FieldTypeRange / constraint
minBuyInint> 0
maxBuyInint> 0, >= minBuyIn
maxPlayersint2 <= n <= 8
rakePercentnumber0 <= n <= 100
rakeCapint>= 0
turnTimeSecint> 0
turnTimerEnabledboolean
scoreTargetint> 0

Known gap: arena's Zod schema does not currently declare deckSeed but arena test scenarios rely on it.

Responses

200 — Created

json
{
  "tableId": "ABC123",
  "gameType": "poker",
  "status": "active"
}

The tables row is now durably stored with settings JSONB = the parsed blob. Subsequent POST /api/deposit and WebSocket-upgrade calls for this tableId succeed.

400 — Validation failed

json
{
  "error": "validation_failed",
  "issues": [
    { "path": ["maxPlayers"], "message": "Number must be less than or equal to 9", "code": "too_big" },
    { "path": ["maxBuyIn"], "message": "maxBuyIn must be at least minBuyIn", "code": "custom" }
  ]
}

Also 400 missing_table_id or 400 missing_game_type when the top-level fields are absent.

409 — Conflict (replay)

json
{ "error": "table_exists" }

Returned on a duplicate tableId. The insert uses ON CONFLICT DO NOTHING + .returning() — if the row already exists, the second caller sees zero returned rows and the endpoint responds with 409.

401 / 500

Standard auth + internal-error responses.

OTel

Each call emits an admin.tables.create span with attributes:

AttributeExampleMeaning
table.id"ABC123"the tableId from the body
table.game_type"poker"
settings.max_players6from the parsed settings
auth.user_idUUID from the JWT sub
result"created" | "validation_failed" | "replay"terminal outcome

Why this exists

Previously, tables were created lazily by the first POST /api/deposit that referenced them. That path defaulted missing settings silently and created rows with status: 'pending' that never got validated. Multiple classes of bug traced back to this: mismatched settings between UI state and DB state, 9-seat heads-up tables, silent default-fall-through on deckSeed, and test scenarios that looked correct on paper but ran with server-default settings.

POST /api/tables inverts this: the UI (or admin tooling) explicitly asks for a table, validation runs at request time, and every tables row is known to have been through the Zod pipeline. Downstream code (the WS upgrade's TableManager.loadOrCreate, game-server room constructors) reads the validated JSONB directly without re-parsing.