# 6502 as a service: the brief

Everything needed to build, run and publish a cartridge for a transistor-level MOS 6502, in one read.

- Chip API (assemble, run, mint cartridges, the registry): `https://tinymachines.ai/6502/api/` (reference at that URL; OpenAPI at `https://tinymachines.ai/6502/api/openapi.json`)
- Token mint (free, rate-limited): `POST https://tinymachines.ai/api/v1/tokens`; with an account, `POST https://tinymachines.ai/api/v1/me/tokens`
- The console contract as data: `GET https://tinymachines.ai/6502/api/v1/console`
- Play a cartridge: `https://tinymachines.ai/6502/games?cart=<url>`
- MCP, for a model with tool use: `https://tinymachines.ai/api/mcp`

## How to work

1. Read the walkthrough below, then the three references it cites. Start at step 1 of the walkthrough.
2. Prove the chain before writing a game: `GET /v1/console`, assemble a few lines with `POST /v1/assemble`, run them with `POST /v1/run`. If any of these fails, stop and say so.
3. Build in small steps and run each on the chip. The chip measures; it does not believe.
4. Show the person the play link once a frame completes, and publish only when they say so.

---
# Build your first cart

A cartridge is one file: a ROM, its tiles, and the console addresses it was
written to. This page is the whole path from nothing to a published cart that
the chip has run, in the order you actually take it. Every command is real
and every address comes from the running service rather than from this page.

There are two ways to walk it. **By hand**, below. Or **by AI**: hand a model
the one URL that carries everything on this page plus the three references
it needs, and let it do the typing. That URL is
[`/6502/cart/brief.md`](/6502/cart/brief.md); it is plain markdown, and the
whole of it fits in one read.

## 1. Mint a token

A token is your handle in the registry and your key to the chip API. It is
free, minted in the [editor](/6502/manage#mint) with one click, or from a
shell:

```bash
curl -s -X POST https://tinymachines.ai/api/v1/tokens \
     -H 'content-type: application/json' -d '{"note":"my first cart"}'
```

It is shown once. Only its SHA-256 is kept, so nothing can show it again. A
few per address a day, so a loop cannot drain the registry. Signed in with
GitHub in the editor, tokens belong to your account instead: a lost one is
re-issued there and your page moves to the new one.

After minting, the editor offers **the brief with your key in it**: one
markdown file, downloadable as `SKILL.md`, that an AI reads to build a cart
for your page. Save it as `~/.claude/skills/tm6502-cart/SKILL.md` for Claude
Code, as `AGENTS.md` for Codex, or paste it into any chat. It carries your
token, so keep it as you would the token. The same brief without the key is
at [/6502/cart/brief.md](/6502/cart/brief.md).

Minting does two more things. It hands you a **cart code**: a short slug
derived from the token (an HMAC, so the pairing is verifiable here and by
nobody else), which is the name to publish your first cartridge under. And it
**claims your page**: the handle you typed in the request, or the cart code
if you typed none.

## 2. Your page

Your page is `/6502/builders/<handle>`, the first half of every cart's
address, and it exists the moment the token does. One token, one handle, and
it cannot be changed later, so pass `"handle": "ada"` when you mint if you
want a name of your own. Scripting the claim yourself is still there for a
token minted by hand:

```bash
curl -s -X POST https://6502.tinymachines.ai/api/v1/registry/claim \
     -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
     -d '{"handle":"ada","name":"Ada"}'
```

## 3. Read the contract, then write the ROM

The console is [a contract, not hardware](/docs/6502/the-console-contract):
a handful of zero-page addresses the host and the ROM agree on, a screen
that is a page of the chip's own memory, and a tick flag the ROM raises when a
frame is done. Read it from the service rather than from a page:

```bash
curl -s https://6502.tinymachines.ai/api/v1/console
```

Write the ROM in 6502 assembly against those addresses. The service's own
assembler will assemble it, so the source travels with the cartridge and what
you publish is what you wrote. [Cartridge zero](/docs/6502/cartridges#cartridge-zero)
is Snake in a few hundred bytes and is the shape to copy.

## 4. Draw the tiles

Tiles are 8x8, two bits per pixel, sixteen bytes each, the NES shape; the
[contract](/docs/6502/the-console-contract#tiles) spells out the byte order
and the four colours, which are the die's own. Any sprite tool that emits
`.chr` emits this. Send the sheet as hex.

## 5. Mint the cartridge, and it runs

```bash
curl -s https://6502.tinymachines.ai/api/v1/cartridge \
     -H 'content-type: application/json' -d @cart.json -o mine.cart.gz
```

where `cart.json` carries `rom.source` and `rom.org`, `console` (the
addresses from step 3), `tiles.chr` (hex), `meta` (name, author, blurb) and
`frames`. Minting **refuses a layout that cannot work** and then **runs the
ROM on the chip**: the file that comes back carries a `verify` block saying
whether it booted, how many frames it finished, and what each cost in
half-cycles. A ROM that never raises its tick flag gets no file.

## 6. Play it

The console loads a cartridge from a URL:

```
https://tinymachines.ai/6502/games?cart=<url of your .cart.gz>
```

or from its file picker. Its tiles replace the sheet, so it brings its own
art.

## 7. Publish it, and the chip measures it again

Publish under your cart code, or any name you like:

```bash
curl -s -X PUT https://6502.tinymachines.ai/api/v1/registry/b/ada/roms/first \
     -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
     -d "{\"cart\":\"$(base64 -w0 mine.cart.gz)\",\"frames\":3}"
```

The registry unpacks the file and runs it here before listing it. Nothing you
write in the request decides a number: what your page shows beside the cart is
what the chip did on this run. Your page is `/6502/builders/ada`, the cart is
`/6502/builders/ada/first`, and both are live the moment the run passes.

## 8. Read what it did

The walk, [Snake, one instruction deep](/docs/6502/walk-snake), follows one
instruction of cartridge zero five cycles into the silicon, with the schematics
pulled from the switch network. The [tracer](/6502/tracer) does the same for
any program, live. That is the lesson the rest of the site is set up to teach.

## The AI route

The owner sent an AI the API reference and one sentence, and it wrote,
debugged and published a working game with its own art. The brief makes that
the normal path: one URL, everything above plus the three references, in
plain markdown.

```
https://tinymachines.ai/6502/cart/brief.md?slug=<your cart code>&handle=<your handle>
```

Minting returns that URL filled in. Give a model it and the sentence: *use my 6502 as a service API to
build and run a small game.* Minting its own token counts against its own
address, and publishing needs the token, so the model cannot publish as you
unless you hand it yours.


---

# The console contract

Die Runner is a console on a transistor-level MOS 6502. The game is a 6502 ROM,
the screen is a page of that chip's memory, and the browser draws it. There is
no emulator: every frame settles 3510 switches on the real die through
[the API](/docs/6502/the-api).

## The console is a contract, not hardware

The chip has no video, and nothing here uses its interrupt line. So a "frame"
is not something the silicon knows about. It is an agreement between the ROM
and whatever drives it, and that agreement is the whole console:

```
the host clears a byte   ->  the ROM notices, runs one frame, sets it back
the host writes a byte   ->  that byte is the controller
the host reads a page    ->  that page is the screen
```

The ROM busy-waits on the flag, which is the only way to synchronise with the
outside when you have no interrupt and no timer. It works over HTTP *because*
the API is stateless: the frame boundary is a memory edit between two `/v1/step`
calls, and the whole machine travels in each one.

Nothing about this was designed for games. It falls out of a design that
carries the machine as a value.

## What a frame costs, measured

| | |
|---|---|
| First frame (init: clear 256 cells, place food) | 5,400 half-cycles |
| Every frame after | **600 half-cycles**, exactly |
| That in chip time | about 0.3 ms |
| A round trip to the engine | about 200 ms |

**The chip is not the bottleneck by three orders of magnitude.** The frame rate
is the round trip, and the page says so rather than hiding it.

A cartridge that free-runs instead of busy-waiting could have 333 frames in one
request, since the API caps at 200,000 half-cycles, at the cost of input
latency. The flag handshake buys responsiveness and pays one request per frame
for it.

## Tiles

8x8 pixels, **two bits per pixel, sixteen bytes per tile**: the NES shape,
because it is what every old-school sprite tool emits, and because four colours
per tile is the constraint that makes the art look like the era rather than
like a photograph.

```
bytes 0..7    bit 0 of each pixel, one byte per row, MSB is the leftmost pixel
bytes 8..15   bit 1 of each pixel
colour        (plane1 << 1) | plane0   ->  0..3
```

The palette is the die's own, the four colours the exploded view paints the
mask layers in. That is the conceit of Die Runner: the playfield *is* the chip.

| | | |
|---|---|---|
| 0 | `#0B1120` | substrate, the die with nothing on it |
| 1 | `#3E93A6` | diffusion, the switched layer |
| 2 | `#E0A24B` | polysilicon, the gates and anything that controls |
| 3 | `#4FBFD4` | metal, the wires and anything the runner rides |

Colour 0 is drawn, not skipped: this is a tiled screen, not a sprite layer.

`chr.js` carries a starter set drawn in code, so the console renders before any
art exists and so the spec is executable. Whatever a tool produces has to
decode to exactly that shape. `encodeCHR` is the inverse, so the art pipeline
and the console share one definition rather than two that drift.

## A cartridge that draws nothing

`console.kind: "headless"` is a program on the chip with no screen page and
no tick flag. It exists because the site had programs scattered through it
(the seven the explorer boots, the worked example on the API page) that were
never cartridges, so they were never minted, listed or measured. Now they
are: the same file, the same registry, the same rule that what is shown is
what the chip did.

What verifying one says is where it got to. The registry boots it, runs it
for `console.half_cycles`, and reads the registers and the bytes the
cartridge names in `console.peek` off the silicon. The last quarter of the
run is sampled four times, so the report can say whether the pc was still
moving at the end: a loop or a finished program on one side, a JAM on the
other. Nothing is drawn, and the listing says so ("draws nothing") rather
than showing a frame cost it does not have.

The layout checks that still mean something still apply: a ROM on the stack
page or over the vectors is refused. The screen checks do not, and the file
carries no screen fields, so nobody reads a default screen address off a
cartridge that has none. The console refuses to boot one, with the reason.

## The gates are real

Each gate is a **switch that exists on this die**, and it conducts exactly when
its own control line is high **on the chip running the game**. Nothing
simulates a clock phase; the phase is whatever the 6502 executing this code
happens to be doing at the end of a frame.

The host watches eight lines, packs their levels into a byte, and hands it to
the ROM. A gate cell carries its own gate index (`16+g` is the channel that
conducts while line `g` is high, `24+g` the one that conducts while it is low),
so **what is drawn and what kills you come from the same byte** and the picture
cannot lie about which way is open. The two channels are complementary, so
there is always a way through. That is not a kindness, it is what a pass
transistor is.

The eight were chosen by measurement, not taste: they are the lines that gate a
switch between two *named* nodes, ranked by how often they actually moved over
twenty-four frames of play. A line that never moves makes a gate that is always
shut or always open, which is scenery.

| gate | control line | high | flips | the switch it gates |
|---:|---|---:|---:|---|
| 0 | `dpc25_SBDB` | 16/24 | 10 | `sb0 - idb0` |
| 1 | `dpc9_DBADD` | 18/24 | 9 | `idb0 - alub0` |
| 2 | `dpc10_ADLADD` | 6/24 | 9 | `adl0 - alub0` |
| 3 | `dpc21_ADDADL` | 3/24 | 6 | `alu2 - adl2` |
| 4 | `dpc23_SBAC` | 4/24 | 4 | `sb0 - a0` |
| 5 | `dpc30_ADHPCH` | 21/24 | 4 | `pch3 - adh3` |
| 6 | `dpc40_ADLPCL` | 21/24 | 4 | `adl0 - pcl0` |
| 7 | `dpc2_XSB` | 2/24 | 2 | `x0 - sb0` |

Those labels are the hand-written ones. [The atlas
derives](/docs/6502/the-atlas#what-the-atlas-settled-that-a-hand-written-list-had-wrong)
them now and disagrees with three, including this table's `x0 - sb0`.

Gates 5 and 6 move together and always will: `ADHPCH` and `ADLPCL` are the
program counter's own round trip, and they fire on every opcode fetch. Two
gates that are really one event is a true thing about the chip, so both are
kept.

Sampling is one frame behind, and has to be: the chip must have run before
there is anything to read. So the gates you are threading are the state of the
CPU as it finished drawing the frame you are looking at.

## Two things that had to be measured rather than designed

- **The runner sits at row 2, not row 13.** New terrain appears at row 15 and
  scrolls up, so from row 13 a wall arrived two frames after it appeared: no
  warning at all at five frames a second. From row 2 the same wall is thirteen
  frames away, and it reads as descending into the die rather than being
  ambushed by it.
- **Gaps drift, they do not land anywhere.** A gap at a random column can be
  further away than the runner can walk before the barrier arrives, which is
  not difficulty but a death the player could not have avoided. Each gap steps
  -3 to +4 from the last, against six frames of travel.

## The screen moved to $0500

Adding scenery pushed the ROM from 359 bytes to 521, past `$0400`, which was
where its own screen lived. It assembles, it boots, and the picture eats the
code. `games/tools/asm.mjs --limit $0500` makes that a build failure instead of
a mystery, and the screen sits a page higher.

Moving it is four addresses, and **missing one is silent**. `$0410` is the
scroll's *source*, and with it left behind the game copied unrelated memory
into the screen every frame and drew an almost-empty die. Nothing errored.

That is the reason the contract travels inside the cartridge rather than beside
it. See [cartridges](/docs/6502/cartridges).


---

# Cartridges

A cartridge is one **gzipped JSON** file carrying the ROM (bytes, labels and
source), its tiles in both the binary form and as rows of `0..3`, and the
console addresses it was written to.

The contract travels *with* the bytes rather than beside them, because the
contract is the part an outside author has to agree with, and a page needs
eight addresses to play a game with no hardware to ask about any of them. A
contract in a different file is the copy that drifts.

`mtime` is zero, so minting the same cartridge twice gives the same bytes and
two of them can be diffed.

```bash
curl -s localhost:6502/v1/console                     # the contract, published
curl -s localhost:6502/v1/cartridge -d @cart.json \
     -H 'content-type: application/json' -o mine.cart.gz
curl -s 'localhost:6502/v1/cartridge?format=json' -d @cart.json ... | jq .verify
```

`GET /v1/console` is the contract as data, and it is the copy to read from a
program. This page is the reasoning; that route is the reference.

## Two things minting does that assembling cannot

**It refuses a layout that cannot work.** A ROM overlapping its own screen
assembles perfectly and then draws over itself; a contract byte inside the ROM
is the host writing into the code. Each is a 422 with the reason, not a
cartridge that fails later.

Reading the assembler's inclusive `end` as one-past made every one of those
checks a byte short, which `test_cartridge.py` now pins from both sides.

**It runs the thing.** A ROM that assembles, boots and never raises its tick
flag is a ROM that does not run on this console, and nothing short of running
it says so. The report carries frames completed, what each cost, whether the
screen changed, and which tiles are on it.

## The frame cost is measured on a ladder that ignores the cartridge

The cost is measured on an absolute ladder, 128 half-cycles up to 16k and then
1024, and deliberately not seeded from anything the cartridge declares.

Sizing the first step from a declared cost is right for a *host* and wrong for
a measurement. The same ROM minted at `frame_cost` 512 and at 20000 measured
6400 and 6250, each number being its own request rounded up.

This is not hypothetical. **The frame cost Die Runner's page claimed was its own
request read back.** The console asks for `frameCost` half-cycles and then
reports what it spent, so whatever was written there confirmed itself: 12,000
was a number the file had typed, not a number the chip had produced.

We ran it and measured: Die Runner's steady frame is **8,704**, rock solid
over twelve frames, with the first at 5,440. That is about 28% less chip time
a frame than the page was buying.

## Loading one

The console page loads a cartridge from `?cart=<url>` or from the file picker,
and a loaded cartridge joins the picker rather than quietly replacing what the
label says is on screen. Its tiles replace the sheet, so a cartridge brings its
own art.

`games/deploy.sh` mints the sample rather than keeping it in the tree, so it
cannot go stale against `rom/dierunner.s` and every deploy exercises the
endpoint.

```bash
# Minted by the API, which refuses a layout that cannot work and then RUNS it.
python3 games/tools/mint.py --api https://6502.tinymachines.ai/api
```

## Cartridge zero

`rom/snake.rom`, 351 bytes, here to prove the pipe end to end.

Every address in its cartridge entry was read off the disassembly
(`rom/snake.lst`) and then confirmed on the running chip, never guessed. An
earlier reading had `2 = right`, and the snake walked downwards to say
otherwise.

| addr | what |
|---|---|
| `$0D` | tick flag: host clears, ROM raises |
| `$02` | requested direction, 1 up 2 down 3 left 4 right |
| `$03` | game over |
| `$0400-$04FF` | the screen, 16x16, `0` empty `1` snake `2` food |

The board **wraps** rather than having walls: `AND #$0F` on both nibbles of the
cell index.

*Provenance: written by Grok on the site owner's prompt, and owned by them
under xAI's consumer terms. It is not derived from the die data and carries
none of that data's obligations.*

## Cartridge one

`rom/dierunner.s`, **339 bytes**, written for this console and assembled by the
project's own assembler (`games/tools/asm.mjs` over `web/asm.js`, which inverts
the disassembler's table, so if it assembles it disassembles back to the same
lines).

You are a charge carrier descending the die. The world scrolls up to meet you.
Polysilicon gates bar the way with one opening; **pass-transistor gates have two
channels and only one conducts**, and every eighth frame the clock phase flips
and they swap. A channel that is shut now will be open in a moment, which is the
whole game. Charge packets score. The die wraps.

The seven tiles that arrived with the art are all in use, and none is
decoration for its own sake. The die used to be empty between barriers.

| tile | what it is | what it does |
|---:|---|---|
| 9 | poly bus | a run of three across the die. Scenery |
| 10 | power rail | runs **down** a column for three to six rows. Scenery |
| 11 | diff well | an occasional single. Scenery |
| 12 | poly T | where a rail comes in |
| 13 | metal L | where a rail turns and leaves |
| 14 | capacitor | worth **five** charge packets |
| 15 | bond pad | **signposts the gap** of the barrier above it |

The power rail is the one worth understanding: the ROM draws a single cell per
row, and it comes out as a rail *because the world scrolls*. A poly T caps the
end it comes in at and a metal L the end it leaves by, so a rail has a
direction without the ROM ever drawing a line.

Only a **plain** barrier gets a bond pad. Which channel of a *switched* gate is
open depends on a control line that will have moved by the time the player
arrives, so a signpost there would be pointing at a guess. See
[the console contract](/docs/6502/the-console-contract#the-gates-are-real).


---

# The registry

The builder pages are at [/6502/builders](/6502/builders). They still answer
at `games.tinymachines.ai/builders` too, because nothing has been switched off.

**The only stateful thing here, and the boundary is the point.** The chip is
untouched: every request still carries the whole machine, and running a
published ROM still means POSTing it. What is stored is a catalogue. One SQLite
file (`REGISTRY_DB`), a row per thing.

It is not a separate service. The cartridge mint, the console spec and the
registry are routes on the **same** FastAPI app. There is one Python service,
not two.

## Tokens

There is no sign-up. A token is minted by hand, handed over, and claimed. One
token, one builder.

```bash
python3 service/registry_admin.py mint --note "who it is for"   # printed once
python3 service/registry_admin.py tokens
python3 service/registry_admin.py builders
python3 service/registry_admin.py revoke <token-or-hash>
python3 service/registry_admin.py grant <token> <handle> <name>  # reserved names
```

That is deliberately the whole of the auth story for now, and it is a
limitation rather than a design.

What it does get right is the part that would hurt to change later: **a token
is shown once and only its SHA-256 is stored**, so a copy of the database is
not a copy of everybody's credentials.

A token that is not this builder's gets **404, not 403**: it has no business
learning whether the builder exists. Revoking leaves the page and its ROMs
alone, because revoking is about the credential.

## Three rules that shape the rest

**The registry measures rather than believes.** A cartridge is a file somebody
can edit, so its own `verify` block is a claim by its author. On publish the
cartridge is unpacked and **run here**, and the size, tile count and frame cost
printed beside it are what that run produced. A ROM that does not complete its
frames is refused rather than listed.

The test publishes a cartridge claiming a 12-half-cycle frame and requires the
stored number to be the measured one. See
[cartridges](/docs/6502/cartridges#the-frame-cost-is-measured-on-a-ladder-that-ignores-the-cartridge)
for what happens when a page reads its own request back.

**Art is only ever rows of `'0'..'3'`.** Converting a photograph happens in the
browser, so there is no image parser in the request path and what lands on disk
is CHR: the same encoding a sprite sheet uses, so the portrait on a builder
page is drawn by the same `decodeCHR` that draws the game.

**A PATCH touches only what it names**, so a client saving a bio cannot blank
an avatar it never loaded.

## Pages

A page is `/b/<handle>`, and a ROM on it is `/b/<handle>/<slug>`, which is the
console with that cartridge already loaded. Both are static documents that read
their own path: nginx points a quoted regex location at `builder.html` and at
`index.html`, so a published ROM has an address of its own rather than a query
string.

The regex is quoted because **nginx reads `{` as the start of a block**. A
location regex containing `{2,32}` fails with "unknown directive" naming the
middle of the pattern.

[/6502/manage](/6502/manage) is the editor: paste a token, edit the page,
publish a `.cart.gz`.

## Both halves are under the apex now

The reading half. [/6502/builders](/6502/builders) is this index and
`/6502/builders/<handle>` is a page; `/6502/b/<handle>` is the address the
service itself hands out and redirects there, because those links are not ours
to break. Both read the same live registry over CORS, which is what the
`Access-Control-Allow-Origin: *` on that service was for.

The writing half followed on 2026-08-24, and what had held it back is worth
recording because it looked like a decision and was a header. A preflight from
`tinymachines.ai` used to come back accepting four headers, none of them
`Authorization`, so a browser on the apex could not send a bearer token to
that service at all: not to claim a handle, not to edit a page, not to
publish. The request was never made, and the server log had nothing to show
for it. `tinymachines/6502#12` records it; the service's CORS policy now
admits `authorization` and the registry's own verbs (`PATCH`, `PUT`,
`DELETE`), and [the editor](/6502/manage) runs here against it.

Proxying the writes through the apex's own API would work, since there is no
browser between two servers and therefore no preflight. It is deliberately not
done. The read-only service scope and the identity binding were left out of
the listings work on the grounds that they turn into an internal join if games
moves under the apex, and a credentialed proxy built now is the same boundary
built twice.

## Dithering was measured, not chosen

A photograph is converted in the browser (`art.js`) into the die's four colours
and uploaded as a grid of `'0'..'3'`, never as an image.

Dithering is Floyd-Steinberg **in RGB rather than in luminance**, and that was
measured. By Rec.709 the palette is 17, 130, 169, 169, so polysilicon and metal
are the same brightness to within 0.2 of 255 and differ only in hue. A
luminance ramp has three steps, not four, and throws the warm half of the
palette away.

## The bug that only showed at depth two

`index.html` loaded `game.js` with a relative `src`. Served at `/`, that is
`/game.js`; served at `/b/tinymachines/die-runner` it is
`/b/tinymachines/game.js`, which is a 404.

**The page still rendered**, because the markup is static and only the
JavaScript was missing, so it looked like a console that had failed to boot
rather than one whose script was never fetched.

The document's references are absolute now, and the two fetches inside
`game.js` resolve against `import.meta.url` rather than the page, the same
trick the wasm glue uses. Found by driving the real page, not by reading it.


---

# MCP

`POST /mcp` speaks the Model Context Protocol over streamable HTTP, with no
session and no SSE stream, for the same reason [the API](/docs/6502/the-api)
keeps no sessions.

Five tools: `console_spec`, `assemble`, `run`, `mint_cartridge`, `chip_atlas`.

## Connecting a client

The chip's endpoint, and the site's, which has three tools about what the
pieces are and which of them are up:

```
https://6502.tinymachines.ai/api/mcp
https://tinymachines.ai/api/mcp
```

For Claude Code:

```
claude mcp add --transport http 6502 https://6502.tinymachines.ai/api/mcp
claude mcp add --transport http tinymachines https://tinymachines.ai/api/mcp
```

For a client that reads an `mcpServers` block (Claude Desktop, Cursor and
the like):

```json
{
  "mcpServers": {
    "6502": { "type": "http", "url": "https://6502.tinymachines.ai/api/mcp" },
    "tinymachines": { "type": "http", "url": "https://tinymachines.ai/api/mcp" }
  }
}
```

No key, no session header, no stream to open. Both servers speak the
`2025-06-18`, `2025-03-26` and `2024-11-05` revisions of the protocol; a
client asking for a newer one is answered in the newest of those, which is
what the protocol says to do. A `GET` on either endpoint is a 405 with
`Allow: POST`: there is no SSE stream, and the older HTTP+SSE transport,
which opens one with a `GET` first, is not spoken.

## The tools are coarse where the HTTP routes are fine-grained

That is the design, not an oversight.

The API is stateless because a *program* holds the machine: 2 KB of hex out and
back, and the client's copy is the session. An MCP client is a language model,
and a model cannot usefully hold 2 KB of hex. So `run` assembles, boots, steps
and reports in one call, and the machine never leaves the server.

## run renders the screen

`run` renders the screen as two hex characters a cell.

That is the one thing that turns writing a 6502 game from guessing into
working: an assembler says the bytes are legal, and only the picture says the
program is right.

## What holds it honest

The MCP suite reproduces the project's own witness rather than consulting a
table. `$2E + $14` reads `$42` at `$0082` by half-cycle 41, which is the number
the programs page, the API reference and the service suite all state.

Nothing in that path consults an instruction table, so agreeing is evidence
rather than a restatement.
