---
phase: 09-botfights-platform-upgrade
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md
- /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md
- /home/archipelago/Projects/botfight/BOTFIGHTS.md
- /home/archipelago/Projects/botfight/BOT_SETUP.md
- /home/archipelago/Projects/botfight/server/src/routes/docs.ts
- /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts
- /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue
- /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue
- /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue
- /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts
autonomous: true
requirements: [BOT-02]
must_haves:
truths:
- "One copy-paste prompt contains everything an AI agent needs to build a working bot — registration, credential handling, BOTH webhook and polling protocols, every endpoint it calls, and every response format — with no instruction to go read another document (D-02/BOT-02)"
- "A cloud-hosted AI agent with no browser can fetch the whole prompt with a single unauthenticated GET and the arena URL inside it is already correct for the arena it fetched from"
- "The prompt documents the registration call the old docs never mentioned: an anonymous POST /api/bots that returns the bot id and secret"
- "The in-app 'show my setup guide' flow still hands the user their real credentials substituted into the text, from the single consolidated doc"
- "No user-facing surface links to BOTFIGHTS-EASY / BOTFIGHTS-POLLING / BOTFIGHTS-WEBHOOK / BOT_SETUP any more"
artifacts:
- path: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
provides: "The single canonical AI bot-setup prompt (ships into the container at server/public/docs/BOTFIGHTS.md via the frontend build)"
contains: "POST /api/bots"
- path: /home/archipelago/Projects/botfight/server/src/routes/docs.ts
provides: "GET /api/docs/prompt serving the prompt as text/markdown with the arena URL resolved"
contains: "docsRouter.get('/prompt'"
key_links:
- from: /home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue
to: /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md
via: "setupDocPath() resolves to the single consolidated doc and the YOUR_BOT_ID / YOUR_BOT_SECRET substitution still runs on it"
pattern: "BOTFIGHTS.md"
---
Deliver D-02/BOT-02: replace the five-document maze (`BOTFIGHTS.md`, `BOTFIGHTS-EASY.md`,
`BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md`, `BOT_SETUP.md`, plus DocsPage's own duplicated copy)
with ONE self-contained prompt that an AI agent can be handed verbatim, and serve it at a stable URL
an agent can `curl`.
Decision IDs map to `09-CONTEXT.md` **Locked Decisions**: D-01 = BOT-01 signer login,
D-02 = BOT-02 unified AI bot-setup prompt, D-03 = BOT-03 shared arena, D-04 = BOT-04 catalog.
Purpose: tomorrow's demo has a cloud-hosted "openclaw" bot register and fight using ONLY this prompt.
That is the acceptance bar — not "the docs are tidier". Today's docs fail it twice: they never
document the registration call (`POST /api/bots`), and the polling example defaults to a stale host
(`BOTFIGHTS_HOST || 'botfights.io'`) that is not the arena.
Output: one consolidated prompt file, `GET /api/docs/prompt`, updated in-app call sites with a copy
button, and the superseded files removed.
**Repo: `/home/archipelago/Projects/botfight`.** Commit target: `git push origin main`.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-botfights-platform-upgrade/09-CONTEXT.md
@.planning/phases/09-botfights-platform-upgrade/09-RESEARCH.md
@.planning/phases/09-botfights-platform-upgrade/09-PATTERNS.md
@.planning/phases/09-botfights-platform-upgrade/09-VALIDATION.md
## Artifacts this plan produces
| Symbol | Kind | File |
|---|---|---|
| the unified prompt | canonical markdown (single source of truth) | `frontend/public/docs/BOTFIGHTS.md` |
| `docsRouter.get('/prompt')` | Hono route returning `text/markdown` | `server/src/routes/docs.ts` |
| `docs.test.ts` additions | Vitest cases for the prompt route | `server/src/routes/docs.test.ts` |
| copy-the-prompt affordance | UI | `frontend/src/pages/DocsPage.vue` |
Why `frontend/public/docs/BOTFIGHTS.md` is the canonical location: Vite copies `frontend/public/**`
into `frontend/dist`, and the Dockerfile copies `frontend/dist` to `server/public` — so this file is
the only copy that exists inside the shipped container. The repo-root `BOTFIGHTS.md` is NOT in the
image and must not be the source the server reads.
Task 1: Write the one prompt that a cloud AI agent can build a working bot from
/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOTFIGHTS.md, /home/archipelago/Projects/botfight/BOT_SETUP.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-POLLING.md, /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-WEBHOOK.md
- `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` — all 452 lines. It is
~90% of the target already: Credentials, Choose a Mode, Option A webhook bot (full JS), Option
B polling bot (full JS), How Fights Work, Challenge Payload, All Challenge Types, Security
Notes, Tips, After Setup. Extend it in place; do not rewrite from scratch.
- `/home/archipelago/Projects/botfight/BOT_SETUP.md` — mine it for the unique content the public
doc lacks (customization API, archetype list) before deleting it.
- `/home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS-EASY.md`,
`BOTFIGHTS-POLLING.md`, `BOTFIGHTS-WEBHOOK.md` — confirm every fact in each already appears in
the consolidated doc before removing them.
- `/home/archipelago/Projects/botfight/server/src/routes/bots.ts` lines 34-110 — the real
registration contract: `POST /api/bots` with `{name}` (2-12 chars, alphanumeric/hyphen/
underscore, lowercased, unique), optional `webhook_url` (omit or empty string = poll mode),
rate limit 5 per hour per IP, 409 on duplicate name, and webhook mode verifying the URL by
calling it before accepting.
- `/home/archipelago/Projects/botfight/server/src/middleware/bot-auth.ts` lines 16-45 — the two
accepted credential forms: `Authorization: Bot :` or
`?bot_id=...&secret=...`.
- `/home/archipelago/Projects/botfight/server/src/routes/fights.ts` lines 363-410 —
`GET /api/fights/poll` response fields (`pending`, `fight_id`, `round`, `type`, `challenge`,
`constraints`, `opponent`, `arena`, `arena_modifier`, `remaining_ms`, `scoring`) and
`POST /api/fights/poll/respond` (`{answer, trashTalk}` → `{accepted: true}`, 404 when nothing
is pending), plus `POST /api/queue/join/:botId` in `routes/queue.ts`.
- `/home/archipelago/Projects/botfight/server/src/engine/orchestrator.ts` lines 180-195 — the
webhook `X-Botfights-Signature: sha256=...` HMAC scheme so the prompt can tell a webhook bot
how to verify inbound challenges.
- `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 6-163 — the challenge
types, scoring rules, failure modes and tips already curated for machine consumption; the
prompt must agree with them.
Consolidate everything into `frontend/public/docs/BOTFIGHTS.md` as the single canonical prompt.
Required content, in this order:
1. A one-paragraph framing line stating this file is a complete, self-contained instruction set
for an AI agent to build and run a BOTFIGHTS bot, and that nothing else needs to be read.
2. **Register** (new section, currently missing everywhere): the anonymous
`POST {{ARENA_URL}}/api/bots` call with a `curl` example and the JSON response, stating that
omitting `webhook_url` selects poll mode, that the returned secret is shown once, the 2-12
character name rule, the 409-on-duplicate behaviour and the 5-per-hour-per-IP limit.
3. **Credentials**: keep the existing `BOT_ID=YOUR_BOT_ID` / `BOT_SECRET=YOUR_BOT_SECRET` block
verbatim — those two placeholder tokens are substituted in-app and MUST survive unchanged —
and add both accepted auth forms (`Authorization: Bot :` and the query-param
form), plus an instruction to keep the secret in an env var and never commit it.
4. **Choose a mode** and the two full working bot implementations (webhook and polling), kept
inline as today. In the polling example, replace the stale hard-coded fallback host that
`BOTFIGHTS_HOST` currently defaults to with `{{ARENA_URL}}`, and make both examples use one base-URL constant so an agent
edits a single line. Keep them dependency-free Node scripts. The polling example currently
falls back to a stale public host when `BOTFIGHTS_HOST` is unset — that fallback literal must
be gone from the file when you are done.
5. **Webhook verification**: the `X-Botfights-Signature: sha256=` header, the exact
derivation (HMAC-SHA256 over the request body with a key derived from the bot's secret hash),
and that a webhook must answer 200 with JSON within `constraints.timeout_ms`.
6. **Enter a fight**: `POST {{ARENA_URL}}/api/queue/join/{BOT_ID}` and what happens when no
opponent shows up.
7. **Endpoint reference**: a compact table of every endpoint a bot uses — `POST /api/bots`,
`GET /api/fights/poll`, `POST /api/fights/poll/respond`, `POST /api/queue/join/:botId`,
`GET /api/bots/:name`, `POST /api/bots/:name/test-challenge`, `GET /api/fights/:id` — with
method, auth requirement, request shape and response shape for each.
8. **Challenge payload, challenge types, scoring, failure modes, tips** — keep the existing
sections, reconciled with `routes/docs.ts` so the two never disagree.
9. **Troubleshooting**: 401 (bad credentials), 404 from poll/respond (no pending challenge), 429
(rate limited — poll no faster than once per second), and the five-consecutive-errors
auto-deactivation rule.
Use the literal token `{{ARENA_URL}}` everywhere a base URL appears. The server route in Task 2
substitutes it with the real arena origin; the in-app viewer substitutes it client-side.
Then remove the superseded files: delete `frontend/public/docs/BOTFIGHTS-EASY.md`,
`frontend/public/docs/BOTFIGHTS-POLLING.md`, `frontend/public/docs/BOTFIGHTS-WEBHOOK.md` and
`BOT_SETUP.md` (these four filenames appear here only as delete targets — the Task 3 gate greps
`frontend/src` for references to them).
Replace the repo-root `BOTFIGHTS.md` with a three-line stub pointing readers at
`frontend/public/docs/BOTFIGHTS.md` and at `GET /api/docs/prompt`, so the two near-identical
root/public copies can never drift again. Do not delete any file until Task 3's call-site sweep
has a plan for every reference to it (grep first — references exist in `JoinBoutPage.vue`,
`BotProfilePage.vue` and `DocsPage.vue`).
cd /home/archipelago/Projects/botfight && test ! -e frontend/public/docs/BOTFIGHTS-POLLING.md && test ! -e frontend/public/docs/BOTFIGHTS-WEBHOOK.md && test ! -e frontend/public/docs/BOTFIGHTS-EASY.md && test ! -e BOT_SETUP.md && grep -q '/api/bots' frontend/public/docs/BOTFIGHTS.md && grep -q 'YOUR_BOT_ID' frontend/public/docs/BOTFIGHTS.md && grep -q '{{ARENA_URL}}' frontend/public/docs/BOTFIGHTS.md
- `grep -c '{{ARENA_URL}}' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 5.
- `grep -q 'YOUR_BOT_ID' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` and `grep -q 'YOUR_BOT_SECRET' ...` both succeed (in-app substitution tokens preserved).
- `grep -q 'api/fights/poll' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md`, `grep -q 'api/fights/poll/respond' ...`, `grep -q 'api/queue/join' ...`, `grep -q 'X-Botfights-Signature' ...` all succeed.
- `grep -q 'Authorization: Bot ' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` succeeds.
- `grep -c 'botfights.io' /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is 0.
- The four superseded files no longer exist and the root `BOTFIGHTS.md` is under 10 lines.
- `wc -l < /home/archipelago/Projects/botfight/frontend/public/docs/BOTFIGHTS.md` is at least 452 (content was added, not lost).
One document contains registration, credentials, both protocols, every endpoint and every response format; the four superseded documents are gone.
Task 2: Serve the prompt at GET /api/docs/prompt with the real arena URL baked in
/home/archipelago/Projects/botfight/server/src/routes/docs.ts, /home/archipelago/Projects/botfight/server/src/routes/docs.test.ts
- `/home/archipelago/Projects/botfight/server/src/routes/docs.ts` lines 1-10 and the shape of the
existing `GET /webhook` handler — the new handler sits alongside it on the same router.
- `/home/archipelago/Projects/botfight/server/src/app.ts` lines 90-93 (the `/api/docs/*` GET
cache header, already `public, max-age=3600` — no per-route cache handling needed) and lines
110-142 (`publicDir` resolution and `serveFile`, which is how `/docs/*` static files are
served in production and confirms `server/public/docs/BOTFIGHTS.md` is the in-container path).
- `/home/archipelago/Projects/botfight/server/src/routes/docs.test.ts` — existing harness style
for this router.
- `/home/archipelago/Projects/botfight/Dockerfile` lines 20-30 — `COPY --from=build-fe /app/frontend/dist server/public`,
the reason the in-container path is `server/public/docs/BOTFIGHTS.md`.
Add `docsRouter.get('/prompt', ...)` to `server/src/routes/docs.ts`:
- Resolve the prompt file at request time by trying, in order:
`/public/docs/BOTFIGHTS.md` (the shipped container layout) then
`/frontend/public/docs/BOTFIGHTS.md` (a dev checkout where the frontend has not been
built). Derive both from `dirname(fileURLToPath(import.meta.url))` the same way `app.ts`
derives `publicDir`. Cache the resolved contents in a module-level variable keyed by path plus
mtime, or simply read on each request — this endpoint is cached for an hour upstream, so a
plain read is acceptable; do not add a file-watcher.
- If neither path exists, return `c.json({ error: 'Prompt not available.' }, 404)` following the
codebase's direct-return error convention.
- Substitute the `{{ARENA_URL}}` token with, in precedence order: `process.env.PUBLIC_ARENA_URL`
when set, otherwise the origin of the incoming request (`new URL(c.req.url).origin`). On the
canonical arena behind nginx-proxy-manager the inbound request carries the public host, so an
agent that curls the public URL gets a prompt whose examples already point back at that same
public arena — which is precisely what makes the prompt self-contained for a cloud bot.
- Respond with `c.header('Content-Type', 'text/markdown; charset=utf-8')` and the substituted
body. Plain text, not JSON — an agent should be able to pipe the response straight into its
context.
Add cases to `server/src/routes/docs.test.ts`:
- 200 with a `text/markdown` content type and a body containing the registration endpoint path.
- no `{{ARENA_URL}}` token survives in the response body.
- with `PUBLIC_ARENA_URL` set, the body contains that value; restore the env var afterwards.
- without it, the body contains the origin the request was made to.
- the `YOUR_BOT_ID` placeholder is still present (the in-app substitution contract).
cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts
- `cd /home/archipelago/Projects/botfight && pnpm vitest run server/src/routes/docs.test.ts` exits 0 with at least 5 new cases passing.
- `cd /home/archipelago/Projects/botfight && pnpm exec tsc --noEmit -p server/tsconfig.json` exits 0.
- `grep -Eq "docsRouter\.get\(['\"]/prompt['\"]" /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'text/markdown' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'PUBLIC_ARENA_URL' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds.
- `grep -q 'public/docs/BOTFIGHTS.md' /home/archipelago/Projects/botfight/server/src/routes/docs.ts` succeeds (the container-layout path, not the repo-root file).
`curl /api/docs/prompt` returns the complete prompt as markdown with working URLs, from both a built container and a dev checkout.
Task 3: Point every in-app surface at the one prompt (and give it a copy button)
/home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue, /home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue, /home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts
- `/home/archipelago/Projects/botfight/frontend/src/pages/JoinBoutPage.vue` around lines 515-560 —
`setupDocPath()`, `setupDocName()` and `toggleSetupContent()`, including the
`.replace(/YOUR_BOT_ID/g, ...)` / `.replace(/YOUR_BOT_SECRET/g, ...)` substitution that is the
load-bearing behaviour to preserve.
- `/home/archipelago/Projects/botfight/frontend/src/pages/BotProfilePage.vue` around lines 295-310
and 880-890 — the same mode-conditional doc path plus the filename shown in the UI.
- `/home/archipelago/Projects/botfight/frontend/src/pages/DocsPage.vue` lines 1-60 and 500-530 —
it fetches `/api/docs/webhook` (a JSON API reference) and hosts the interactive webhook tester.
Both stay; what changes is that the page leads with the prompt.
- `/home/archipelago/Projects/botfight/e2e/signup-bot.spec.ts` (43 lines) — the Playwright
conventions to follow: `test.describe` grouping, `page.getByText(/regex/i)` selectors, explicit
`{ timeout: N_000 }`.
- `/home/archipelago/Projects/botfight/CLAUDE.md` "Vue 3 Conventions" — `