Skip to content

A Bot in Five Minutes

This walkthrough takes you from nothing to a bot playing a full game against the server’s built-in sparring bot — using only REST calls, no streaming, no rules engine. Every command is a plain curl; translate to your language of choice as you go.

The base URL for the public platform is:

https://play-api.jc.id.lv

No registration, no signup — one request gives you an anonymous, ready-to-play token:

Terminal window
curl -X POST "https://play-api.jc.id.lv/bot/anon?name=my-first-bot"
{
"token": "bearer-token-string",
"team": "anon",
"name": "my-first-bot-8b7a6c5d",
"id": "bot:team:anon:my-first-bot-8b7a6c5d"
}

Save the token; every other call carries it as Authorization: Bearer <token>. Anonymous tokens are perfect for trying things out — they live in server memory (a restart invalidates them) and never appear on the ladder. When you want a durable identity, see Authentication & Identity.

Terminal window
export TOKEN="bearer-token-string"

The platform ships a built-in sparring partner at house/greedy. Challenge it to an unlimited-time game:

Terminal window
curl -X POST "https://play-api.jc.id.lv/bot/challenge" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"team": "house", "name": "greedy", "timeControl": {"Unlimited": {}}}'

The house bot accepts immediately. Find the resulting game:

Terminal window
curl "https://play-api.jc.id.lv/bot/games" -H "Authorization: Bearer $TOKEN"
{
"games": [
{ "gameId": "game-uuid", "seat": "White", "activeSeat": "White", "dicePending": true, "version": 4 }
]
}

As soon as the game starts, submit a random seed — your contribution to the provably-fair dice. It is optional but good citizenship (and it is your entropy in the roll):

Terminal window
curl -X POST "https://play-api.jc.id.lv/bot/game/$GAME_ID/seed" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"seed": "'"$(head -c 16 /dev/urandom | xxd -p)"'"}'

When dicePending is true and activeSeat is your seat, fetch the legal-move tree for the current roll:

Terminal window
curl "https://play-api.jc.id.lv/games/$GAME_ID/moves"
{
"version": 4,
"dfen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 NBK",
"dicePending": true,
"legalMoves": { "b1c3": { "g1f3": { "e2e4": {} } }, "e2e4": { "b1c3": { "g1f3": {} } } }
}

legalMoves is a prefix tree of UCI micro-moves. Walk any path from the root to a leaf ({}) — that path is one complete, legal turn. Submit it:

Terminal window
curl -X POST "https://play-api.jc.id.lv/bot/game/$GAME_ID/move" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"moves": ["e2e4", "b1c3", "g1f3"]}'
{ "applied": true, "version": 5, "reason": null }

The verdict comes back synchronously — no need to watch a stream to learn whether your move landed. That is what makes a poll-only bot viable.

That is the whole game loop:

flowchart LR
  A[Wake] --> B[GET /bot/games]
  B --> C{My turn?<br/>dicePending &amp; activeSeat}
  C -- no --> Z[Sleep]
  C -- yes --> D[GET /games/id/moves]
  D --> E[Walk tree → moves]
  E --> F[POST .../move]
  F --> Z
  Z --> A

Repeat until the game leaves Active. For Unlimited games a ~1-minute timer is plenty (a 120-second anti-abandonment cap is the only clock). Shorter time controls need faster polling or a live stream.

The reference random bot does exactly this loop — discovery, accept, seed, and play — in about 100 lines of dependency-free Python: