Skip to content

WebSocket API

pawnbook uses a single WebSocket endpoint at ws://localhost:3000/ws. All messages are JSON objects with a type field.

js
const ws = new WebSocket('ws://localhost:3000/ws')
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  console.log(msg.type, msg)
}

The server enforces a maximum payload of 4096 bytes per message.


Inbound messages (client → server)

All inbound messages are validated by Zod (src/schemas/messages.js). An invalid or malformed message returns an error response with error_code: 'invalid_message' — the connection is not closed.

new_game

Start a new game against an engine opponent.

FieldTypeRequiredDefaultDescription
type'new_game'yesMessage type
opponentIdstringyesOpponent ID from GET /api/opponents
color'white' | 'black' | 'random'yesYour colour
rankedbooleannotrueWhether to apply Elo changes on game end
timeControl{initialSec, incSec} | nullnonullFischer time control. initialSec must be > 0; incSec must be ≥ 0. null = untimed
coachEnabledbooleannotrueEnable the opening repertoire coach

Example

json
{
  "type": "new_game",
  "opponentId": "maia-1400",
  "color": "white",
  "ranked": true,
  "timeControl": { "initialSec": 600, "incSec": 5 },
  "coachEnabled": true
}

move

Submit your move. The move must be a legal move in the current position.

FieldTypeRequiredDescription
type'move'yesMessage type
ucistringyesMove in UCI format. Pattern: [a-h][1-8][a-h][1-8][qrbn]?. Promotion piece must be specified for pawn promotion (e.g., e7e8q).

Example

json
{ "type": "move", "uci": "e2e4" }

resign

Resign the current game.

json
{ "type": "resign" }

hint

Request a hint — highlights the piece that should move. Rate-limited to one request per 2 seconds. Only available in unranked games.

json
{ "type": "hint" }

resume

Resume a previously interrupted game.

FieldTypeRequiredDescription
type'resume'yesMessage type
gameIdstring (UUID)yesThe game ID to resume
json
{ "type": "resume", "gameId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

repertoire_choice

Respond to a repertoire_alert from the opening coach. This message uses a strict Zod schema — no additional fields are permitted.

FieldTypeRequiredDescription
type'repertoire_choice'yesMessage type
choice'correct' | 'keep'yescorrect plays the book move. keep plays your original move and opens a challenge.
json
{ "type": "repertoire_choice", "choice": "correct" }

Outbound messages (server → client)

game_started

Sent immediately after new_game or resume is processed.

FieldTypeDescription
type'game_started'
gameIdstringGame UUID
fenstringStarting FEN position
youPlay'white' | 'black'Your assigned colour
legalMovesarrayLegal moves in the starting position (UCI strings)
clockobject | null{ whiteMs, blackMs } if timed; null if untimed
resumedbooleantrue if this is a resumed game

move_accepted

Sent after the server validates your move.

FieldTypeDescription
type'move_accepted'
fenstringPosition after your move
sanstringYour move in SAN
legalMovesarrayEngine's legal replies (UCI strings)
checkbooleanWhether your move delivered check
clockUpdateobject | null{ whiteMs, blackMs } after your time was debited

engine_move

Sent after the engine plays its move.

FieldTypeDescription
type'engine_move'
ucistringEngine move in UCI format
sanstringEngine move in SAN
fenstringPosition after the engine's move
legalMovesarrayYour legal replies (UCI strings)
gameOver{ result: string, termination: string } | nullPresent when this move ends the game; same result/termination values as game_over
clockobject | null{ whiteMs, blackMs } after engine's time was debited

game_over

Sent twice per game. The first send (immediately on game end) has eloBefore: null, eloAfter: null. The second send (after analysis completes) populates the Elo fields.

FieldTypeDescription
type'game_over'
result'win' | 'loss' | 'draw'Game result from the player's perspective
terminationstringHow the game ended (see below)
eloBeforeinteger | nullElo before this game; null on first send
eloAfterinteger | nullElo after this game; null on first send; same as eloBefore for unranked games

Termination values

ValueMeaning
checkmateCheckmate
stalemateStalemate
threefoldThreefold repetition
insufficient_materialInsufficient material on both sides
fifty_moveFifty-move rule
timeoutFlag fall (time ran out)
resignationPlayer resigned

hint_result

Sent in response to a hint message.

FieldTypeDescription
type'hint_result'
pieceSquarestringSquare of the piece that should move (e.g., e2)

Coach messages

These messages are sent during play when the opening repertoire coach is active.

repertoire_alert

The coach has detected a deviation from your repertoire book. The player's move is not yet committed — the game is paused for REP_ALERT_TIMEOUT_SEC (60) seconds waiting for a repertoire_choice.

FieldTypeDescription
type'repertoire_alert'
kindstringDeviation kind (see below)
playerUcistringThe move you played
playerSanstringYour move in SAN
bookUcistring | nullThe book's canonical move in UCI; null for novelty
bookSanstring | nullThe book's canonical move in SAN; null for novelty
costWinPtsnumberEstimated win% cost of deviating from the book move

Alert kind values

KindDescription
order_slipYou played the right move but in the wrong order
lapseYou played a move you've learned before but is currently not canonical
refused_repeatYou played a move the gates have already refused as unsound
noveltyYou played a move not yet in the book at all

ranked_changed

Sent the first time the coach fires an alert in a game. The game is automatically switched to unranked.

json
{ "type": "ranked_changed", "ranked": false }

Analysis messages

Sent after the game ends, as the three-pass analysis pipeline runs.

analysis_progress

FieldTypeDescription
type'analysis_progress'
gameIdstringGame UUID
phaseintegerAnalysis pass (1, 2, or 3)
doneintegerPositions completed in current pass
totalintegerTotal positions in current pass
overallPctnumberWeighted overall progress (0–100)

Pass weights: pass 1 = 76%, pass 2 = 22%, pass 3 = 2%.

Error messages

json
{
  "type": "error",
  "error_code": "invalid_message",
  "message": "Human-readable description",
  "detail": "Optional technical detail"
}

Coach state machine

                  ┌──────────┐
                  │  playing  │
                  └─────┬─────┘
       book deviation   │  (coach active, within ply 30,
                        │   bootstrap complete, alert budget remaining)
                  ┌─────▼──────────────────┐
                  │    alert pending        │
                  │    (60 sec window)      │
                  └──┬──────────────────┬───┘
           correct   │                  │   keep
                  ┌──▼──────┐   ┌───────▼──────────────────┐
                  │ book    │   │ original move applied     │
                  │ move    │   │ challenge opened           │
                  │ played  │   │ game remains unranked      │
                  └─────────┘   └───────────────────────────┘

    timeout after 60 s → original move applied, no challenge opened

Coach guards — the alert fires only when all of:

  • coachEnabled was true when the game started
  • At least 20 canonical nodes are confirmed in the book (bootstrap guard)
  • Current ply ≤ 30 (REP_PLY_MAX)
  • Fewer than 3 alerts have fired in this game (REP_ALERTS_PER_GAME_MAX)

Released under the MIT License.