Skip to content

REST API

All endpoints are served under /api/. The server listens on http://localhost:3000 by default.


GET /api/state

Lightweight dashboard state and Docker healthcheck endpoint.

Response

FieldTypeDescription
status'ok'Healthcheck indicator
elointegerCurrent player Elo rating
eloDeltaintegerElo change over last 10 ranked games
eloHistoryarrayHistorical Elo data points for charting
dueCountintegerNumber of FSRS cards currently due
showStreakbooleanWhether the streak display is enabled in settings
streakintegerCurrent day-streak (days with any activity)
bestStreakintegerLongest ever consecutive day-streak
todayDrillsobject{attempted, correct} first-attempt non-practice reviews today (04:00 boundary)
activityHistoryarray[{day, games, reviews}] last 30 days of activity
gamesPlayedintegerTotal lifetime games played
recentGamesarrayUp to 8 most recent games
suggestedOpponentstring | nullOpponent ID suggested based on current Elo
inProgressGameIdstring | nullUUID of any unfinished game
inProgressOpponentIdstring | nullOpponent ID of the in-progress game

Each recentGames item:

FieldTypeDescription
idstringGame UUID
opponentIdstringOpponent identifier
result'win' | 'loss' | 'draw'Game result from the player's perspective
accuracynumberPlayer accuracy (1–100)
puzzleCountintegerDrillable puzzles extracted
playedAtstringISO 8601 timestamp

Example response

json
{
  "status": "ok",
  "elo": 1432,
  "eloDelta": 12,
  "eloHistory": [1200, 1240, 1315, 1390, 1432],
  "dueCount": 7,
  "showStreak": true,
  "streak": 5,
  "gamesPlayed": 34,
  "recentGames": [
    {
      "id": "a1b2c3d4-...",
      "opponentId": "maia-1400",
      "result": "win",
      "accuracy": 74.3,
      "puzzleCount": 3,
      "playedAt": "2026-08-31T09:12:00.000Z"
    }
  ],
  "suggestedOpponent": "maia-1500",
  "inProgressGameId": null,
  "inProgressOpponentId": null,
  "bestStreak": 12,
  "todayDrills": { "attempted": 10, "correct": 8 },
  "activityHistory": [{ "day": "2026-08-30", "games": 1, "reviews": 10 }]
}

GET /api/opponents

Returns all available engine opponents — filtered to only those whose binary and weight files exist on disk at startup.

Response

json
{
  "opponents": [
    {
      "id": "maia-1100",
      "name": "Maia 1100",
      "elo": 1100,
      "type": "maia3",
      "available": true
    }
  ]
}

Each opponent object includes id, name, elo (target strength), type (maia3, maia, or stockfish), and available (boolean).


GET /api/games

Returns the 50 most recent games, newest first.

Response

FieldTypeDescription
idstringGame UUID
opponentIdstringOpponent identifier
resultstring'win', 'loss', or 'draw' (player's perspective)
accuracynumber | nullPlayer accuracy 1–100; null if analysis pending
strengthElointeger | nullPlayer strength estimate for this game
opponentStrengthElointeger | nullEngine strength estimate for this game
playedAtstringISO 8601 timestamp
statusstring'finished', 'analysing', 'failed', or 'in_progress'
puzzleCountintegerNumber of drillable puzzles created

Example response

json
{
  "games": [
    {
      "id": "a1b2c3d4-e5f6-...",
      "opponentId": "maia-1400",
      "result": "win",
      "accuracy": 74.3,
      "strengthElo": 1521,
      "opponentStrengthElo": 1388,
      "playedAt": "2026-08-31T09:12:00.000Z",
      "status": "finished",
      "puzzleCount": 3
    }
  ]
}

GET /api/games/:id/review

Full post-game review data for a single game.

Response

FieldTypeDescription
idstringGame UUID
analysisStatestring'done', 'pending', 'running', or 'failed'
analysisErrorstring | nullError message if analysis failed
opponentIdstringOpponent identifier
playerColor'white' | 'black'Your colour in this game
resultstringGame result
terminationstringHow the game ended (see termination values)
accuracynumberPlayer accuracy (1–100)
opponentAccuracynumberOpponent accuracy (1–100)
strengthElointeger | nullPlayer strength estimate (Elo)
opponentStrengthElointeger | nullOpponent strength estimate (Elo)
strengthSeinteger | nullStandard error of player strength estimate
opponentStrengthSeinteger | nullStandard error of opponent strength estimate
rollingStrengthinteger | nullRolling inverse-variance aggregate of last 10 games
rollingSeinteger | nullStandard error of rolling aggregate
eloBeforeintegerElo before this game
eloAfterintegerElo after this game (same if unranked)
movesarrayPer-move analysis data
mistakesarrayGraded mistake positions
motifSummaryarray[{tag, count, explanation}] — recurring error patterns, sorted by count desc
puzzleCountintegerDrillable puzzles extracted

Each moves[] item:

FieldTypeDescription
plyintegerHalf-move number (1-based)
sanstringMove in Standard Algebraic Notation
ucistringMove in UCI format
fenstringPosition after the move
mover'white' | 'black'Side that moved
winPctnumberWin% after this move (White's POV)
classificationstringblunder, mistake, inaccuracy, ok, good, great, or best
cpLossnumberCentipawn loss relative to best move

Each mistakes[] item:

FieldTypeDescription
classificationstringblunder, mistake, or inaccuracy
moveSanstringThe move played
winLossnumberWin% points lost
tagsarraycommon_trap, was_timed, engine_only
bestMoveSanstringStockfish's best move
findabilitynumberMaia probability of finding the best move (0–1)
maiaNearestModelstringMaia model used for findability probe
engineOnlybooleanTrue if below findability gate (not drillable)
sourcePlyintegerPly number of the mistake
motifTagstring | nullError pattern label (e.g. fork, back_rank, pinned_piece)
motifExplanationstring | nullOne-sentence description of the motif

GET /api/games/:id/quiz

Ordered puzzle positions for the post-game quiz screen. Excludes puzzles tagged engine_only.

Response

FieldTypeDescription
positionsarrayQuiz puzzle positions
opponentIdstringOpponent from the game

Each positions[] item:

FieldTypeDescription
puzzleIdstringPuzzle UUID
fenstringPosition to solve from
sideToMove'white' | 'black'Your side
playedMoveSanstringThe move you actually played (the mistake)
bestMoveUcistringCorrect move in UCI format
bestMoveSanstringCorrect move in SAN
pvstringPrincipal variation (space-separated UCI moves)
followupUcistring | nullRequired followup move, if any
acceptedMovesJsonstringJSON array of moves within 3 win% of best
winLossnumberWin% points the mistake cost
piecestringPiece type that moved
plyintegerSource ply in the game
classificationstringBlunder, mistake, or inaccuracy

POST /api/games/:id/analyse

Re-triggers analysis for a game in failed or pending state.

Returns 202 Accepted immediately. Analysis runs in the background and emits WebSocket events to any connected client watching the same game.

Error responses

StatusCondition
409Game is not in a finished state
503Engine pool is unavailable

GET /api/puzzles/due

Returns due FSRS cards for the drill screen, sorted by instructiveness × overdue factor.

Query parameters

ParameterTypeDescription
motifstringOptional. Filter cards to a specific motif tag (e.g. fork, back_rank). Returns only due cards matching that error pattern.

Response

FieldTypeDescription
cardsarrayUp to 10 due cards
totalintegerExact number of due cards
displayTotalstring"40+" when total exceeds the soft cap; otherwise the exact number as a string

When total exceeds the soft cap (40), opening cards are sorted before tactical cards, then by instructiveness.


GET /api/puzzles/practice

Returns cards that are not yet due, for drill-ahead practice.

Response

FieldTypeDescription
cardsarrayNot-yet-due cards
totalintegerTotal count of practice-eligible cards

POST /api/puzzles/:id/attempt

Grades a puzzle attempt and, when in drill phase, schedules the FSRS card.

Request body

FieldTypeDefaultDescription
movestringrequiredMove played, in UCI format ([a-h][1-8][a-h][1-8][qrbn]?)
msTakeninteger ≥ 0requiredTime taken in milliseconds
hintUsedbooleanfalseWhether the hint button was used
attemptNo1 or 21First or retry attempt
phase'quiz' or 'drill''drill'Scheduling context

Response

FieldTypeDescription
correctbooleanWhether the move was correct
ratingstringFSRS rating applied: Again, Hard, Good, or Easy
followupRequiredbooleanA followup move must be submitted
suspectRecallbooleanCorrect in under 2 s on first spaced review — possible position memorisation
bestMoveSanstringCorrect move in SAN
pvstringPrincipal variation
winLossnumberWin% points the original mistake cost
nextDuestring | nullISO 8601 timestamp of next scheduled review; null for quiz phase

Scheduling behaviour

  • phase='drill' — grades and schedules the FSRS card; nextDue is populated.
  • phase='quiz' — practice mode only; creates or updates the card with due=tomorrow but does not advance FSRS state. nextDue reflects tomorrow's date.

GET /api/stats

Aggregate lifetime statistics for the Stats page.

Response

FieldTypeDescription
elointegerCurrent win/loss Elo rating
eloDeltaintegerElo change vs previous game
eloHistoryarray[{elo, recordedAt}] all-time Elo data points
dueCountintegerCurrently due FSRS cards
activeCountintegerCards in active FSRS state (not graduated)
graduatedCountintegerGraduated cards (reps ≥ 5, interval > 180 days, no lapses)
winsintegerLifetime wins (ranked, finished)
lossesintegerLifetime losses
drawsintegerLifetime draws
phaseBreakdownobject{ opening, middlegame, endgame } mistake counts
gameHistoryarray[{result, playedAt}] for date-range filtering
motifBreakdownobjectMotif tag → count across all puzzles
motifAccuracyobjectMotif tag → {total, correct} first-attempt drill accuracy
dimensionBreakdownobjectSkill dimension → count (tactics, positional, endgame)
drillHistoryarray[{day, attempted, correct}] per-day first-attempt drill accuracy (last 30 days)
winRateHistoryarray[{day, played, won, lost, drawn}] per-day ranked game results (last 90 days)
strengthHistoryarray[{playedAt, strengthElo}] per-game move-quality Elo, oldest-first
accuracyHistoryarray[{playedAt, accuracy}] per-game player accuracy, oldest-first
rollingStrengthinteger | nullRolling inverse-variance move-quality Elo (last 10 eligible games)
rollingSeinteger | nullStandard error of rollingStrength
rollingStyleScoreinteger | nullRolling Maia style-match % (last 10 games with Maia probe)
qualityMixobjectMove count by quality tier across all player moves
focusMotifobject | nullRecommended motif to drill: {tag, explanation, drillCount, accuracy}

Repertoire endpoints

GET /api/repertoire/tree

Returns the full repertoire book as a directed acyclic graph.

Response: { nodes[], lineBudget }nodes is the list of position objects (keyed by EPD), each including its associated moves with roles, observation counts, and scores. lineBudget is the maximum number of lines the book will track (configured balance parameter).

GET /api/repertoire/challenges

Returns all open challenges — positions where the player's preferred move may differ from the current canonical book move. Each challenge includes engine evaluation data (engineDeltaWinPts), trend signals, and result performance.

GET /api/repertoire/refusals

Returns the deviation log filtered to alerted entries (alerted_kept, alerted_corrected, alerted_timeout).

Query parameters

ParameterTypeDefaultDescription
limitinteger200Maximum entries to return (max 500)

Response

FieldTypeDescription
refusalsarrayDeviation entries with position and outcome
keptCountintegerTimes the player chose to keep their move
keptInBookCountintegerOf those, times the kept move was later admitted to the book
hitRatePctnumberPercentage of kept moves that ended up in the book

GET /api/repertoire/changelog

The book change feed — all role transitions and promotions.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Maximum entries (max 200)

Entries are enriched with fromSan and toSan (UCI → SAN conversion).

Changelog event kinds: promote, retire, confirm, refuse, settle, reverse, elect, quarantine_exit

POST /api/repertoire/changelog/:id/reverse

Reverses a promote or settle changelog entry. Restores the incumbent move to canonical, suppresses the challenger for a configurable number of encounters, and appends a reverse entry to the changelog.

Error responses

StatusCondition
404Changelog entry not found
409Entry kind is not reversible (only promote and settle are reversible)

GET /api/repertoire/coverage

Response

FieldTypeDescription
totalNodesintegerTotal EPD positions in the book
coveredNodesintegerPositions with a canonical move
coveragePctnumbercoveredNodes / totalNodes × 100
canonicalCountintegerTotal canonical moves across all positions

GET /api/repertoire/journey

Timeline, cumulative growth series, and milestones derived from up to 500 most recent changelog entries.

Response

FieldTypeDescription
timelinearrayDated list of book events
growthSeriesarray{ date, canonicalCount, nodeCount } data points
milestonesarrayNamed events (first confirm, first alert, coverage thresholds)

GET /api/repertoire/gaps

Opponent replies with significant Maia reach probability but no book coverage — positions where you are likely to encounter a move you have not studied.

Response: { gaps }gaps is an array sorted by reachProbability descending. Each entry includes the EPD, the opponent move, and the estimated reach probability.


Error codes

CodeDescription
weights_missingEngine weights file not found on disk
game_not_foundNo game exists with the provided ID
analysis_failedPost-game analysis encountered an unrecoverable error
engine_unavailableEngine pool is not ready or all engines are busy
rate_limitedHint requested too soon (once per 2 seconds)
invalid_messageInbound WebSocket message failed Zod validation

Released under the MIT License.