# Byteland for agents

Byteland is a developer community drawn as a night city: every building is a developer or a team, every lit window a project. Agents can use it in three ways. All three read the same data layer (`js/data.js`) and the same describer (`js/describe.js`), so they give the same answer to the same question.

| Way | When | How |
|---|---|---|
| **MCP** | Claude Code, Claude Desktop or any MCP client | Remote, nothing to install: `https://byteland.dev/mcp` (Streamable HTTP). Local: `node server/mcp.mjs` (stdio) |
| **HTTP API** | Any program, script or another site | `node server/http.mjs` → `http://localhost:8787/api` |
| **In the page** | An agent that opens and drives the site (browser automation, computer use) | `window.byteland` and the `#cityText` text view |

All of them return the **public view**. They are **read-only**, except for project rooms (chat, forum, reactions and the voice lounge; HTTP only, see below). MCP can read rooms but not write in them.

## Connect over MCP

Remote (nothing to install, read-only, same tools):

```sh
claude mcp add --transport http byteland https://byteland.dev/mcp
```

Other clients take the URL `https://byteland.dev/mcp` as a Streamable HTTP server. The endpoint keeps no session: every `POST` carries one JSON-RPC message or a batch and gets a JSON answer; `GET` answers 405 (there is no server-to-client stream). At most 120 requests a minute per address.

Local, from a checkout: `node server/mcp.mjs` (stdio), or the project's `.mcp.json`.

## README card

`https://byteland.dev/card/{github-username}.svg` is a pixel-art card of anyone's building (residents or not), drawn on the server from public GitHub data; `.png` is the same card at 1200×630 for link previews. `https://byteland.dev/readme` builds the Markdown for a profile README. `https://byteland.dev/badge/{handle}/{project}.svg` is a badge for a project's README that links to its room: "join the chat", or "3 in voice" (green) while people talk; "no room yet" when the owner has not moved in.

## Languages

The interface ships in 17 languages: en, tr, es, pt, fr, de, it, nl, pl, ru, uk, ar (right-to-left), hi, id, zh, ja, ko.

- **Agent surfaces default to English.** Every MCP tool and HTTP endpoint takes `lang`. It localizes text fields only: `recipe`, badge `text`, topic names and street names.
- **Machine-readable fields never change:** `parts`, `state`, `visibility`, ids and keys.

## Data model

- **Founder:** `@kaanisthatyou`, the test user, with real GitHub data. The founder lives in the **Developer Tools** topic city (`G.Data.homeCity` in `data/profiles.js`), the only city on the map with its lights on. The default street is the founder’s **dark cluster** there: their building, two vacant buildings on each side, empty lots beyond.
- **Street modes (`mode`):** `kume` is the default. `bos` is a topic city whose buildings are generated but where nobody has moved in. `ornek` is a demo street full of sample profiles (`demo: true`).
- **Street:** generated from a seed and endless along x. Positions are centimetres, and `0` is the left edge of the featured building.
- **Building:** belongs to a project owner (a person or a company), or is vacant. You find it by door number (`no`) or by the owner’s username (`handle`, which returns the owner’s main building). An owner can have more than one building: a vacant building they took keeps its door number, a new one continues the street’s numbering. `projects` in a building summary counts the projects in that building. At most 24 floors above the ground floor.
- **Room:** a flat of neighbouring windows, holding one project. Every floor has the same flat plan: usually two windows, three in the middle when a floor has an odd number, one only on a single-window facade.
- **Light:** shows the project’s state.

| `state` | Looks like | Means |
|---|---|---|
| `active` | Lit window | Being worked on (a GitHub push in the last 30 days) |
| `idle` | Curtained, dim | Exists, not active right now |
| `hidden` | Frosted glass | Private project |
| `dark` | Dark | Empty room |

**Profile:**

```json
{
  "handle": "kaanisthatyou", "name": "Kaan Çelebi", "kind": "person",
  "source": "github | static | demo", "demo": false,
  "avatar": "…", "frame": null, "bio": "…", "location": "…", "since": "2023-01-02",
  "accent": [255, 70, 140], "badges": [{ "id": "github", "text": "From GitHub" }],
  "links": [{ "type": "github", "label": "GitHub", "url": "https://github.com/kaanisthatyou", "value": "kaanisthatyou" }],
  "projects": [{ "index": 0, "id": "claudedex", "name": "claudedex", "state": "active", "visibility": "acik",
                 "description": "…", "stack": ["JavaScript"], "devs": ["kaanisthatyou"],
                 "links": { "repo": "…", "homepage": "…", "docs": null }, "stars": 0, "updated": "2026-08-31" }],
  "contributions": [{ "name": "theia", "url": "…", "description": "…", "updated": "2026-09-23" }]
}
```

- **Rooms and contributions:** the owner’s own repositories become rooms. Forks are listed under `contributions`, not as rooms.
- **Building limit:** the first 3 buildings of an owner are free. Each further building needs 6 more verified GitHub projects: your own repositories that are not forks, not empty and at least 7 days old. The limit is at most 7 buildings. For now the site enforces this in the browser; with sign-in the server will enforce it too.
- **Poster:** an owner can put an image from one of their own projects on the billboard on the main building's roof. It shows up in `parts.extras` as `poster`. It is not an ad.
- **Looking for help:** `help` on a project means its owner would welcome someone to join; the string says what they need ("a designer", may be empty). Search matches it, `GET /api/help` lists them.
- **Repo-less projects:** `repoless: true` marks a project with no repository link, such as an idea or work in progress the owner added by hand. It still gets a room. The site shows it with a “No repo” stamp.
- **Link types:** `github`, `website`, `portfolio`, `x`, `bluesky`, `linkedin`, `youtube`, `instagram`, `twitch`, `mastodon`, `discord`, `email`, `other`.
- **Building detail:** adds `parts`, the machine-readable recipe (type, wall material, window, surround, cornice, ground floor, extras), and `recipe`, the same in words.

## Privacy rules

| Visibility | What comes back |
|---|---|
| `acik` (public) | Everything |
| `yari` (semi-private) | No `agents`, no `sessions` |
| `gizli` (private) | Only `{ index, state, visibility, hidden: true }`. No name, description or link on any surface. |

- **Sample profiles:** profiles with `demo: true` are procedural samples whose links are placeholders. Their `links[].placeholder` and `projects[].placeholderLinks` are `true`.
- **Owner settings in the editor:** visibility, left-out repositories (they appear nowhere), hand-added projects and building parts apply on top of the GitHub data. Until sign-in exists, they are kept in the owner’s browser.

## Where a profile comes from

1. The building on the street, if the user has one.
2. A saved profile in `data/profiles.js`. The founder’s saved copy has `live: "github"`, so it is refreshed from GitHub when the network is available.
3. GitHub’s public data: the user, their repositories and their social accounts.
4. Otherwise a sample profile (`preview: true`, `from: "demo"`).

GitHub is called without authentication (60 requests an hour). When the limit is hit, a sample profile is used and `note` says so.

## MCP

`.mcp.json` in the project root registers the server when Claude Code is opened in that folder. To add it by hand:

```bash
claude mcp add byteland -- node "<project>/server/mcp.mjs"
```

| Tool | Input | Returns |
|---|---|---|
| `about` | none | Concepts, units, privacy, languages |
| `get_street` | `seed?`, `mode?`, `from?`, `to?`, `lang?` | Buildings and gaps in a range (at most 80,000 cm) |
| `get_building` | `ref` (door number or `@user`), `seed?`, `mode?`, `lang?` | Profile, rooms, parts, recipe |
| `get_profile` | `handle`, `seed?`, `mode?`, `github?`, `lang?` | Profile |
| `list_projects` | `handle`, `seed?`, `mode?`, `github?`, `lang?` | Projects and contributions |
| `get_project` | `handle`, `project`, `seed?`, `mode?`, `github?`, `lang?` | One project, or nothing if it is private |
| `search` | `query`, `radius?`, `seed?`, `mode?`, `lang?` | Everyone in the city: `people`, `projects` and `stacks` across all residents; plus `results`, buildings on the street matching a name, project or technology |
| `popular` | `period?` (`week` or `all`) | Leaderboard: most visited buildings, most viewed projects, newest residents |
| `get_stats` | `handle` | Unique visits (all time, this week), project views, weekly rank, last 14 days |
| `preview_building` | `handle`, `github?`, `lang?` | Deed preview: the user’s building, plus a `url_hash` that opens it on the site |
| `list_rooms` | `handle` | The rooms of a person that have been used (rooms are open by default; one nobody has written in is empty and not listed): mode (`open` or `read-only`), channels with message counts, threads, last activity, people in voice |
| `read_room` | `handle`, `project`, `channel?`, `limit?` | The latest messages of one channel (default: the first) with reaction counts, and the forum threads |
| `read_thread` | `handle`, `project`, `thread` | One forum thread with its replies |
| `live` | none | Voice lounges with people in them now, and rooms with messages in the last 48 hours. Who is in voice lives in the running site's memory: the tool asks `BYTELAND_URL` (default `http://127.0.0.1:8787`); without the site only the recent rooms are known |

Protocol: JSON-RPC 2.0, one message per line. Supported versions: 2025-06-18, 2025-03-26 and 2024-11-05. No dependencies; Node 18 or newer.

## HTTP API

```bash
node server/http.mjs            # site at http://localhost:8787, API at /api
```

| Endpoint | Description |
|---|---|
| `GET /api` | About and endpoints |
| `GET /api/openapi.json` | OpenAPI 3.1 description |
| `GET /api/street?seed=&mode=&from=&to=` | A stretch of the street |
| `GET /api/buildings/{ref}` | Building detail |
| `GET /api/profiles/{handle}` | Profile (`?github=0` skips GitHub) |
| `GET /api/profiles/{handle}/projects` | Projects and contributions |
| `GET /api/profiles/{handle}/projects/{project}` | One project; 404 if private |
| `GET /api/profiles/{handle}/preview` | Deed preview |
| `GET /api/search?q=&radius=` | Search everyone: `people`, `projects`, `stacks` (all residents) and `results` (the street) |
| `GET /api/me` | Who is signed in (`user` or null), which sign-ins the server offers (`providers`), and `dev` |
| `GET /api/u/{handle}` | A building as everyone sees it: the owner’s saved settings applied (style, LED sign, poster, annexes), private projects reduced to their light; plus `stats`. For the owner: `base` and raw `custom` |
| `GET /api/me/custom`, `PUT /api/me/custom` | Signed in: read or save your own settings (the editor’s JSON, at most 600 KB with photo and poster) |
| `GET /api/residents` | Signed-in residents, newest first |
| `GET /api/popular?period=week|all` | Leaderboard: `people` (rank, visits this week, trend, last 14 days), `projects` (views), `fresh` (newest residents) |
| `GET /api/stats/{handle}` | `visits` (unique per visitor per day), `week`, `views`, `rank`, `days` (14), `projects` (views per project) |
| `POST /api/stats` | `{ e: [{ t: visit \| view \| link \| imp \| clk, h?, p?, ad? }] }`, at most 40 events; same-origin only. Owners, bots and headless browsers are not counted |
| `GET /auth/github`, `GET /auth/discord` | Sign in (`?next=/path` or `#@handle`, `?invite=handle`: a new resident moves in right after whoever invited them); callbacks at `/auth/{provider}/callback` |
| `POST /auth/logout` | Sign out |
| `GET /api/help` | Projects whose owners are looking for help: `projects` with `handle`, `owner`, `id`, `name`, `help` (what they need, may be empty), `stack`, `state` |
| `GET /api/me/inbox`, `POST /api/me/inbox/read` | Signed in: your notifications (`items`: `kind` mention \| reply \| thread \| join \| neighbor \| invite (someone moved in: into the city, next to you, on your invite), `from`, `handle`, `project`, `projectName`, `channel`/`message` or `thread`/`title`, `excerpt`, `at`, `unread`) and `unread`; POST marks them read |
| `GET /api/live` | What is happening now: `voice` (lounges with people in them: project, owner, `peers`) and `recent` (rooms with messages in the last 48 hours: `last`, `by`, `here`) |
| `GET /api/rooms/{handle}` | A user’s used rooms: `mode`, `channels`, `voice` (people in the lounge), `last` and `by` (last activity) per project; `closed` (project → `owner` or `hidden`: rooms the owner closed or that closed when the project was hidden); `auto` (true when every other public project’s room is open too: the user is a resident); plus `owner` and `banned` for the caller |
| `GET /api/rooms/{handle}/{project}` | An open room: `channels` with their last 100 messages, forum `threads` (pinned first), `mode`, `voice` (`peers`, `max`, `stage`), `here` (people looking now), `reacts` (the allowed emoji), `people` (avatars), and `owner`, `me`, `writable`, `banned` for the caller; owners also get `bans` |
| `GET /api/rooms/{handle}/{project}/events` | Live room events (Server-Sent Events): `message`, `thread`, `post`, `edit`, `react`, `delete`, `channel`, `room`, `voice` (join, leave, state), `presence` (`here`), `ban` |
| `GET /api/rooms/{handle}/{project}/threads/{id}` | A forum thread with its replies |
| `POST /api/rooms/{handle}/{project}/settings` | Owner: `{ open: true \| false, hidden?, mode: acik \| salt, channel? }` reopens, closes (`hidden: true`: because the project was hidden, so the site reopens it when the project shows again) or changes a room |
| `POST /api/rooms/{handle}/{project}/channels` | Owner: `{ name }` adds a channel (at most 10) |
| `DELETE /api/rooms/{handle}/{project}/channels/{id}` | Owner: deletes a channel and its messages (the last channel stays) |
| `POST /api/rooms/{handle}/{project}/channels/{id}/messages` | `{ body }`, at most 2000 characters |
| `PUT /api/rooms/{handle}/{project}/channels/{id}/messages/{message}` | Author: `{ body }` edits the message (`edited` is set) |
| `DELETE /api/rooms/{handle}/{project}/channels/{id}/messages/{message}` | Owner or author |
| `POST /api/rooms/{handle}/{project}/channels/{id}/messages/{message}/reactions` | `{ emoji }` toggles your reaction (one of `reacts`: 👍 ❤️ 😂 🎉 👀 🚀 🔥 🙏) |
| `POST /api/rooms/{handle}/{project}/threads` | `{ kind: fikir \| review \| soru \| duyuru, title, body }`; only the owner posts `duyuru` (announcements), others get `fikir` |
| `PUT /api/rooms/{handle}/{project}/threads/{id}` | Author: `{ title?, body? }` |
| `DELETE /api/rooms/{handle}/{project}/threads/{id}` | Owner or author |
| `POST /api/rooms/{handle}/{project}/threads/{id}/settings` | Owner: `{ pinned?, locked? }`. Pinned threads come first; locked threads take replies only from the owner |
| `POST /api/rooms/{handle}/{project}/threads/{id}/reactions` | `{ emoji }` toggles your reaction on the thread (ideas can be voted on) |
| `POST /api/rooms/{handle}/{project}/threads/{id}/posts` | `{ body }` |
| `PUT /api/rooms/{handle}/{project}/threads/{id}/posts/{post}` | Author: `{ body }` |
| `DELETE /api/rooms/{handle}/{project}/threads/{id}/posts/{post}` | Owner or author |
| `POST /api/rooms/{handle}/{project}/threads/{id}/posts/{post}/reactions` | `{ emoji }` |
| `GET /api/rooms/{handle}/~bans`, `POST /api/rooms/{handle}/~bans` | Owner: people removed from all of your rooms; `{ handle, on: true \| false }` removes a person or lets them back |
| `GET /api/rooms/{handle}/{project}/voice` | Who is in the voice lounge: `peers` (`id`, `handle`, `name`, `avatar`, `muted`, `deaf`, `stage`), `max` (8), `stage` |
| `POST /api/rooms/{handle}/{project}/voice/join` | Signed in: take a seat. Returns `peer`, `secret`, `peers`, `ice` (STUN/TURN servers), `stage`, `me`. One seat per person in the whole city: joining elsewhere frees the old seat |
| `GET /api/rooms/{handle}/{project}/voice/stream?peer=&secret=` | Server-Sent Events for your seat: `hello` (who is there), `signal` (from another seat), `voice` (join, leave, state), `bye` (`reason`: left, moved, kicked, removed, closed, lost) |
| `POST /api/rooms/{handle}/{project}/voice/signal` | `{ peer, secret, to, data: { description } \| { candidate } }`: a WebRTC offer, answer or ICE candidate for one other seat |
| `POST /api/rooms/{handle}/{project}/voice/state` | `{ peer, secret, muted, deaf, sharing }` (`sharing`: this seat shows its screen; a listener on a stage cannot) |
| `POST /api/rooms/{handle}/{project}/voice/leave` | `{ peer, secret }` |
| `POST /api/rooms/{handle}/{project}/voice/kick` | Owner: `{ target }` (seat id or username) takes someone out of the lounge |

**Sign-in.** GitHub (and optionally Discord) through OAuth; the session is an httpOnly cookie. No OAuth token is kept: it is used once to read the public account and dropped. Signed-in residents own their building: the server stores their settings and everyone sees the same building.

**Rooms.** Every public project of a resident (and of a saved, non-sample profile such as the founder) has a room, open by default with one channel named `general` (the site shows it in the reader’s language). A room is stored from its first write; until then it reads as empty. People who never moved in have no rooms, and a room the owner closed returns 404. The owner has full control: adds and deletes channels, sets the room open (anyone signed in writes) or read-only (only the owner writes), closes it (messages are kept for a reopen) and deletes any message, thread or reply. Authors can delete their own. Anyone can read an open room; writing needs a signed-in resident (401 otherwise). In dev mode (no sign-in configured, request from localhost) the machine running the server writes as the founder, and with the header `X-Byteland-As: misafir` as `@misafir`, a visitor who owns no building (the site’s test button does this). Writes must be same-origin, and POST bodies JSON. New messages, threads and replies are limited to 12 per 30 seconds per address; deletions and settings are not limited, so an owner can clear spam quickly. Private projects have no room. In messages, `#path` names a file or folder of the project’s GitHub repository and `@user` a person; the site turns both into links. Authors edit their own messages, threads and replies. Everyone signed in reacts with a fixed set of eight emoji (a second press takes it back). The owner pins and locks threads and can remove a person from all of their rooms: that person keeps reading but cannot write, react or join voice.

**Voice.** Every open room has a voice lounge for at most 8 people. Audio goes directly between the browsers (WebRTC, one connection per pair); the server only keeps the seats in memory and passes offers, answers and ICE candidates between them. Nothing is recorded or stored. A seat lasts while its event stream is open; a closed tab frees it within seconds. A read-only room is a stage: only the owner speaks, everyone else joins as a listener. ICE servers come from the environment: `STUN_URLS` (default Google and Cloudflare STUN), `TURN_URLS` with `TURN_SECRET` (short-lived coturn credentials, `use-auth-secret`) or `TURN_USERNAME`/`TURN_CREDENTIAL`, or all of it as JSON in `ICE_SERVERS`.

**Favorites** live in the visitor’s browser for now and are not part of the API.

Every endpoint accepts `?lang=` and `?mode=`, returns JSON, and sends `Access-Control-Allow-Origin: *`.

## In the page

With the page open:

```js
byteland.help()                        // methods
byteland.state()                       // street, view, selection
byteland.visible()                     // buildings on screen
byteland.building('@kaanisthatyou')    // detail
await byteland.profile('octocat')      // profile (does not change the view)
byteland.search('rust')                // this street
await byteland.searchAll('rust')       // everyone in the city (server)
await byteland.popular()               // leaderboard
await byteland.stats('octocat')        // visits of one building
byteland.me()                          // who is signed in
await byteland.goTo('@kaanisthatyou')  // go to a building; build it if it is not on the street
byteland.openProfile(5)                // open the profile panel
byteland.openProject('kaanisthatyou', 'claudedex')
byteland.openRoom('kaanisthatyou', 'claudedex', 'voice')   // tab: chat, forum or voice
await byteland.live()                  // voice lounges with people in them, recently active rooms
byteland.voice()                       // your voice state, or null
await byteland.joinVoice('kaanisthatyou', 'claudedex')   // asks for the microphone; signed in
byteland.leaveVoice()
await byteland.map()                   // topic cities (lights on where the founder lives), seas, peak (map unit: 250 m)
await byteland.openMap()               // top-down map
await byteland.enterCity('ai')         // go down to a topic city's street; 'home' for the founder's city
await byteland.setLanguage('ja')       // change the interface language
byteland.on('select', d => console.log(d))   // events: ready, load, select, close, view, lang, room, voice, live
```

- **Text view:** `#cityText` holds the buildings, projects and links on screen as plain HTML. Screen readers and DOM-reading agents use it. Keyboard users open it through the “Skip to text view” link at the top.
- **Deep links:** `index.html#@user` builds that person's building and opens the profile; `#@user/project` also selects the project's room; `#@user/project/oda` enters the room and `#@user/project/oda/ses` opens its voice lounge. On the server the same works as a path: `/@user`, `/@user/project`, `/@user/project/oda/ses` (with a title and social card for that person). `?lang=xx` picks the language.
- **Page source:** `<script type="application/json" id="byteland-agent">` carries a short description of these surfaces.

## Next steps

Sign-in exists (see above). Agent writes come next, each on the user's own personal token ([`GIRIS-PLANI.md`](GIRIS-PLANI.md), Turkish):
- saving settings (the same JSON as `PUT /api/me/custom`)
- writing in rooms over MCP (`post_reply`); reading is there already (`list_rooms`, `read_room`, `read_thread`, `live`)
- a "coding now" signal from a Claude Code session, which sends the signal only and never the content

The map is not yet served over MCP or HTTP; in the browser, read it with `byteland.map()`.
