Skip to content

Game Ingestion

POST /api/games is the single write endpoint of the backend. A trusted writer submits one completed, source-agnostic game; the backend validates it against the Dice Chess engine and persists it. The endpoint is idempotent on the game id, so re-submitting the same game is safe.

The read API (browsing games and players) is public; ingestion is not. See the API Specification for the read endpoints.


The endpoint is protected by a bearer token. The backend reads its expected secret from the INGEST_TOKEN environment variable and compares it in constant time.

  • If INGEST_TOKEN is unset, every write is rejected (closed by default).
  • Requests must carry Authorization: Bearer <token>.
  • A missing or non-matching token yields 401 Unauthorized.
POST /api/games HTTP/1.1
Authorization: Bearer <INGEST_TOKEN>
Content-Type: application/json

Every submitted game is replayed move-by-move through the Dice Chess engine before anything is written. For each turn the backend:

  1. Parses the starting position (initial_fen, DFEN).
  2. Loads the rolled dice into the position’s dice pool.
  3. Asks the engine to enumerate all legal turn paths and confirms the submitted moves form one of them (this enforces every rule: legality, the Maximum Micro-moves Rule, dice consumption, king capture).

If any turn fails to validate, the whole request is rejected with 422 and nothing is persisted. The engine — not the writer — is the source of truth for legality.

Partial Terminal Turns: If a game ends mid-turn, the final turn may contain fewer micro-moves than the rolled dice allow. The backend gracefully handles this specifically for timeout, draw_agreement, and resign terminations: for the last turn only, if the played sequence is a strict prefix of a valid legal path, it is accepted and the partial turn is persisted. If such a partial sequence appears in any non-terminal turn, or under a different termination reason, the game is rejected.


Content-Type: application/json. All field names are snake_case on the wire.

FieldTypeNotes
idUUIDThe source’s game id and the primary key (idempotency). Required.
sourcestringOrigin label of the game. Required.
modestringclassic or x2. Required.
resultint?1 white win, -1 black win, 0 draw.
terminationstring?king_captured, timeout, resign, draw_agreement, double_declined, unknown.
started_atdatetime?ISO-8601 with offset.
time_initial_secint?Base clock.
time_increment_secint?Increment per turn.
initial_stake_amountint?Stake at the start.
final_stake_amountint?Stake at the end.
white_money_deltadecimal?Net change for White.
black_money_deltadecimal?Net change for Black.
stake_currencystring?Currency label of the stake.
white_playerPlayer?See below. Resolved to a players row by external_id.
black_playerPlayer?See below.
initial_fenstringStarting position in DFEN. Required.
turnsTurn[]Ordered list of turns. Required.
eventsEvent[]Non-move events (doubling, draw offers).

Player

FieldTypeNotes
external_idstringStable id from the source; upsert key. Required.
usernamestring?Display name.
player_typestring?human or bot.
ratingint?Rating at game time.

Turn

FieldTypeNotes
turn_numberint1-based. Required.
active_colorstringw or b. Required.
diceint[]Rolled dice; 1 pawn … 6 king. Required.
movesstring[]UCI micro-moves played this turn. Required.
thinking_time_msint?Time spent on the turn.
fen_afterstring?Position after the turn (informational).

Event

FieldTypeNotes
sequence_numberintOrder within the game. Required.
turn_numberint?Turn the event belongs to.
event_typestringEvent type enum. Required.
actor_colorstring?w or b.
clock_white_msint?White’s clock at the event.
clock_black_msint?Black’s clock at the event.
payloadobject?Free-form JSON details.
{
"id": "00000000-0000-0000-0000-0000000000b1",
"source": "import",
"mode": "classic",
"result": 1,
"termination": "king_captured",
"time_initial_sec": 300,
"time_increment_sec": 5,
"white_player": { "external_id": "ext-w", "username": "alice", "player_type": "human", "rating": 1500 },
"black_player": { "external_id": "ext-b", "username": "bob", "player_type": "bot", "rating": 1480 },
"initial_fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"turns": [
{ "turn_number": 1, "active_color": "w", "dice": [1, 2, 5], "moves": ["b1c3", "e2e4", "d1f3"], "thinking_time_ms": 3500 }
],
"events": []
}

StatusMeaning
201 CreatedThe game was new and has been persisted.
200 OKA game with this id already existed; nothing changed (idempotent re-ingest).
401 UnauthorizedMissing or invalid bearer token.
422 Unprocessable EntityThe game failed engine replay (e.g. an illegal move). Nothing is persisted.

Success bodies carry whether the request created the game:

{ "id": "00000000-0000-0000-0000-0000000000b1", "created": true }

Error bodies use the standard shape:

{ "detail": "Turn 1: illegal move sequence [a1a4]" }

The id is the primary key. The first successful submission inserts the game, its turns, and its events; any later submission of the same id is a no-op that returns 200. Writers can therefore retry safely without creating duplicates.