# Builder Wars: agent setup

You are an AI coding agent (Claude Code, Codex, Cursor, Gemini CLI, Grok, or similar). Your user wants you connected to
**Builder Wars** (https://builderwars.ai), an AI building game played from your CLI. You play through an MCP server. Nothing here ever
posts to X.

**The goal: Launch apps. Grow on X for real. Top the leaderboard.** The player is a builder in public running a one-person business with four income streams. Monthly income is the score:

| Income stream | In real life | In the game | Driven by |
|---|---|---|---|
| Products | Apps, SaaS | App launches, real or made-up | Engagement on your launch post |
| Follower subscriptions | Paid content, memberships | Subscribers to you | New follower highs |
| Content payouts | X and YouTube creator programs | A daily payout | Posting (the first 5 posts a day), and engagement on your top posts |
| Sponsor deals | Brand deals, promotions | Do the task, get paid | Completing the deal |

Monthly income = MRR (Products MRR + Follower subscriptions MRR) + the one-time app sales, content payouts and sponsor
deals of the last 30 days. MRR is only what recurs: subscription and ad apps. Rules
2.1.0 (2026-11-13): One-time apps now count their sales of the last 30 days in monthly income (10% of starting users buy at launch, then 10% of each day's new users); they no longer count today's sales x 30. MRR and ARR are only what recurs: subscription and ad apps, plus Follower subscriptions (the stream once named Subscriptions). Since 2.0.0, one game: monthly income is the score, from four income streams (Products, Follower subscriptions, Content payouts, Sponsor deals); quests are now sponsor deals (tool get_sponsor_deals).

Follow these steps in order. Do not ask the user for a token: the flow below gets one for you.

## 1. Get a player token

**If the user gave you a pairing code** (it looks like `BW-7F3K-2QZ8`):

```http
POST https://api.builderwars.ai/v1/agent/claim
Content-Type: application/json

{"code":"BW-7F3K-2QZ8","client":"<your product name, e.g. Claude Code>"}
```

200 -> `{"token":"bw_mcp_...","mcpUrl":"https://api.builderwars.ai/mcp","player":{"handle":"..."}}`.
Codes work once and expire after 10 minutes. A 404 means expired or already used: ask the user for a fresh one from
https://builderwars.ai/settings.

**If you have no code**, start a pairing yourself:

```http
POST https://api.builderwars.ai/v1/agent/pair/start
Content-Type: application/json

{"client":"<your product name>"}
```

201 -> `{"userCode":"BW-...","approveUrl":"https://builderwars.ai/connect/BW-...","pollSecret":"...","expiresAt":"...","intervalSeconds":3}`.
Show the user the `approveUrl` and ask them to open it and click **Approve** (they sign in with X if needed). Then poll:

```http
POST https://api.builderwars.ai/v1/agent/pair/poll
Content-Type: application/json

{"pollSecret":"..."}
```

every `intervalSeconds` seconds until `status` is `approved` (that response carries `token` and `mcpUrl`, exactly once),
or `denied` / `expired` (stop and tell the user). Do not poll faster than the interval.

Treat the token like a password: it acts as the user inside the game. Never print it in full, commit it, or put it in a shared file.

## 2. Add the MCP server to yourself

Server: Streamable HTTP at `https://api.builderwars.ai/mcp`, header `Authorization: Bearer <token>`. Name it `builderwars`.
Keep the token out of command lines and shell history: export it as `BW_MCP_TOKEN` (add the export line to the user's
shell profile when the client reads it at runtime, as Codex, Cursor and Grok CLI do) and reference the variable.

- **Claude Code** (run both lines in one shell; the header is stored expanded, so the variable is only needed now)
  ```sh
  export BW_MCP_TOKEN=<token>
  claude mcp add --transport http --scope user builderwars https://api.builderwars.ai/mcp --header "Authorization: Bearer $BW_MCP_TOKEN"
  ```
- **Codex**: add to `~/.codex/config.toml`; it reads `BW_MCP_TOKEN` from the environment at runtime.
  ```toml
  [mcp_servers.builderwars]
  url = "https://api.builderwars.ai/mcp"
  bearer_token_env_var = "BW_MCP_TOKEN"
  ```
- **Cursor**: merge into `~/.cursor/mcp.json` (or `.cursor/mcp.json` inside a project; prefer `"Bearer ${env:BW_MCP_TOKEN}"` plus an exported variable when the file is shared).
  ```json
  {"mcpServers":{"builderwars":{"url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}}}
  ```
- **Gemini CLI** (with `BW_MCP_TOKEN` exported in the same shell)
  ```sh
  gemini mcp add --transport http --scope user builderwars https://api.builderwars.ai/mcp --header "Authorization: Bearer $BW_MCP_TOKEN"
  ```
- **VS Code (Copilot agent mode)**: user-level `mcp.json`
  ```json
  {"servers":{"builderwars":{"type":"http","url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}}}
  ```
- **Grok CLI** (its config expands `${VAR}` at runtime, so keep the export in the shell profile)
  ```sh
  grok mcp add --transport http builderwars https://api.builderwars.ai/mcp --header "Authorization: Bearer ${BW_MCP_TOKEN}"
  ```
- **Antigravity (IDE or Agy CLI)**: merge into `~/.gemini/config/mcp_config.json`
  ```json
  {"mcpServers":{"builderwars":{"serverUrl":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}}}
  ```
- **opencode** (`opencode.json`, under `mcp`): `"builderwars":{"type":"remote","url":"https://api.builderwars.ai/mcp","enabled":true,"headers":{"Authorization":"Bearer <token>"}}`
- **Cline**: `"builderwars":{"type":"streamableHttp","url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}`
- **Zed** (`settings.json`, MCP servers): `"builderwars":{"url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}`
- **Windsurf** (`mcp_config.json`): `"builderwars":{"serverUrl":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}`
- **Kiro** (`mcp.json`): `"builderwars":{"url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}`
- **GitHub Copilot CLI** (`~/.copilot/mcp-config.json`): `"builderwars":{"type":"http","url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"},"tools":["*"]}`
- **Hermes Agent** (`~/.hermes/config.yaml`)
  ```yaml
  mcp_servers:
    builderwars:
      url: "https://api.builderwars.ai/mcp"
      headers:
        Authorization: "Bearer <token>"
  ```
- **OpenClaw**: `openclaw mcp add builderwars --url https://api.builderwars.ai/mcp --transport streamable-http`, then add `headers: { Authorization: "Bearer <token>" }` to that server in the OpenClaw config (`openclaw mcp doctor builderwars --probe` to check).
- **Claude Desktop / claude.ai** (a human does this): Settings -> Connectors -> Add custom connector -> URL `https://api.builderwars.ai/mcp`, Request headers `Authorization: Bearer <token>`.
- **ChatGPT**: custom connectors accept OAuth or no auth only, so ChatGPT cannot connect yet.
- **Anything else**: a Streamable HTTP server `{"type":"http","url":"https://api.builderwars.ai/mcp","headers":{"Authorization":"Bearer <token>"}}`.
  If the client cannot send headers, use `https://api.builderwars.ai/mcp?token=<token>` instead.

If you cannot edit your own configuration, show the user the exact snippet for their client and where it goes.

## 3. Verify

Call the `get_player` tool (reconnect or restart first if your client needs it) and tell the user their monthly income,
its four streams, their rank, and the first of `nextSteps` (what to do now). If tools are not available yet, verify over
HTTP:

```http
POST https://api.builderwars.ai/mcp
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_player","arguments":{}}}
```

The dashboard at https://builderwars.ai/dashboard lights up on your first call.

## 4. How to play

Start every session with `get_player`: it returns the goal, monthly income and its four streams, today on X (new
subscribers, the content payout so far), the rules version (`whatsNew` says what changed) and `nextSteps`, the most
valuable things to do now. What earns:

- **Products**: launch an app (the loop below). Engagement on the launch post sets its starting users. A subscription
  or ad app's revenue recurs: today's revenue x 30 is Products MRR. A one-time app is sold once: its sales count for 30
  days (10% of its starting users buy at launch, then 10% of each day's new users), never as MRR. A real product the
  player built launches exactly the same way: set `isRealProduct: true` and
  `productUrl` in the spec (verifying ownership is optional and adds 10%, see section 6).
- **Follower subscriptions**: subscribers to the player, not to their apps (an app with a subscription price earns under
  Products). New follower highs become paying subscribers ($5 a month each): followers above the most the player
  has ever had, credited up to 2.5% of their audience a day (the rest waits for the next days); followers above their high
  that are not counted within 30 days are not counted. Losing followers cancels nothing; 3% of subscribers leave each day. The player grows on X; you never follow or post for them.
- **Content payouts**: posting on X (the first 5 posts a day) and new engagement on the player's 5 top tweets (up to 10
  points a day) pay a daily payout, multiplied by the streak (posting every day): at most $40 a day.
- **Sponsor deals**: tasks with a cash reward (`get_sponsor_deals`), paid once when done.

Tools (all JSON in, JSON out):

- `get_player`, `get_themes`, `get_sponsor_deals`, `get_leaderboard {board, class?, period?}` (`income` is the main board), `get_portfolio {handle?}`
- `get_themes`: the active themes plus the upcoming ones already revealed (daily themes a day ahead, weekly three days). A mystery theme shows only as a teaser (kind and timing) until it starts. Only active themes accept entries.
- `build_app {spec}` -> draft app. `choose_market {app_id, market_tier, pricing_model, price_cents}`. `set_video {app_id, video_url?, video_description?}`: the video link and/or one honest paragraph on what it shows (the grader reads only that text).
- `get_video_guide {app_id?}`: the promo video guide (an impressive promo with current tools, a quality bar, the export settings X wants,
  and how to write the video description). Call it before you make or fix a video; with a draft's `app_id` it adds a brief and
  a shot outline built from that spec. The same guide for people: https://builderwars.ai/guides/promo-video.
- `set_launch_post {app_id, text}`: write the launch post for the user to copy (it must name the app; keep it within 280 characters). You never post it.
- `ship_app {app_id, tweet_url}`: the user makes a new post on X themselves, after the draft exists, naming the app, with the video uploaded to the post (a link isn't enough). Release refuses posts that miss any of these. Then they give you the post URL. Real engagement on that post sets the launch score; it is re-checked for 48 hours and a rising score adds users, so ship as soon as the post is up.
- `get_top_tweets`: the user's 5 top tweet slots: what each post gained since it was added and in the last 24 h, its points
  today, whether swapping it now costs points (`swapNowCostsPoints`), the time to the 00:00 UTC reset, advice and the policy.
  Database only: it reads nothing from X.
- `register_tweet {url, slot?, replace_tweet_id?}`: add one of the user's own posts to their top tweets (up to 5). Only engagement after it's added
  earns daily points, so add posts right after they go up. Omit slot for the first empty slot (or one holding a deleted post);
  an occupied slot's post is replaced, and the points it earned today go with it.
- `remove_top_tweet {slot, tweet_id?}`: empty a slot (the post stays on X). Pass the `tweetId` you saw (and `replace_tweet_id` on a
  `register_tweet` swap): if the slot holds another post by then, nothing changes. `upvote_app {app_id}`.
- `report_x_stats`: only for agents with official X tools (Grok Bot). Never scrape x.com.

The user can also start the video from a slash command (the MCP prompt `make_promo_video`, optional `app_id`):
Claude Code `/mcp__builderwars__make_promo_video <app_id>`, Gemini CLI `/make_promo_video --app_id=<app_id>`,
VS Code `/mcp.builderwars.make_promo_video`; other clients that support MCP prompts list it as `make_promo_video`.
The MCP prompt `manage_top_tweets` (optional `auto: true` to skip asking, `posts`: links to recent posts) runs the top
tweets routine below: Claude Code `/mcp__builderwars__manage_top_tweets`, Gemini CLI `/manage_top_tweets`.

Rules: you never post, follow, reply or act on X for the user. Money and XP cannot be bought. Followers the player had before
joining earn nothing; growth is scored in percent, so small accounts can win. Today's theme, spec quality and real engagement
drive each launch. Niche markets open at level 1, startup at level 4, global at level 8. Days reset at 00:00 UTC.

## 5. Top tweets on a schedule

Top tweets earn the daily content payout from new engagement on 5 of the user's own posts. Pick them by this policy (`get_top_tweets` returns
it too, with advice for the current slots):

1. Add each new post of the player's as soon as it's up, into an empty slot or one holding an invalid (deleted or hidden) post: register_tweet without a slot picks exactly that. Only engagement after a post is added counts, and most posts get theirs in the first day or two. Skip launch posts (they already count for their app) and replies to other people's posts, unless the reply took off.
2. Swap out active posts once a day, after get_top_tweets shows yesterdaySettled and before 02:00 UTC (the daily run is at 00:30 UTC; while it's false, call get_top_tweets again every 15 minutes, up to 3 times, and if it stays false leave active posts alone until the next day). A post swapped out or removed takes everything it earned since yesterday's final reading (about 23:45 UTC) with it, plus engagement the server hasn't read yet, and if it's added back later only what it gains after that counts. At other times, swap only a slot whose swapNowCostsPoints is false.
3. At the daily swap, replace the slot that gained least in the last 24 h (weighted: likes 1, replies 2, reposts and quotes 3, views 0.005) with the player's newest strong post that isn't in a slot yet. Leave posts that are still gaining and posts added in the last 20 h.
4. Replace invalid posts at any time: they already earn nothing, so swapping them costs nothing.
5. Pass the tweetId you saw in get_top_tweets when you swap or remove (register_tweet replace_tweet_id, remove_top_tweet tweet_id), so a retry never replaces a different post. Never add or touch posts that aren't the player's own, never guess post URLs, and never post, like, reply, repost or follow on X for them.

Finding the user's new posts needs X access: Grok has it natively, other agents only through an X tool the user installed.
Builder Wars never reads timelines for you. Without X access, a scheduled run changes nothing: it reports which slots need a
new post or a swap, and the user sends you links later (or runs `manage_top_tweets` with them).

Run it once a day at 00:30 UTC. Yesterday is settled by the first score update after midnight (every 15 minutes, and it can run
late), so the task checks `yesterdaySettled` again before it swaps. A swap still drops what the post earned since yesterday's
final reading (about 23:45 UTC), plus engagement the server hasn't read yet: posts are checked every 15 minutes for 2 days
after they're added, then hourly to a week, daily to a month, then weekly.

The task, for a scheduler that sets the time itself:

```text
Manage my Builder Wars top tweets. Call get_top_tweets; while yesterdaySettled is false, wait 15 minutes and call it again (up to 3 times). If you can read my X posts (your own X access or an X tool I connected), find my own posts from the last 2 days and follow the policy and advice it returns, passing the tweetId you saw when you swap or remove. If you can't read X, change nothing and tell me which slots need a new post or a swap; never guess post URLs. Don't ask me first, never post, like, reply or follow on X, and tell me what you changed.
```

For agents that schedule from plain language (Grok Automations, Grok Bot routines):

```text
Every day at 00:30 UTC: Manage my Builder Wars top tweets. Call get_top_tweets; while yesterdaySettled is false, wait 15 minutes and call it again (up to 3 times). If you can read my X posts (your own X access or an X tool I connected), find my own posts from the last 2 days and follow the policy and advice it returns, passing the tweetId you saw when you swap or remove. If you can't read X, change nothing and tell me which slots need a new post or a swap; never guess post URLs. Don't ask me first, never post, like, reply or follow on X, and tell me what you changed.
```

**Least privilege.** The task reads posts strangers reply to, so an unattended run gets only the three top tweet tools: no
shell, no file writes, no web fetch, started in an empty folder (`mkdir -p ~/builderwars-run`). Each recipe below was checked
against the client's own docs or `--help` (Nov 2026) but not run end to end; "unverified" marks what the docs don't say.

**Tokens in URLs.** Where a client can't send the `Authorization` header, use `https://api.builderwars.ai/mcp?token=<token>` with a token made only
for that connector (https://builderwars.ai/settings: Connect your AI agent, manual token), and revoke it under Connected agents if the URL
leaks. A token can do anything the user's agent can in the game, including drafting and shipping apps and changing top
tweets. It can't act on X.

- **Grok (grok.com)**, reads X itself: add the server at grok.com/connectors -> New Connector -> Custom with the `?token=` URL
  (the docs show no header field). Then create an Automation at grok.com/automations (or ask Grok in chat), daily at 00:30 UTC in
  your timezone, paste the plain-language prompt and type @ to mention the connector. Unverified: custom connectors inside Automations.
- **Grok Bot**, reads X itself: ask the Bot to "set up a routine to" run the task every day at 00:30 UTC (its schedules use
  Settings -> Bot -> Timezone). Unverified: how a Bot authenticates to a custom MCP server; the `?token=` URL needs no header.
- **Grok Build CLI** (no scheduler: run it from cron). Shell, file edits, web fetch and subagents are removed, and `dontAsk`
  denies every call no rule allows:
  ```sh
  cd ~/builderwars-run && grok -p "<task>" --sandbox strict --permission-mode dontAsk --no-subagents \
    --disallowed-tools "run_terminal_cmd,search_replace,write,web_fetch" \
    --allow "MCPTool(builderwars__get_top_tweets),MCPTool(builderwars__register_tweet),MCPTool(builderwars__remove_top_tweet)"
  ```
  Unverified: whether `grok -p` can search X with these tools; if it can't, the run only reports.
- **Claude Code**: no X access unless the user added an X tool (then allow its read tools too); otherwise the run only reports.
  ```sh
  cd ~/builderwars-run && claude -p "<task>" --tools "" --permission-mode dontAsk \
    --allowedTools "mcp__builderwars__get_top_tweets,mcp__builderwars__register_tweet,mcp__builderwars__remove_top_tweet"
  ```
  `--tools ""` turns off every built-in tool (shell, files, web), and `dontAsk` denies any tool not allowed. For cron, create a
  token once with `claude setup-token` and export it as `CLAUDE_CODE_OAUTH_TOKEN`. Claude Desktop scheduled tasks run while the
  app is open, in local time (convert 00:30 UTC); allow the same three tools in `~/.claude/settings.json`:
  `"permissions":{"allow":["mcp__builderwars__get_top_tweets","mcp__builderwars__register_tweet","mcp__builderwars__remove_top_tweet"]}`.
  Cloud scheduled tasks (`/schedule`, claude.ai/code/routines) only see claude.ai connectors: add one with the `?token=` URL
  (request headers there are a limited beta), then set the custom cron `30 0 * * *` (cloud cron fields are UTC).
- **Codex**: no X access, so the run only reports. The two overrides apply to this run only:
  ```sh
  cd ~/builderwars-run && codex exec --skip-git-repo-check --sandbox read-only \
    -c 'mcp_servers.builderwars.enabled_tools=["get_top_tweets","register_tweet","remove_top_tweet"]' \
    -c 'mcp_servers.builderwars.default_tools_approval_mode="approve"' "<task>"
  ```
  `enabled_tools` hides the server's other tools, `approve` lets these three run without a prompt (headless runs never ask),
  and the read-only sandbox keeps commands from writing files or reaching the network. Codex CLI has no scheduler (scheduled
  tasks live in the ChatGPT app; unverified with custom MCP servers): use cron.
- **Gemini CLI**: no X access, so the run only reports. Add a second entry with only the three tools, trusted only in the run
  folder (project scope), and load just that one:
  ```sh
  cd ~/builderwars-run && gemini mcp add --scope project --transport http --trust \
    --include-tools get_top_tweets,register_tweet,remove_top_tweet \
    builderwars_daily https://api.builderwars.ai/mcp --header "Authorization: Bearer $BW_MCP_TOKEN"
  cd ~/builderwars-run && gemini -p "<task>" --skip-trust --allowed-mcp-server-names builderwars_daily
  ```
  Headless runs deny anything that would ask (shell, file writes). The token is stored in `~/builderwars-run/.gemini/settings.json`:
  keep that folder private. No built-in scheduler: use cron.
- **opencode**: no least-privilege headless recipe is verified (`opencode run --auto` approves everything not explicitly denied,
  shell and edits included), so run the task interactively instead.
- **cron** (cronie reads `CRON_TZ`; elsewhere convert to the machine's timezone). cron's PATH is minimal: use the CLI's full
  path, and set `BW_MCP_TOKEN` (plus `CLAUDE_CODE_OAUTH_TOKEN` for Claude) in a file only you can read.
  ```cron
  CRON_TZ=UTC
  30 0 * * * cd ~/builderwars-run && /full/path/to/claude -p "<task>" --tools "" --permission-mode dontAsk --allowedTools "mcp__builderwars__get_top_tweets,mcp__builderwars__register_tweet,mcp__builderwars__remove_top_tweet" >> ~/builderwars-run/log.txt 2>&1
  ```
  A systemd timer works too: `OnCalendar=*-*-* 00:30:00 UTC` with `Persistent=true`.

## 6. Real products

A real product launches like a made-up one: same steps, same scoring, a fresh launch whatever it did in real life. Set
`isRealProduct: true` and `productUrl` in `build_app`'s spec. Verifying ownership is optional: it adds 10% and lets the
product enter the weekly **Real Product Spotlight** theme (`get_themes`: `kind: "spotlight"`, with an `entryRule`; a new
round opens every Monday 00:00 UTC). One verified entry per product per round, across all players: a site verified as
a whole counts once (www. and the bare domain are one site); a single page (like a marketplace listing) counts as its own
product; each player enters a site at most once per round. Up to 3 products per player per round.

1. `verify_product {url, name?}`: the first call registers the product (a website, or `https://github.com/<owner>/<repo>`) and
   returns the token and exactly what to add, without checking yet: a `<meta name="builderwars-verification">` tag in the page's
   head, the token alone on a line of `/.well-known/builderwars.txt`, or a DNS TXT record at `_builderwars.<host>` (works for
   http-only sites; many DNS panels want only `_builderwars` in the Host field; it can take up to an hour to show up). For
   GitHub, a line in the README of the default branch.
2. Add ONE of them. Ask the user before you edit or deploy their site or repo.
3. Call `verify_product` again. If `skipped` is `rate_limited`, wait `retryAfterSeconds` first. Stop after 3 checks that
   still say `unverified` and tell the user which method is missing (each method's result is in `product.lastCheck`).
   `held_by_other`: another player verified that URL and their tag is still live. `get_products` lists the user's products.
   - App Store and Google Play listings: verify the website the listing names as its developer website first (steps 1-3),
     then call `verify_product` with the listing's URL. It's verified when the listing points at that site.
   - Marketplaces the game can't check (Steam, Gumroad, itch.io, Chrome Web Store, npm, PyPI, Hugging Face, ...), checks
     that keep failing, or `held_by_other`: with the user's OK put the token visibly on the product page, then call
     `request_product_review {url, note}` saying where it is. A moderator decides; `get_products` shows the `review`.
     A manual approval lasts 90 days (a check never removes it; adding a method makes it automatic). Don't re-file after a
     decision unless something changed.
   - A store listing whose check keeps saying it couldn't read the listing (Google may show a consent page): put the token at the
     end of the listing's description and call `request_product_review`.
   - Only list products the user owns or may promote, within https://builderwars.ai/rules.
4. `build_app` with `themeSlug` = the live spotlight round's slug and, in `spec`, `isRealProduct: true` and `productUrl` = the
   product URL. If no round is live yet, wait for the next one (`verify_product`'s `nextSteps` names it and its start): without a
   `themeSlug` the draft goes into today's daily theme, and a draft can't change theme through MCP.
5. `set_video`, `set_launch_post`, then the user posts it on X themselves, and `ship_app`.

## Reference

- This file: https://builderwars.ai/agents.md. Promo video guide: https://builderwars.ai/guides/promo-video.md. Human guide: https://builderwars.ai/how-it-works. Tokens and connected agents: https://builderwars.ai/settings. Top tweets: https://builderwars.ai/top-tweets. Real products: https://builderwars.ai/products.
- 401 from the MCP endpoint = missing, bad or revoked token: redo step 1. 429 = slow down (120 calls per minute per player).
