The Lichess Integration¶
Linking a Lichess account to a board buys three things the product would otherwise have to build: an endless rated tactics library (Chapter Puzzles), a second rating that moves when you solve on the board, and — later — the board as a physical client for the player’s own online games.
This chapter covers the terms the integration is built on, the auth flow, and the fair-play rule, which is the most important paragraph in this manual’s connected half.
The terms¶
D-76 — No permission is required, but the board must be fully functional with no Lichess account
Context Lichess’s terms grant commercial use, and the Board API’s stated purpose is playing on Lichess with physical boards. So the integration is permitted.
Decision Build it — and treat access as revocable at their discretion. Puzzles therefore ship on the board from the CC0 database, and the account link is a sync upgrade, never the feature itself.
Rejected Depending on the integration. A consumer appliance that becomes less of a chess board when a third party changes its API terms is not a product we can ship.
Consequences Every Lichess-touching feature has an offline answer: puzzles come from the shipped book, the rating that the board displays by default is its own (D-79), and broadcast mirroring needs no account at all (D-82). Trademark use — the Lichess name or logo on packaging or a store listing — is a separate question from API access and is one for counsel, not for engineering.
D-78 — HTTP stays in the Qt layer; kirin_app gains no HTTP dependency
Context The integration needs a token store, account fetches, puzzle batches and result posts.
Decision LichessClient (kirin-gui/src/lichess_client.h) is a QNetworkAccessManager client in the GUI layer, exactly like the matchmaking, device-link and upload clients. Everything chess-shaped — parsing, identity assignment, the solve state machine, ratings — stays pure.
Rejected Networking in the core. It would put an HTTP dependency into the library that is supposed to run in the hardware-free test suite, and the parsing is the part worth testing.
Consequences The pure parser is shared rather than reimplemented in Qt: the client hands raw JSON to parseApiBatch and does not parse puzzles itself. One request is in flight at a time; an HTTP 429 parks the queue for a minute and re-heads the request rather than retrying into a block, and a 401 unlinks rather than spending the queue on a dead token. Solved puzzles are persisted before they are sent and re-reported on the next boot — a rated solve lost to flaky Wi-Fi is rating the player earned and did not get.
Auth: the phone does the OAuth¶
D-83 — The account link is OAuth PKCE on the player’s phone, and we are a courier, not a custodian
Context An access token is a live credential on someone’s real Lichess account, and typing one on a touchscreen is not a flow anybody completes.
Decision The board mints a QR (and a typeable code); the phone performs OAuth PKCE against lichess.org in the browser; the phone pushes the resulting token to our Worker addressed to that board; the board collects it on its next poll — and the pickup deletes the server’s copy. The board persists the token before doing anything else with it, because after the pickup there is no second copy to ask for.
Rejected Holding tokens at rest. It would make the table worth stealing and make us responsible for a secret we have no need to keep. Also rejected: a paste-a-personal-token escape hatch as the shipping path (the setter remains, and is still the development entry point).
Consequences PKCE is what makes a browser-side exchange safe: we are a public OAuth client, so there is no client secret, Lichess requires no registration, and the client_id is simply our origin — which is also what the player sees on the consent screen, and anything else there would be lying to them about who is asking. Scopes requested are puzzle:read and puzzle:write only; board:play is deliberately not requested until online play exists, because a scope you do not need is a permission you have to justify on a consent screen you did not have to show.
Fair play — the constraint that must not be violated¶
Kirin is a chess board with a strong engine inside it and LEDs under every square that can display the engine’s recommended move. The Lichess Board API says, twice, that engine assistance or any kind of outside help is forbidden.
If a player ran a rated Lichess activity through this board with hints on, that would be engine-assisted cheating committed by our product on their account. The fallout lands on them (a ban) and on us (a blacklisted integration, and a consumer chess brand known as a cheating device — close to fatal).
D-77 — Fair play is structural: no hint source is supplied at all during Lichess-rated activity
Context A setting that disables hints is one bug, one refactor, or one enthusiastic feature request away from being wrong.
Decision Enforce it where the session is constructed. PuzzleSession takes no IHintSource — not one that is switched off, one that does not exist — and PuzzleLedAssist holds none either, rather than reusing the assist layer that can light the engine’s recommendation. Both are pinned by static_assert in their tests, and the bridge’s test walks its metaobject asserting that nothing on it offers a hint, a solution or an expected move. QML can call only what the metaobject exposes.
Rejected A toggle, or a guard in the GUI. Both are enforcement in the wrong place: they can be bypassed by any new call site, and neither fails at compile time.
Consequences There is an asymmetry the project already lives with: the Bot API explicitly allows engine assistance, which is why the lichess-bot on the VPS is legitimate — it is a bot account, a different thing entirely. These two code paths must never blur. The product also says the rule out loud, which is both a compliance artifact and a trust signal.
Two ratings, kept apart¶
D-79 — Kirin keeps its own rating, authoritative for the device; the Lichess rating is displayed alongside
Context The player has a Lichess rating when linked, and most Kirin games are played offline against the board’s own engine.
Decision The Kirin rating (Chapter Playing a Game) is computed and stored on-device and is what the board shows by default. The Lichess rating is fetched when linked and displayed beside it — ours first and in the accent colour, theirs greyed and labelled.
Rejected Depending on Lichess to know how strong a player is. Most games happen with no account and no network; a strength estimate that requires either is not available when it is needed.
Consequences The board rates only games against an engine of published strength — never a friend game, which the server rates against a real opponent whose rating this board does not know. And the Kirin rating never goes near the leaderboard: the server refuses to rate solo games for the same reason (D-72’s schema records them but leaves them unrated), because a ladder you can climb by beating an engine you configured yourself is not a ladder.
Solving a puzzle on the board posts the result to Lichess and moves the player’s real puzzle rating, reporting win honestly; an unrated practice mode is offered. A solve from the shipped book still reports when linked — the ids are Lichess’s own and the solve was unaided.
What is not built¶
Online play — the board as a physical Board API client for the player’s own Lichess games — is designed but not built. It would be an INetworkTransport implementation (the same seam networked play uses, Chapter Networked Play), with the fair-play lockout enforced at construction, and it adds the board:play scope that D-83 deliberately does not request yet.