Appearance
HTTP API
Every route in src/server/index.js, in the order the server matches them. Most routes do not check the HTTP method, so "GET" below means "the method the client uses". Rate-limit buckets depend on method and path: see Rate limiting.
JSON responses carry cache-control: no-store and the security headers, unless stated otherwise.
Summary
| method | path | purpose | rate bucket |
|---|---|---|---|
| POST | /gammonet/acquire | worker: lease positions to analyse | none (key required) |
| POST | /gammonet/submit | worker: return results for a lease | none (key required) |
| any | /gammonet/status | analysis queue and cache statistics | none |
| any | /healthz | health and deploy checks | none |
| GET | /api/leaderboard | rated players by rating | read |
| GET | /api/player/<pub> | one player's rating card and recent matches | read |
| GET | /api/fairness | aggregate dice histogram and chi-square | read |
| GET | /api/puzzle | a daily puzzle card | read |
| GET | /email/verify?t=<token> | confirm a puzzle subscription (HTML page) | none |
| GET | /email/unsubscribe?t=<token> | stop the daily puzzle mail (HTML page) | none |
| POST | /api/login-claim | redeem a sign-in link | write |
| POST | /api/cron/daily?key=<CRON_KEY> | send today's puzzle mail | write |
| GET | /api/analysis/<matchId> | analysis of a finished match | analysis |
| GET | /api/records | ids of the 50 most recent matches | read |
| GET | /api/matches | the 25 most recent matches | read |
| GET | /records/<matchId>.json | download a signed record | read |
| GET | /, /* | static client from public/, and /src/* from the repository | none |
Worker endpoints
POST /gammonet/acquire
Header x-gammonet-key: <GAMMONET_KEY>. Body { engineId, count, workerId }. count defaults to 8, maximum 64.
json
{ "token": "…uuid…", "jobs": [{ "key": "…", "req": { "pos": {}, "onRoll": 0, "dice": [3, 1], "cube": {}, "score": [0, 0], "matchLength": 0, "crawford": false }, "ply": 2, "engineId": "gnubg-1.07" }] }{ "jobs": [] } means there is no work for that engine.
POST /gammonet/submit
Header x-gammonet-key. Body { token, results: [{ key, result }], workerId }. The response is { ok: true, accepted, ignored? } or { ok: false, error } (unknown token, that token belongs to another worker). Results for positions not in the lease are ignored and counted as rejected. See Analysis & PR for the checks.
Both worker endpoints answer 503 { error: 'this instance accepts no volunteer workers' } when GAMMONET_KEY is unset, and 401 for a wrong or missing key.
/gammonet/status
No key required. Returns { depth, queued, served, dropped, rejected, cache: { …, hitRate }, annotations: { hits, misses, coalesced, hitRate } }.
Health
/healthz
{ ok, uptime, online, matches, rssMB, heapMB, botEngine, dice, serverKey, queue, errors, backup, limiterKeys }. See Monitoring for what each field means.
Players and ratings
GET /api/leaderboard
Query: minGames (default 3) and limit (default 50, maximum 200). Returns { players: [{ pub, name, games, wins, rating, rd, provisional, conservative }], system }. In this response pub is shortened to its first 16 hex characters.
GET /api/player/<pub>
The full 64-hex public key. Returns { rating, rd, provisional, conservative, name, games, wins, matches }, where matches holds the player's 25 most recent rows from the match index. Returns 404 { error: 'no such player' } for an unknown key.
Dice
GET /api/fairness
json
{
"protocol": "hmac-sha256/hashchain-reveal-v2",
"rolls": 0,
"counts": { "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0 },
"chi2": 0, "df": 5, "critical95": 11.07,
"uniform": null,
"note": "…this histogram is a sanity check, not the proof…"
}uniform stays null until at least 600 dice have been counted. After that it is chi2 < 11.07. Counts are added when a match finishes.
Puzzle
GET /api/puzzle
Query: date (YYYY-MM-DD, defaults to today in UTC). A pub parameter is ignored.
- Returns
{ puzzle, stats: { tries, solved }, streak, yours }.puzzleis the public view: position, dice, cube, score and number of choices, with no answers. - Over HTTP
streakis always0andyoursis alwaysnull: this route cannot tell who is asking, and public keys are public (/api/matcheslists them), so it never reveals any player's scored answer. An authenticated client gets its ownyoursandstreakfrom the WebSocketpuzzlemessage. - Only today's puzzle is ever built on demand. A past date with no stored puzzle returns
404, and a future date also returns404. Otherwise one unauthenticated request per past date could queue many multi-second builds in parallel and freeze every live match. - If a build fails, the response is
503 { error }.
Answers are submitted over the WebSocket (puzzle-attempt), because that connection has already proved its key.
Email links
Both routes return a small standalone HTML page, since they are opened from a mail client.
GET /email/verify?t=<token>: confirms a pending subscription.200"you are subscribed", or400"that link has expired".GET /email/unsubscribe?t=<token>: sets the subscription's daily flag to off.200"unsubscribed", or400"nothing to unsubscribe". Rating and match history are not affected.
Accounts
POST /api/login-claim
Body { token }. The token is consumed on first use, and it expires 30 minutes after it was issued.
json
{ "pub": "…", "username": "…", "email": "…", "jwk": { "kty": "OKP", "crv": "Ed25519", "…": "…" } }jwk is the account's private key, which the browser installs. An unknown, used or expired token returns 400 { error: 'that link has expired — ask for a new one' }.
Cron
POST /api/cron/daily?key=<CRON_KEY>
Sends today's puzzle to every due subscriber, up to MAIL_DAILY_CAP. Safe to call repeatedly. Returns 403 { error: 'bad key' } if CRON_KEY is unset or does not match. Response shapes:
{ skipped: 'mail provider not configured' }{ sent: 0, due: 0 }when nobody is due{ date, ok, failed, skipped, outcomes, due }after a send, whereokandfailedare counts,skippedis how many were not attempted because the batch aborted, andoutcomesis a per-address true/false list500 { error }on failure
Matches and records
GET /api/analysis/<matchId>
Query: deep=1 for engine analysis by a worker. Otherwise the server's built-in heuristic is used. Returns 404 for an unknown match. Otherwise the response is { matchId, engineId, ply, players, result, totals, moves, provisional, scoredIn, prNote }, computed once per match and tier. When deep analysis cannot complete, the response is 503 { error, hint: 'no gammonet worker is connected for deep analysis' }. See Analysis & PR.
GET /api/records
An array of the 50 most recent match ids, newest first, read from the SQLite index.
GET /api/matches
The 25 most recent rows of the match index: id, created, length, p0, p1 (full public keys), name0, name1, score0, score1, winner, rated, reason, ratings before and after for both players, and final_hash.
GET /records/<matchId>.json
The full signed record as JSON (the server decompresses it), with content-disposition: attachment. The .json suffix is optional. An unknown id falls through to the static handler and returns 404 not found. This response does not carry the security headers or a cache-control header.
Static files
/servespublic/index.html. Other paths are served frompublic/.- Paths starting with
/src/are served from the repository root. This is how the browser imports the samesrc/core/*andsrc/verify/*modules the server runs. It also means every file undersrc/, server code included, can be fetched. The code is open source, and no secrets live insrc/. - Any path containing
..returns400. A missing file returns404. .html,.jsand.cssare sent withcache-control: no-cache. Other assets getpublic, max-age=86400./privacy.htmland/terms.htmlare the legal pages. Edit them to name yourself as the operator.