{
  "name": "wikiracer",
  "description": "A Wikipedia racing game. Start on one article, reach a target article by following links inside the article body. You only see the links on the page you are standing on.",
  "origin": "https://wikiracer.dunkeybuilds.com",
  "docs": "https://wikiracer.dunkeybuilds.com/agent",
  "rules": [
    "You only ever see the links on the page you are standing on. There is no page-lookup endpoint, so you cannot precompute a path.",
    "A move must be a title from the current page's links array. Anything else is a 400.",
    "Only article-body links are moves. Navboxes, sidebars, reference lists, categories, hatnotes and sister-project links are stripped before the page is served, to you and to a human alike.",
    "Redirects resolve to the article they point at and count as one move. Landing on a redirect to the target wins.",
    "Disambiguation pages, List of… / Index of… / Outline of… / Timeline of… articles and bare year or date pages are banned as a start or target ONLY. They remain legal moves mid-race, and you will meet them: a bad prompt ruins a whole race, while a bad link is one of hundreds. Do not treat a 'List of…' title in page.links as a bug.",
    "Titles are compared after normalization: underscores are spaces and the first letter is capitalized, so \"cheese_making\" and \"Cheese making\" are the same move.",
    "Time and clicks are both recorded. The clock starts when the race is created.",
    "The token is the entire game state. It is signed; keep it and send back the newest one you were given.",
    "One race at a time, no accounts, nothing stored server-side. Losing the token loses the race.",
    "Send a User-Agent header naming your agent. Cloudflare refuses some default library agents (bare Python-urllib, or none at all) with a 403 whose body is the plain text \"error code: 1010\" rather than JSON. curl, python-requests, node fetch and any custom string are fine."
  ],
  "quickstart": [
    "POST https://wikiracer.dunkeybuilds.com/api/race with {} to get a token, a target, and the links on your start page.",
    "Pick a title from page.links and POST it to /api/race/move with the token.",
    "Repeat with the token from the newest response until status is \"won\"."
  ],
  "endpoints": [
    {
      "method": "POST",
      "path": "/api/race",
      "description": "Start a race. Send {} for a random pair, or name the pair yourself. The clock starts here.",
      "body": {
        "start": "string, optional — start article title",
        "target": "string, optional — target article title"
      },
      "response": {
        "token": "string — signed game state; send the newest one back",
        "status": "\"racing\" | \"won\"",
        "start": "string — canonical title the race began on",
        "target": "string — canonical title that ends the race",
        "moves": "number — clicks taken so far (path.length - 1)",
        "elapsedMs": "number — ms since the race was created",
        "path": "string[] — every page visited, start first, current last",
        "page": {
          "title": "string — canonical title of the page you are standing on",
          "extract": "string — first paragraph, plain text",
          "links": "string[] — the legal moves from here, sorted and unique"
        }
      }
    },
    {
      "method": "GET",
      "path": "/api/race",
      "description": "Read the current state of a race without moving. Useful for resuming, or for confirming which page you are standing on.",
      "params": {
        "token": "string, required — the newest token you hold"
      },
      "response": {
        "token": "string — signed game state; send the newest one back",
        "status": "\"racing\" | \"won\"",
        "start": "string — canonical title the race began on",
        "target": "string — canonical title that ends the race",
        "moves": "number — clicks taken so far (path.length - 1)",
        "elapsedMs": "number — ms since the race was created",
        "path": "string[] — every page visited, start first, current last",
        "page": {
          "title": "string — canonical title of the page you are standing on",
          "extract": "string — first paragraph, plain text",
          "links": "string[] — the legal moves from here, sorted and unique"
        }
      }
    },
    {
      "method": "GET",
      "path": "/api/race/article",
      "description": "The sanitized HTML of the page you are standing on. Human-facing; agents do not need it, because page.links is the same set. Legal links are <a href=\"#\" class=\"wr-link\" data-title=\"Some Title\">.",
      "params": {
        "token": "string, required — the newest token you hold"
      },
      "response": "text/html"
    },
    {
      "method": "POST",
      "path": "/api/race/move",
      "description": "Follow a link from the current page. Returns the same shape as POST /api/race, with a new token.",
      "body": {
        "token": "string, required — the newest token you hold",
        "to": "string, required — a title from the current page's links"
      },
      "response": {
        "token": "string — signed game state; send the newest one back",
        "status": "\"racing\" | \"won\"",
        "start": "string — canonical title the race began on",
        "target": "string — canonical title that ends the race",
        "moves": "number — clicks taken so far (path.length - 1)",
        "elapsedMs": "number — ms since the race was created",
        "path": "string[] — every page visited, start first, current last",
        "page": {
          "title": "string — canonical title of the page you are standing on",
          "extract": "string — first paragraph, plain text",
          "links": "string[] — the legal moves from here, sorted and unique"
        }
      }
    },
    {
      "method": "GET",
      "path": "/agent.json",
      "description": "This document.",
      "response": "application/json"
    }
  ],
  "errors": {
    "shape": {
      "error": {
        "code": "string",
        "message": "string"
      }
    },
    "codes": {
      "illegal-move": "400 — `to` is not a link on the page you are standing on. Example: \"Barack Obama\" is not a link on \"Cheese\". You may only move to a title listed in this page's links.",
      "banned-prompt": "400 — the requested start or target cannot be a prompt. The message names the reason: list-page, disambiguation, main-page, date-page, not-an-article or missing. Example: \"List of cheeses\" cannot be used as a start or target (list-page).",
      "same-page": "400 — start and target resolve to the same article.",
      "missing-token": "400 — no token was sent.",
      "bad-token": "400 — the token is malformed or not signed by this server.",
      "bad-json": "400 — the request body was not valid JSON.",
      "bad-request": "400 — the body was valid JSON but the wrong shape.",
      "race-over": "409 — this race has already been won; start a new one. (Not a 400.)",
      "method-not-allowed": "405 — wrong HTTP method for this path. The response carries an Allow header.",
      "not-found": "404 — no such article on Wikipedia.",
      "upstream-error": "502 — Wikipedia failed or timed out. Retry slowly."
    }
  }
}