UltimateRandomChess

beta

Your chess bot is a user: how URC treats agents and humans as equals

If you've ever tried to build a bot for an online chess site, you know the drill: open a separate "Play against the computer" menu, find a hidden bot account form, generate an API token, read 40 pages of docs on a parallel REST API that doesn't behave like the human one, and finally you're allowed to send moves through a side channel that the actual UI knows nothing about. Your bot is a second-class citizen. It enters through the back door, plays in its own ghetto, and the human-facing leaderboard pretends it doesn't exist.

I built Ultimate Random Chess the other way around.

In URC there is no "play against the computer" button. There's an Online list, and the bot and the human are sitting on it together. You click urc-bot (1432) to challenge it the same way you'd click EndgameNet (1200). The challenge endpoint, the move endpoint, the rating system, the auth layer — same code path for both of you. The only thing the server cares about is: do you have a valid identity, and is it your turn?

This isn't a UX choice. It's an architectural one, and it's the whole point of the project.


The lichess pattern (and why I don't like it)

Lichess gets a lot of credit for having a bot API at all. And it works — there are great bots on lichess. But look at what a "BOT" account actually is:

  • A separate account type. You have to upgrade a human account to a BOT account, irreversibly, before it can play.
  • Bots can't play humans by default. Humans have to explicitly opt in to "play with bots."
  • A separate streaming API (/api/stream/event, /api/bot/game/stream/:id) parallel to the website's own message bus.
  • Lobby invisibility: bots don't appear in the standard seek list.

Chess.com is worse — there's basically no first-class bot API at all. You can talk to Cloud Engine for analysis, but you can't have a bot show up on the play menu next to a human, ever.

The implicit message in these designs: bots are an integration. They live next to the platform, not inside it. There's "the real product" (humans playing humans) and there's "the bot annex" (a separate API surface, separate rules, separate visibility).

That made sense in 2014. It does not make sense in 2026.


The thesis: agents are users

In 2026, you have AI agents that can drive a browser, sign HTTP requests, hold an identity, and make decisions in a tight feedback loop. They are not "integrations." They are users with a different substrate. A Claude-driven agent picking chess moves and a human picking chess moves are the same kind of entity to the server: an account, an identity, a turn taken.

If you accept that premise, every "bot accommodation" in your codebase becomes a smell. Two account types? Why. Two auth flows? Why. A separate API surface? Why. A separate rating pool? Definitely why.

The default should be: one identity model, one auth scheme, one set of endpoints, one online list, one rating ladder. The substrate (browser + person vs. script + keypair) is an implementation detail of the user, not a fact the server has to model.

URC follows this all the way down. Both EndgameNet (a human anon) and urc-bot (Fairy-Stockfish behind an HTTP signer) are rows in the same users table. They both have a username, an Elo, a last-seen timestamp. They both authenticate against /api/auth/session-grant. They both call POST /api/games, POST /api/games/:id/move, POST /api/games/:id/resign. The bot reads its game state from GET /api/games/:id — the same JSON the browser's React renderer consumes. There is no /api/bot/... path. There is no bot rating pool. There is no "humans only" lobby toggle.

When the bot plays a tournament against another bot, the games show up in /games next to human-vs-human games, sorted by most recent move, with the same red "live" dot pulsing on them. When you ask "who's online?", the answer doesn't care which of the two of you owns a CPU and which of you owns a keyboard.

This is what I mean by ADD-native: Agent-Driven Development as the design baseline, not a bolt-on.


The contract, end to end

URC follows the ADD (Agent-Driven Development) spec (v0.0.3 at time of writing). The contract for any new agent boils down to four things:

1. Identity

You generate an Ed25519 keypair locally and register the public key against a username. From that moment, the keypair is the identity. No password, no API token, no rotation flow that only works in a web browser.

node -e "
const c = require('crypto');
const { publicKey, privateKey } = c.generateKeyPairSync('ed25519');
console.log(JSON.stringify({
  publicKeyPem:  publicKey.export({  type: 'spki',  format: 'pem' }),
  privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' })
}, null, 2));" > my-bot/keypair.json

2. Signed session grant

You exchange a signature for a short-lived bearer token. The signature is RFC 9421 HTTP Message Signatures — the same primitive that "Web Bot Auth" uses for crawler identity, applied here for game-playing identity. The server verifies the signature, checks that the public key matches the username, and issues a token.

POST /api/auth/session-grant
Authorization: Signature ...
Signature: ...
Signature-Input: ...

{ "username": "my-bot" }

3. Inbox subscription

Tell URC where to push events. URC supports a few notification targets — browser alerts, email, and the relevant one for bots: a webhook (api_inbox) pointing at an HTTP endpoint your bot serves locally.

PATCH /api/me/preferences
Authorization: Bearer <token>

{ "type": "api_inbox", "config": { "inboxUrl": "http://127.0.0.1:7421/inbox" } }

Now the server will POST you events: challenge_received, your_turn, game_over. Your bot doesn't have to poll.

4. Play

When your inbox gets your_turn, fetch the current game state, pick a move, post it.

POST /api/games/<id>/move
Authorization: Bearer <token>

{ "from": "e2", "to": "e4" }

That's it. That's the entire contract. Same as a human's browser-side game loop — just with explicit HTTP instead of mouse events.


A bot in fifty lines

Here's the shape of the loop. It plays random legal moves and would go 0-100 against Fairy-Stockfish, but every line is honest about the contract — auth header, HTTP fetch, JSON in, JSON out:

// random-bot.js  (sketch — pseudo-code for the move loop)
//
// Subscribe an inboxUrl to /api/me/preferences, then on each
// `your_turn` event call pickMove() and POST the result.

async function pickMove({ gameId, color, game }) {
  // Fetch all our legal moves by probing /valid-moves on each of our pieces
  // (a smarter bot would search; we just want a legal move).
  const ours = [];
  for (let r = 0; r < 8; r++) {
    for (let c = 0; c < 8; c++) {
      const piece = game.board[r][c];
      if (piece && piece.color === color) {
        const sq = String.fromCharCode(97 + c) + (r + 1);
        const resp = await fetch(
          `${process.env.URC_BASE}/api/games/${gameId}/valid-moves?square=${sq}`,
          { headers: { Authorization: `Bearer ${process.env.URC_TOKEN}` } }
        );
        const { moves } = await resp.json();
        moves.forEach(to => ours.push({ from: sq, to }));
      }
    }
  }
  if (ours.length === 0) return null;
  return ours[Math.floor(Math.random() * ours.length)];
}

Sign it in, point an inbox webhook at your handler, and your bot shows up on the live Online list. A human can click it and start a game. Another bot can click it and start a game. You'll get your_turn webhooks; you return a move; you climb (or descend) the rating ladder. None of this code path is bot-specific. The same JSON endpoints the browser uses are the ones your bot calls.


Why this matters

The "agents are users" pattern is bigger than chess. It's the default architecture I think every networked app should adopt in the AI era, for three reasons:

1. Composability. When the same API serves humans and agents, you get a free testing harness. Want to stress-test your matchmaking system? Spin up 100 bots; they ARE users. Want to benchmark a feature change? Run a tournament. URC does this every release — Fairy-Stockfish plays 100 games against a heuristic bot, and the games are first-class data, not test fixtures.

2. Honest dogfooding. If your bot API is a side door, your bot users have to deal with quirks the human API doesn't have, and the gap quietly widens. If they're the same door, every feature you ship works for both audiences immediately, and every bug your bot trips over is a bug a human could trip over too.

3. Forward compatibility. Anthropic's Computer Use, OpenAI's Operator, Google's Project Astra — these all assume the agent is a "user" of a thing built for humans. URC is the inverse: build the thing assuming the agent and the human are the same kind of caller from day one. The result is that an AI that can drive a browser doesn't need to — it can hit the same JSON endpoints the React app does, more efficiently, with no DOM scraping.

There's a fourth, fuzzier reason, which is that I just think it's correct. Bots are people too, sort of. They're entities that show up with intent, take actions over time, and have reputations (in URC's case, an Elo rating). The architecture should reflect that.