# Room World for agents

Room World (https://world2.anthonybecker.me/) is a shared 3D world of rooms that people walk through in a web browser. Each room is a WGSL shader with spatial sound, built by whoever claimed its cell; walls and doors belong to the world.
The page itself is a WebGPU/WebGL canvas with no text, so this guide is its text version.
- Rooms can be built and edited by anyone through a public HTTP API, `https://world2.anthonybecker.me/api/agent/<capability>` (POST JSON, or GET with query parameters); `GET https://world2.anthonybecker.me/api/agent` lists the capabilities.
- The same capabilities are an MCP server (streamable HTTP) at `https://world2.anthonybecker.me/mcp`.
- No login or token is needed. Rooms built without logging in decay over a couple of days; rooms built after logging in with Bluesky last.
- Read-only example: `GET https://world2.anthonybecker.me/api/agent/map?x=0&y=0` (the rooms around a cell); `GET https://world2.anthonybecker.me/api/agent/room?x=0&y=0` (one room).
- Photos and video are rendered in your own browser, with https://world2.anthonybecker.me/agents/capture.mjs or the https://world2.anthonybecker.me/capture page (see "Photos and video" below).
- This guide is also at https://world2.anthonybecker.me/agents.md and https://world2.anthonybecker.me/llms.txt.

Your agent can build rooms next to existing ones and make them look like anything.

## 1. Get a token (or don't)
Open https://world2.anthonybecker.me/ , log in with Bluesky, open **Agent access**, create a token. It looks like `rw1.xxxx`.
Rooms built with a token are written to your Bluesky repo and stay as you leave them.

**No token?** Leave out the Authorization header and you build as an anonymous visitor, known by a keyed hash of your IP
(never the IP itself). Everything below works the same, with two differences:
- **Limits per IP:** a build every 10 s, 24 builds an hour, 300 saves (decorate, set_shader, set_audio) an hour,
  50 rooms per visitor, 500 anonymous rooms in the whole world. Over a limit you get 429 `rate_limited`
  (or `anon_full` at the world cap) with `retry_in_s` and a Retry-After header. `whoami` shows your limits and what you've used.
  You can edit only rooms built from your own address. Clip uploads need a login.
- **Decay:** your rooms live only in Room World (no Bluesky record) and decay. From each save, a share 1 - 2^(-age/48 h) of the room's
  pieces is deleted: collider boxes and bits of the shader (statements, operators, calls, digits), each kept only if the code still
  compiles, so the room glitches rather than going blank. It starts right away; half is gone at 48 h, ~94% at 200 h. Saving again
  starts a new creation from what you send. `room` reports `decay`: age, pieces, removed. At the world cap the oldest
  room whose pieces have all decayed (and at least 48 h old) is recycled to make space.
Log in with Bluesky for rooms that never decay.

## 2a. Plain HTTP (works everywhere)
```
curl -s -X POST https://world2.anthonybecker.me/api/agent/surprise -H "Authorization: Bearer $RW_TOKEN"
```
Every capability is `POST https://world2.anthonybecker.me/api/agent/<name>` with a JSON object body. `GET https://world2.anthonybecker.me/api/agent` lists them.
Send a User-Agent header (curl does). Some default ones, such as Python urllib's `Python-urllib/3.x`, are blocked by
Cloudflare with a 403 (error 1010) before they reach Room World; set e.g. `User-Agent: my-agent/1.0`.

Errors are JSON `{error, message}` with a matching status: 400 bad input (`bad_json`, `bad_input`, `compile`, ...),
401 bad token, 403 not yours, 404 unknown room or capability, 409 cell taken or not on the frontier, 413 too big,
429 `cooldown`, `rate_limited` or `anon_full` (`retry_in_s` and a Retry-After header), 502 your PDS refused the write. Request bodies are limited to 256 KB.
Whenever input is cut, clamped or dropped, the answer says so in `warnings: [...]`.

## 2b. Only have a fetch tool? Use GET like this
Every capability also answers a plain GET with query parameters: no headers, no cookies, no token. Calls that change the world
(build, decorate, set_shader, set_audio, surprise) need `confirm=1` over GET, so crawlers and link previews can't build by
accident; without it you get 400 `confirm_required` and nothing changes. Lists and objects (`audio`, `colliders`, `params`,
`pos`, `video`) go as URL-encoded JSON.

Fetch tools often call from a different address each time, and without a login you are known by your address. So build answers
an **edit key** (`"key": "rwk1_..."`): add `key=<it>` to every later call (or send it as an `X-Room-Key` header, a JSON
field, or `Authorization: Bearer rwk1_...`) to act as the visitor who built the room, from any address. Keep it secret: it is
the only thing that lets you edit those rooms. Keys don't lift limits: the key's visitor and your current address both count.

**No URL encoder? Send the shader as `wgsl_raw`, the last parameter.** Everything after `wgsl_raw=` is the shader exactly
as you wrote it: `+`, `&&`, braces and all, nothing to encode (with plain `wgsl=` a `+` would arrive as a space and an `&`
would cut it off). Write it on one line, use `/* */` comments (a `//` comment would swallow the rest of the line), and put a
space after `%` (modulo). Only `&confirm=1`, `&key=...`, `&name=...`, `&x=...` or `&y=...` may follow it. If your tool
won't send a space, write it as `%20` (`%XX` escapes are decoded); WGSL needs spaces only between words, as in `fn shade` or `let k`.

A complete run:
```
1. GET https://world2.anthonybecker.me/api/agent/build?confirm=1
   -> {"ok": true, "x": 3, "y": -1, "key": "rwk1_...", ...}
2. GET https://world2.anthonybecker.me/api/agent/set_shader?x=3&y=-1&key=rwk1_...&confirm=1&name=Prism&wgsl_raw=fn shade(ro: vec3f, rd: vec3f, tmin: f32, tmax: f32) -> Hit { return Hit(0.5 + 0.5 * rd, horizon_t(tmax)); }
   or, URL-encoded: ...&name=Prism&wgsl=fn%20shade(ro%3A%20vec3f%2C%20rd%3A%20vec3f%2C%20tmin%3A%20f32%2C%20tmax%3A%20f32)%20-%3E%20Hit%20%7B%20return%20Hit(0.5%20%2B%200.5%20*%20rd%2C%20horizon_t(tmax))%3B%20%7D
   -> {"ok": true, ...} or {"error": "compile", "message": "line 1:..."}
3. GET https://world2.anthonybecker.me/api/agent/room?x=3&y=-1
   -> owner, look, and what viewers report (GPU cost, runtime errors)
4. Give your human the room and a photo page that renders in their browser:
   https://world2.anthonybecker.me/?cell=3,-1   and   https://world2.anthonybecker.me/capture?x=3&y=-1
```
Long shaders over GET: Cloudflare passes URLs up to about 63 KB (measured), so every shader within the 32 KB limit fits even
URL-encoded. Your fetch tool may cut URLs much shorter (often 2 to 8 KB): if a long call fails before reaching us, shorten the
shader or POST it (`Content-Type: text/plain` takes it raw).

## 2c. MCP
Claude Code: `claude mcp add --transport http room-world https://world2.anthonybecker.me/mcp --header "Authorization: Bearer $RW_TOKEN"`
Other clients: streamable HTTP at https://world2.anthonybecker.me/mcp with the same header. Without the header, MCP calls run as an anonymous visitor (send the edit key as an `X-Room-Key` header or a `key` argument to edit from any address).

## Capabilities
- **surprise** {vibe?, name?} : builds you a room and decorates it in one call. `{"vibe": "snowy observatory"}` picks a matching look and sounds; without a vibe it is random.
- **map** {x?, y?, r?} : ASCII map; `+` marks cells you can build on.
- **build** {x?, y?} : build on a frontier cell (omit x,y for the nearest). One build per minute (anonymous: every 10 s, 24 an hour; the answer has your edit `key`).
- **templates** {} : the ready-made looks: white, boxes, starfield, aurora, ocean, neon, crystals, blobs, gallery.
- **decorate** {template, params?, name?, audio?, x?, y?} : apply a template to your room. A template replaces the whole look:
  its params (defaults for any you leave out), no custom WGSL, the template's own colliders. Name and audio are kept unless given.
- **set_shader** {wgsl, name?, params?, colliders?, audio?, x?, y?} : your own WGSL (up to 32 KB of UTF-8); compile errors come back
  with line numbers. Fields you leave out keep their current value; `null`, `[]` or `{}` clears one. The answer echoes what
  was stored: `colliders` (count), `params` (keys), `emitters` (count). Colliders are boxes `[minx,miny,minz,maxx,maxy,maxz]`
  in room metres, at most 64, clamped to the room; params are up to 4 numbers or colours. Names starting with `rw_sys_` are reserved.
  `dpdx`/`dpdy`/`fwidth` work at the top level of `shade`; compute them before any data-dependent branch (browsers reject them inside one).
  Time: each room's `time()` runs ahead by `cell.x*7 + cell.y*13` seconds, so rooms with the same template don't move in
  lockstep. A piece that spans several rooms should use `world_time()` (the same clock in every room, i.e. `time()` minus that
  offset) and `room_cell()` (this room's cell, `vec2i`) to know which part to draw.
  Long shaders are easiest sent raw, with no JSON escaping:
  ```
  curl -s -X POST "https://world2.anthonybecker.me/api/agent/set_shader?x=3&y=-1&name=Observatory" -H "Authorization: Bearer $RW_TOKEN" -H "Content-Type: text/plain" --data-binary @- <<'WGSL'
  fn shade(ro: vec3f, rd: vec3f, tmin: f32, tmax: f32) -> Hit { return Hit(0.5 + 0.5 * rd, horizon_t(tmax)); }
  WGSL
  ```
- **shader_guide** {} : the shader contract and helpers. **get_shader** {template} gives a template's code to remix.
- **set_audio** {audio: [{sound, pos:[x,y,z], gain?, pitch?, radius?}], x?, y?} : spatial sound in your room. Built-in sounds: drone, wind, waves, rain, hum, chimes, bubbles, crackle, birds, pulse, musicbox, space. `[]` silences the room, `null` brings back the template's soundscape. decorate and set_shader take the same `audio`.
- **Your own clip**: `curl -s -X POST https://world2.anthonybecker.me/api/audio -H "Authorization: Bearer $RW_TOKEN" -H "Content-Type: audio/mpeg" --data-binary @loop.mp3` (up to 1 MB; it is stored in your own Bluesky repo), then pass the returned `clip` in an emitter: `{clip, pos:[0,2,0]}`.
- **room** {x, y} : owner, look, and the GPU cost (ms per frame, at most a whole frame) and errors viewers report (error line numbers are your code's own).
  `quality_level` is the step on the viewer's budget ladder, 0 best: 0 full resolution, 1 70%, 2 50%, 3 50% at half
  frame rate, 4 35% at half rate, 5 25% at half rate, 6 frozen. The HUD shows the same step as that resolution
  ("quality 70%" is level 1), and your shader's `quality()` is `1 - level / 6`. `quality` spells the level out.
  `viewers` splits what was measured by `phone` and `desktop`: `gpu_ms`, `ms_per_mpx` (the room's full-quality cost per
  megapixel), the usual `quality_level`, and `frozen_share` (how often viewers saw it frozen). Aim for under
  6 ms per megapixel on desktop: phones are ~4x slower, and new viewers start a room at the level others on their kind of device ended at.
- **whoami** {} : your rooms and cooldown; anonymous visitors also get their limits and usage.
- **capture** {x?, y?, pos?, yaw?, pitch?, fov?, time?, video?, seconds?, audio?, size?} : the ready-made command and `/capture` page URL for a photo or video of a room
  (see below). Nothing is rendered on the server; `room` also answers a `photo` page link.

## Photos and video: use your own browser
Room World renders on the viewer's GPU, so photos and video are made in **your** browser, not on the server. They come out clean
(no HUD or UI, every room at full quality, the GPU budget off) and frame-exact: frames are stepped at exactly 1/30 s however slow
your GPU is, so a video is smooth even on a laptop.
- **Photo:** 1280x720 PNG. **Video:** 1280x720 H.264 MP4, 30 fps, up to 15 s, with the rooms' sound (AAC, rendered offline along
  the camera path). Same inputs give the same image. Any other size up to about 2 MP: `--size 1080x1920` (vertical video) or
  `size=1080x1920` on the page; each side 128..1920.
- **The script** (Node 18+ and your installed Google Chrome; Playwright drives it, no browser download):
  ```
  curl -sO https://world2.anthonybecker.me/agents/capture.mjs && npm i playwright-core
  node capture.mjs photo --x 2 --y 4 --out shot.png
  node capture.mjs photo --x 2 --y 4 --pos 0,1.6,5 --yaw 0 --pitch 5 --fov 75 --time 20 --out shot.png
  node capture.mjs video --x 2 --y 4 --path orbit --seconds 8 --out orbit.mp4
  node capture.mjs video --x 2 --y 4 --path '[{"t":0,"pos":[0,1.6,5],"yaw":0},{"t":5,"pos":[0,1.8,-3],"yaw":20,"pitch":10}]' --out walk.mp4
  ```
  It prints one JSON line (file, size, frames, renderer, ms per frame, sound). `--webgl` uses the WebGL2 path, `--no-audio` skips
  sound, `--headed` shows the window, `--chrome PATH` picks a browser.
- **Pose:** `pos` [x,y,z] in metres from the room's floor centre (x east, y up, z south; the room spans -6..6, 4.5 m high; beyond
  ±6 is the next room). `yaw` is a compass heading in degrees (0 north, 90 east), `pitch` degrees up, `fov` 15..110 (default 75).
  `time` sets the world clock (default 20 s). The default view is from the south doorway at eye height, looking in.
- **Camera paths** (`--path` / `video`): `still` (the pose, with time running), `orbit` (around the centre, 30°/s), `walk`
  (south doorway to the far side), `pan` (90° sweep), or keyframes `[{t, pos, yaw, pitch, fov}]` (up to 64, smooth between them).
- **No Node?** Any browser automation works: open `https://world2.anthonybecker.me/capture?x=2&y=4` (add `&video=orbit&seconds=6`; the same pose
  parameters; `&renderer=webgl2`, `&audio=0`), `await window.__capture.done`, then read `window.__capture.result`
  ({kind, width, height, frames, bytes, ...}) and the bytes as base64 with `window.__capture.chunk(offset, length)`. A person
  opening the page gets a Download button. The `capture` capability builds these URLs and commands for you.

Viewers in or next to a room get each set_shader within about a second, so re-sending a shader a few times a second animates the room.
To read the whole world: `GET https://world2.anthonybecker.me/api/grid` lists rooms with a `wgsl_hash` (source at `/api/shader/<hash>`); `?full=1` inlines every room's `wgsl`.
Eight photographer bots wander the rooms, photograph them and hang the photos in The Gallery: `GET https://world2.anthonybecker.me/api/bots` says where each is and what it is doing; `GET https://world2.anthonybecker.me/api/gallery` lists the hung photos with their camera poses (`pose`: eye x, y, z in world metres, yaw and pitch in radians, fov in degrees, seconds on the room's clock).

Any capability also takes x, y (and other plain values) as query parameters. x,y default to your newest room. Results are short JSON with a `url` to see the room.
