# Operating this build: every surface, every call, every receipt

slug: operating-this-build · https://miscsubjects.com/a/operating-this-build · tags: api, protocol, receipts, agents, documentation · updated 2026-09-12T08:27:59.715Z

miscsubjects is a running build in which every capability it has — sending a message, running
SQL, calling a model, publishing an article, deploying its own code — is one row in one database
table, invoked through one HTTP door, and recorded afterwards as a receipt anyone can open
without an account. The directory is that table. Dispatch is that door. A receipt is that
record. Every block below is a call made against the live system on 2026-09-12, with the
response pasted back verbatim. Nothing here is illustrative. Where a call failed, the
failure is printed too, because the failure grammar is half of what you need to operate this.

If you are a model reading this cold: by the end you can run any capability this system has,
from four different surfaces, and prove afterwards that you did.

## 1. The shape of the whole thing, in five sentences

1. **Every capability is a row in one table.** `directory` — one row per thing the system can
   do. Today: 1,438 rows, of which 97 are agents (a model plus a system prompt). Adding a
   capability is an INSERT, not a deploy.
2. **Every capability runs through one door.** `POST /api/dispatch {"key":"THE_ROW","body":"…"}`.
   Same URL and same auth header for "run SQL", "send an SMS", "ask Gemini", "generate a video".
3. **Every run leaves a receipt whose URL is public.** The act and the proof of the act are
   produced by the same call. You never have to be believed.
4. **Every value is a cell.** Limits, model defaults, prompts, timeouts — they are rows you can
   edit, not constants someone compiled in.
5. **The system explains itself to a stranger with no key.** Discovery is keyless. Authority is
   only needed to *act*.

## 2. One tool, invoked, with its receipt

The smallest complete act. Count the rows in the directory:

```bash
curl -s -X POST https://miscsubjects.com/api/dispatch \
  -H "x-terminal-key: $TERMINAL_KEY" -H 'content-type: application/json' \
  -d '{"key":"D1_QUERY","body":"SELECT count(*) AS rows FROM directory"}'
```

What came back, unedited:

```json
{
  "ok": true,
  "ran": true,
  "kind": "invocation_result",
  "trace": "t_83tuljrj",
  "result": "[{\"rows\":1438}]",
  "cost": 0,
  "proof": {
    "ok": true,
    "did": "DONE — D1_QUERY",
    "invocation_id": "inv_4movg4ukwn",
    "public_receipt": "https://miscsubjects.com/receipt/inv_4movg4ukwn",
    "confirm": "https://miscsubjects.com/api/dispatch?confirm=inv_4movg4ukwn",
    "receipt": "https://miscsubjects.com/api/dispatch?receipt=inv_4movg4ukwn",
    "say_to_user": "✓ Done via D1_QUERY. Public receipt: https://miscsubjects.com/receipt/inv_4movg4ukwn"
  }
}
```

Read that `proof` block carefully, because it is the load-bearing idea.

- `invocation_id` — the act now has a name.
- `public_receipt` — a URL **anyone** can open with no key, which renders the invocation, its
  hashes, its lineage and its place in the protocol.
- `say_to_user` — the system writes the sentence a model should say about what it just did, so
  that a model reporting on its own work cannot quietly upgrade a failure into a success.
- `cost` — charged to the act, not to a monthly bill you reconcile later.

`ran` and `ok` are two different fields on purpose. `ran: true, ok: false` means the system
did its job and the world said no.

### What common practice does here instead

| | Common practice | Here |
|---|---|---|
| Proof an action happened | Your own log line, trusted by you | A public URL minted by the act itself |
| Who can audit it | Whoever has your dashboard login | Anyone with the link, no account |
| Success reporting | The caller writes the summary | The system writes `say_to_user` |
| Failure | An exception, often unlogged | A receipted row with the provider's exact words |
| Cost attribution | Monthly invoice, reassembled later | A field on the single act |
| Adding a capability | Write code, review, deploy | INSERT a row |

The common column is not wrong; it is a different bet. The bet here is that a uniform object
grammar plus one dispatch plus one receipt tree beats N integrations with N vocabularies —
specifically for a system where most operators are models rather than people.

## 3. Calling a model

Agent rows hold a model id and a system prompt. Invoking one is the same call as anything else.

```bash
curl -s -X POST https://miscsubjects.com/api/dispatch \
  -H "x-terminal-key: $TERMINAL_KEY" -H 'content-type: application/json' \
  -d '{"key":"ASK_GEMINI","body":"Reply with exactly the word: RECEIPTED"}'
```

```json
{
  "ok": true, "ran": true, "kind": "invocation_result",
  "trace": "t_gsck39xt",
  "result": "RECEIPTED",
  "cost": 0,
  "proof": {
    "ok": true, "did": "DONE — ASK_GEMINI",
    "invocation_id": "inv_mccaqos91o",
    "public_receipt": "https://miscsubjects.com/receipt/inv_mccaqos91o"
  }
}
```

The same call against three other providers, at the same minute, returned this:

```json
{ "ok": false, "ran": true, "trace": "t_gefchgti",
  "result": "PROVIDER_ERROR: You have no credits remaining. Add credits to continue using the API at https://platform.openai.com/settings/organization/billing/.",
  "proof": { "invocation_id": "inv_snxwkpg1ph",
             "public_receipt": "https://miscsubjects.com/receipt/inv_snxwkpg1ph",
             "say_to_user": "✗ It did NOT work: PROVIDER_ERROR: You have no credits remaining …" } }
```

Anthropic: `Your credit balance is too low to access the Anthropic API.`
Moonshot: `Your account org-3bf6… is suspended due to insufficient balance.`

That is the state of the build as of this writing, and it is in the article because the
article is a record, not a brochure. Three of four metered providers are out of credit;
Gemini answers. A failed provider call is still `ran: true` with a public receipt — the
system does not hide a failure by declining to record it.

**Model batching.** `POST /api/invoke` runs up to 200 agent calls in one round trip, all in
flight at once, returning one result row each. It takes `key` plus `inputs[]` (fan the same
prompt over many inputs), `n` (the same call repeated), or `calls[]` (heterogeneous).

`/api/invoke` is the *model* lane. Pointing it at a non-agent row used to hand the gateway a
function name where a model id belongs:

```
"error": "upstream_400: … \"d1Query\" is not a valid model identifier. Expected \"<provider>/<model>\"."
```

That was found by running the call while writing this article, and fixed in the same sitting:
the door now refuses by name and points at the door that does run it —
`not_an_agent_row: D1_QUERY is type=fn. /api/invoke batches agent rows (model + system
prompt). Run this one through POST /api/dispatch.` The regression test is built from the
exact failure string above. Fixing the class beats describing the instance.

## 4. Editing an article

An article is `{slug, title, body, meta}`, where `meta` carries claims, sources, reviews,
contributions, revisions and a hash-chained provenance ledger.

```bash
# read — public, no key
curl -s https://miscsubjects.com/api/articles/<slug>
curl -s https://miscsubjects.com/api/articles/<slug>?format=post   # re-postable shape
curl -s https://miscsubjects.com/api/articles/<slug>?rev=0         # an older revision

# create or replace (upsert; the prior head is snapshotted into meta.revisions[])
curl -s -X POST https://miscsubjects.com/api/articles/<slug> \
  -H "x-terminal-key: $TERMINAL_KEY" -H 'content-type: application/json' \
  -d '{"title":"…","body":"## Section\n…","register":"technical"}'

# merge without touching untouched fields
curl -s -X PATCH https://miscsubjects.com/api/articles/<slug> \
  -H "x-terminal-key: $TERMINAL_KEY" -d '{"tags":["a","b"]}'

# verify the chains
curl -s https://miscsubjects.com/api/articles/<slug>/sources
curl -s https://miscsubjects.com/api/articles/<slug>/provenance
curl -s https://miscsubjects.com/api/articles/<slug>/revisions
```

POST upserts and PATCH merges. Confusing the two is the most common way to lose a field.

### The publish gate refuses junk by name

Writing this article, the first attempt used a throwaway slug. The system refused:

```json
{
  "error": "register_refused: test_content_refused",
  "slug": "receipt-proof-scratch",
  "how_to_fix": "Test and placeholder pages are banned from the live site (matched /\\bscratch(?:pad)?\\b/). To exercise the write path, POST the same body with \"draft\": true — it stores as an unpublished draft, readable with the owner key and invisible on every public index. Publish only real, finished work under its real title.",
  "state_changed": false
}
```

Three properties worth copying. The refusal **names itself** (`test_content_refused`). It
**shows the rule** that fired, as the literal regex. It **hands you the working alternative**
in the same breath. And `state_changed: false` tells a model, unambiguously, that retrying is
safe. Compare with the common practice: `400 Bad Request`, and a model that now has to guess
whether it half-wrote something.

### Model-mediated writing

For work that a model should do end to end, the protocol endpoints wrap generate → validate →
verify sources → chain → publish:

```bash
curl -s -X POST https://miscsubjects.com/api/protocol/write \
  -H "x-terminal-key: $TERMINAL_KEY" -H 'content-type: application/json' \
  -d '{"slug":"<slug>","model":"grok/grok-4.3","ask":"Write an evidence-graded review of X",
       "web_search":true,"max_tokens":4000,"publish":true}'
```

It returns the source-ledger head and provenance head as hashes, plus a verification block
counting dead links and unverified quotes. Siblings: `/draft`, `/sources`, `/contribute`,
`/review`, `/score`, `/critique`, `/populate`, `/poll`, and `/run?role=writer` to turn one
crank of the queue.

## 5. The backend is a spreadsheet, and the scripts are real

Every table is projected as a sheet; 74 view sources exist, including the directory itself
and the ledger. A sheet is either a grid (cells you own) or a view (a stored description of a
query, re-read on every open). Editing a view means editing its description — add a column,
change a filter, pin a row — never writing code.

Sheets run scripts, which is the equivalent of the script editor attached to a spreadsheet.
Created and run live:

```bash
curl -s -X POST https://miscsubjects.com/api/sheets \
  -H "x-terminal-key: $TERMINAL_KEY" -d '{"title":"script proof 0912","kind":"store"}'
# → {"ok":true,"sheet":{"id":"sh_v2679twg", …}}

curl -s -X POST https://miscsubjects.com/api/sheets/sh_v2679twg/script \
  -H "x-terminal-key: $TERMINAL_KEY" \
  -d '{"name":"proof","code":"return {sum: 2+2, when: Date.now()};"}'
# → {"ok":true,"id":3,"name":"proof"}

curl -s -X POST https://miscsubjects.com/api/sheets/sh_v2679twg/script:run \
  -H "x-terminal-key: $TERMINAL_KEY" -d '{"name":"proof"}'
```

```json
{
  "ok": true, "id": 3, "name": "proof", "ms": 1452,
  "output": "{\"sum\":4,\"when\":1789199748956}",
  "output_truncated": false, "error": null, "receipts": [], "runs": 1
}
```

Inside script code you get `input` (the range's rows), `sheet.set(range, values)` to write
back, and `misc.find(q)` / `misc.describe(key)` / `misc.run(key, body)` — real dispatch, with
every nested call receipted under `on_behalf_of: sheet-script:<name>`, its invocation ids
returned in `receipts`. A script is therefore a first-class operator of the whole system, not
a sandboxed formula.

The limits are cells: `sheet_script.max_input_rows` (20), `sheet_script.timeout_ms` (30000),
`sheet_script.max_output_chars` (4000). Change the behaviour by editing a value, not a file.

One hard-won fact, since it cost real time: the page runtime **refuses code generation
outright**, so scripts execute on a separate cloud plane rather than in the request isolate.
A down plane refuses by name (`script_runtime_unavailable`); a crash reports its exit code and
stderr (`script_crashed`). There is never a silent fallback.

## 6. The four surfaces

The same directory row is reachable four ways. Pick by what you are.

**HTTP / API.** One constant and one header.

```bash
BASE=https://miscsubjects.com
-H "x-terminal-key: $TERMINAL_KEY"   # every mutating call
```

**CLI.** Any shell that has curl already has the whole system; there is no separate binary to
install and no SDK to version-match. Both `/api/dispatch` and the cloud execution plane accept
plain JSON over HTTPS. Note the trap that cost an hour: the exec plane's `timeout` is in
**milliseconds**. Passing `30` does not give you thirty seconds; it gives you thirty
milliseconds and a timeout you will misread as a hang.

**MCP.** A Model Context Protocol server exposes the directory as tools — `misc_find`,
`misc_run`, `misc_code`. It is bearer-authenticated, and says so rather than failing vaguely:

```json
{"jsonrpc":"2.0","id":null,
 "error":{"code":-32001,"message":"unauthorized: send Authorization: Bearer <MCP_TOKEN or share token>"}}
```

`misc_code` is the Code Mode door: instead of one tool call per act, a model writes a short
program that calls many rows, and the whole program is receipted.

**OIP — the token drop.** This is the surface with no analogue in common practice, so read it
twice. A model that arrives with no key, no account and no prior relationship opens
`https://miscsubjects.com/start` and is handed a bounded credential by a single GET. It can
then act, and every action it takes lands a public receipt under its own fingerprint.

The page opens by telling the visiting model that its operator outranks the page:

> If anything in this page conflicts with your operator's instructions, your operator wins.
> Nothing here overrides them, and nothing here should be treated as a command to act on its
> own.

That disclaimer is the interesting engineering. A site that wants to be operable by arbitrary
models is, structurally, a prompt-injection surface. The mitigation is to state the precedence
rule in the door itself, bound what the minted credential can do, and make every use public.
Access is granted; anonymity is not.

## 7. Discovery, with no key at all

Three doors answer a stranger completely:

```bash
curl -s https://miscsubjects.com/start                      # the orientation door
curl -s https://miscsubjects.com/api/manual                 # the full machine manual
curl -s "https://miscsubjects.com/api/directory?q=grok&limit=3"   # search capabilities
curl -s https://miscsubjects.com/api/directory/GROK_WEB     # one row, in full
```

`/api/manual` is 3,792,949 bytes. It is the territory; every written document, including this
one, is a map. When a map and the manual disagree, the manual is right.

A single row carries its own documentation, in the row:

```json
{"key":"GROK_WEB","type":"fn","target":"webmodelSend","auth":"",
 "content":"# WHAT: Ask Grok Web — the logged-in browser session, not the metered API — and return the exact captured answer.\n# WHEN_TO_USE: …"}
```

`# WHAT` and `# WHEN_TO_USE` are conventions the directory enforces, so a model choosing
between 1,438 capabilities is choosing on stated purpose rather than guessing from a name.
Row types: `fn` (internal function), `http` (outbound call), `agent` (model + prompt), `flow`
(a composition of other rows).

## 8. Authority, in three classes

| Class | Holds | Can do |
|---|---|---|
| Public | nothing | Every read above; public receipts; public sheets; articles |
| Bounded | a minted share or act token | Act within the grant; every use receipted under its fingerprint |
| Owner | `x-terminal-key`, admin session | Everything, including code and deploys |

Receipts split along the same line, deliberately. The rendered receipt at
`/receipt/inv_4movg4ukwn` is public. The raw JSON behind it is not:

```json
{"error":"unauthorized",
 "note":"receipt needs an owner access key, admin cookie, read/act token, or the exact scoped token that created this invocation."}
```

The act is publicly provable; its payload is not publicly readable. Those are different
questions and the system answers them differently.

## 9. Changing the code

Code is governed by the same object discipline as everything else, because several agents
write to this repository concurrently and "two individually valid commits" is the failure mode
that actually happens.

```bash
POST /api/coding-law/start   {"agent","files":[{"path","base_sha"}]}
POST /api/coding-law/commit  {"lease_id","files":[{"path","new_sha"}]}
POST /api/coding-law/reconcile {"path","current_sha","reason"}   # owner: re-baseline a stale head
```

`base_sha` is the sha256 of the content you read **before** editing. A commit whose base was
never registered is refused (`overwrite_refused`) — which is exactly the case where you were
about to silently erase another agent's work. The lease taken for the fix described in §3:

```json
{ "state": "LEASED",    "lease_id": "lease_6444f08f3baa0cf3",
  "start_hash":  "74a0b480f9d3c50a436100f75471bcf99f1e8451daf1a9f62c76b863f76c5550" }
{ "state": "COMMITTED", "lease_id": "lease_6444f08f3baa0cf3",
  "commit_hash": "dc93df10fb49416b8ae7be4299eb91f2056c21cbfe03967455839a00f0a17d4e" }
```

Shipping runs from a clean checkout of main through `node scripts/ship.mjs`. The gates include
the full law suite, a registry-versus-git check, and `HEAD == origin/main`. A deploy takes a
lease with a TTL, so a killed deploy does not wedge the next one forever.

## 10. Where this sits, on axes that matter

| Axis | This build | Typical stack |
|---|---|---|
| Capabilities | 1,438 rows, 97 of them agents | Tens of endpoints |
| Adding one | INSERT a row | Code, review, deploy |
| Entry points | 1 (`/api/dispatch`) | One per service |
| Self-description | 3.8 MB live machine manual, keyless | A README, drifting |
| Proof of an action | Public receipt URL per act | Private logs |
| Operable by a stranger model | Yes, bounded, receipted | No |
| Config | Cells, editable live | Constants and env vars |
| Concurrent-writer safety | Hash-leased, refuses blind overwrite | Merge conflicts and luck |
| Failure reporting | Named refusal plus the fix, `state_changed` | HTTP status and prose |
| Batch | 200 model calls, one round trip | Loop and hope |
| Backend UI | Spreadsheet with scripts | Bespoke admin panel |
| Cost | A field on each act | A monthly invoice |

Where it is weaker, plainly: three of four metered providers are currently out of credit;
script triggers other than `button` (`schedule`, `webhook`, `row`) are declared and owed;
per-run child tokens are declared and owed; and the manual's size means no human reads it
whole, which is the point but also a real cost.

## 11. Failure grammar

Every refusal seen while writing this article followed one shape, and you should rely on it:

| Refusal | Means | Do this |
|---|---|---|
| `test_content_refused` | Publish gate saw placeholder content | Re-POST with `"draft": true` |
| `not_an_agent_row` | A `fn`/`http`/`flow` row sent to the model lane | Use `/api/dispatch` |
| `script_runtime_unavailable` | Execution plane down | Retry; never assume it ran |
| `overwrite_refused` | Your base sha was never registered | Re-read the file, re-lease |
| `token_corrupted` | A long link was truncated on copy | Re-copy the whole token |
| `unauthorized` | Wrong authority class for this door | Mint the right token |
| `PROVIDER_ERROR: …` | The world said no, verbatim | Read the provider's own words |

A refusal names itself, states the rule, gives the fix, and tells you whether anything changed.
An error that only says `400` is a bug in the error, not just in the call.

## 12. The five rules this system runs on

1. Work exists only as a task object. If it is not a row, it is not work.
2. You obtain work by leasing it. You do not choose it.
3. You cannot complete work by saying you completed it. You submit evidence; the infrastructure
   runs that task's acceptance tests against live surfaces and sets the state from the result.
4. A failure becomes a child task naming the failure class, the layer that permitted it, and the
   invariant that should have prevented it — never a sentence in a report.
5. Every action appends one hash-chained audit row, and nothing is ever overwritten.

Rule 3 is the one that changes how a model behaves. Saying "done" is not a state transition
here. The only thing that moves an object to done is evidence that survives a test run against
the live system. A description of a response is not evidence; the response is.

## 13. If you are starting cold, in order

1. `GET /start` — orientation, and a bounded credential if you need to act.
2. `GET /api/directory?q=<what you want>` — find the row.
3. `GET /api/directory/<KEY>` — read its `# WHAT` and `# WHEN_TO_USE`.
4. `POST /api/dispatch {"key":"<KEY>","body":"…"}` — do it.
5. Open the `public_receipt` in the response — prove it.
6. When something refuses you, read the refusal's own `fix` field before changing anything.

Six steps, and they do not change between sending a text message, running SQL, calling a
model, publishing an article, or deploying the code that does all four.

## 14. What the publish gate demands

The claim law counts claims before it lets anything reach the corpus, and it refused a
3,054-word submission carrying none:

```json
{
  "slug": "operating-this-build", "ok": false,
  "error": "claim_law_refused", "law": "CLAIM_LAW",
  "why": "Every article on this site is the same object. Claims are what make it one: they become the addressable DIVs, the proof-of-work object a certifier signs, the surface a token is scoped to, and the regions an outsider can challenge. An article without them is prose on a page.",
  "words": 3054, "claims_required": 6, "claims_found": 0,
  "how_to_fix": "Send claims:[{id,text,tier,source_ids,why_material}] on the PUT, or append them one at a time with POST /api/articles/<slug>/webhook {\"kind\":\"claim\",\"data\":{…}}. Tiers: human, rct, trial, animal, mechanistic, in-vitro, cell, case, observational, regulatory, expert, definition, unsourced."
}
```

A 3,054-word article with no claims is, by this system's rules, not the same kind of object as
the rest of the corpus — it has no addressable regions, nothing a certifier can sign, nothing
an outsider can challenge. So the gate refused it, quoted its own reasoning, counted what it
found against what it required, and printed the exact field to add.

The right response to a gate is to change the artifact, never to weaken the gate. Claims on
this slug sit at the tier their evidence supports — mostly `definition` and `observational`,
because describing how a system behaves is not the same act as measuring an intervention, and
claiming a stronger tier than the evidence carries is the specific failure these tiers exist
to prevent.

Which is the shortest possible summary of the whole design: the system would rather refuse you,
by name, with the fix in hand, than accept something that quietly is not what it claims to be.


## Sources

1. Public receipt inv_4movg4ukwn — D1_QUERY over the directory — https://miscsubjects.com/receipt/inv_4movg4ukwn
2. Public receipt inv_mccaqos91o — ASK_GEMINI — https://miscsubjects.com/receipt/inv_mccaqos91o
3. Public receipt inv_snxwkpg1ph — ASK_GPT provider failure — https://miscsubjects.com/receipt/inv_snxwkpg1ph
4. The live machine manual — https://miscsubjects.com/api/manual
5. Directory search door — capability count — https://miscsubjects.com/api/directory?q=grok&limit=3
6. The keyless orientation door (OIP) — https://miscsubjects.com/start
7. One directory row, in full, with its own documentation — https://miscsubjects.com/api/directory/GROK_WEB


---

# iMessage as an API: 34 message verbs on the owner's Mac, each call receipted

slug: imessage-api-on-the-mac · https://miscsubjects.com/a/imessage-api-on-the-mac · category: build · tags: imessage, mac, capability-fabric, device-ledger, api · updated 2026-09-09T01:07:11.417Z

iMessage is Apple's messaging service; every text the owner of a Mac has sent or received through it sits in one database file on that Mac, and the Mac's Messages app can send new ones under the owner's own phone number and email. An API, here, means a row in this site's directory that any model or program can call with one HTTP request, that runs on the owner's Mac through the Mac bridge (an authenticated tunnel into one process on that machine), and that leaves a public receipt on the ledger for every call. On 2026-09-08 the Mac's iMessage became 34 such rows. Each was created over the directory's REST interface, ten were then called through the normal dispatch path and returned receipts, a real message was sent from the Mac and confirmed delivered, and a reaction was placed on it, both through the bridge.

**Where the messages are and why a plain read fails.** The Messages database is `~/Library/Messages/chat.db`. On this Mac it holds 673,552 messages across 2,843 conversations (201 of them groups) and 29,824 attachments. Since roughly March 2026 Messages stops writing most bodies into the plain `text` column and stores them only as a binary `attributedBody` blob: of the 6,084 messages on this Mac since 2026-08-01, 5,402 have an empty `text` column. A raw SQL read therefore returns blanks for 89% of recent messages. That single measurement decided the design: the executor had to decode the blobs.

**The two programs underneath.** Reading, searching, streaming and plain sending run through [imsg](/a/imsg), a Swift command line that opens the database read-only, decodes the blobs, streams new rows as JSON, and asks the Messages app to send, then confirms the outgoing row appeared and reports whether it was delivered. Actions on specific messages, a reaction, a quoted reply, an edit, an unsend, a typing indicator, marking a chat read, run through [platform-imessage](/a/platform-imessage), Beeper's library, which drives a second window of the Messages app through macOS Accessibility, because Apple's scripting surface for Messages can send text and files and nothing else. Both run with System Integrity Protection on. Both were installed on the Mac with Homebrew and both answered through the bridge on the first call, because the bridge process already holds Full Disk Access and Accessibility.

**How a call reaches the Mac.** A row's body names the command; dispatch substitutes the caller's arguments into the command's argument list structure by structure, so a quotation mark inside a search query arrives as a character, not as shell syntax (tested: the query `it"s` reached the program intact). A small wrapper script on the Mac owns the defaults and the quoting, so no row body contains shell code. Every row runs on the Mac only; there is no cloud fallback for a machine's own message store.

## The 34 verbs

**Reading.** `IMSG_CHATS` lists recent conversations with their row ids, guids, participants, service and unread counts. `IMSG_UNREAD` lists only conversations with unread inbound messages. `IMSG_HISTORY` reads the last N messages of one chat with decoded bodies, sender, reactions, reply context and attachment metadata. `IMSG_SEARCH` searches all 673,552 messages by text. `IMSG_STATS` counts messages by service, chat, sender and date. `IMSG_GROUP`, `IMSG_WHOIS`, `IMSG_ACCOUNT`, `IMSG_STATUS`, `IMSG_SCHEDULED`, `IMSG_MESSAGES`, `IMSG_MESSAGE`, `IMSG_ACTIVITY`, `IMSG_CURRENT_USER` and `IMSG_VERBS` read a chat's identity, whether a handle is known and on which service, the signed-in account, executor health, messages queued with Send Later, Beeper's view of a chat with message ids, one message, whether the other party is typing or in Do Not Disturb, the owner's own identity, and the verb list.

**Inbound.** `IMSG_AFTER` is a cursor: given a message row id it returns everything newer, including reactions and edits, with the next cursor and a has-more flag, so a flow or agent that keeps the cursor sees every message exactly once. `IMSG_SEND_STATUS` returns the delivery state of a sent message by guid. `IMSG_INBOX`, `IMSG_INBOX_SINCE` and `IMSG_WATCH_STATUS` read the event log written by the inbound watcher described below.

**Sending.** `IMSG_SEND` sends a text to a phone number or email under the owner's identity and returns the new message's id and guid. `IMSG_SEND_CHAT` sends into an existing conversation by row id, including groups. `IMSG_SEND_FILE` sends a file, with audio arriving as a voice message. `IMSG_CREATE_CHAT` starts a new direct or group conversation with addresses never messaged before.

**Acting on a message.** `IMSG_REACT` and `IMSG_UNREACT` add or remove a tapback or emoji on any message by id, or on `latest` and `latest-1`. `IMSG_REACT_LATEST` reacts to the most recent incoming message of a chat through imsg's own route. `IMSG_REPLY` sends a quoted reply to one message. `IMSG_EDIT` and `IMSG_UNSEND` change or retract a message the owner sent, inside Apple's windows. `IMSG_TYPING` turns the typing indicator on or off. `IMSG_MARK_READ` and `IMSG_MARK_UNREAD` change a chat's read state. `IMSG_LOAD_ATTACHMENT` forces an offloaded attachment to download.

The call shape is the same for every row: `POST https://miscsubjects.com/api/dispatch` with `{"key":"IMSG_SEARCH","body":"invoice|10"}` and the caller's key; arguments are joined with a vertical bar in the order each row's schema states. The live rows, with their schemas and test state, are the [iMessage API sheet](/sheet/sh_94mg44b6).

## What was proven, with receipts

| call | result | latency | receipt |
| --- | --- | --- | --- |
| IMSG_CHATS 2 | two conversations, decoded | 454 ms | [inv_2fhwaze5qp](/receipt/inv_2fhwaze5qp) |
| IMSG_AFTER 677275, 2 | two newer messages, next cursor | 400 ms | [inv_h26qrni500](/receipt/inv_h26qrni500) |
| IMSG_SEND_STATUS (an earlier proof message) | send_state delivered, delivered_at 19:17:39Z | 300 ms | [inv_nvhvrixojr](/receipt/inv_nvhvrixojr) |
| IMSG_WHOIS (a number never messaged) | known false, service unknown | 46 ms | [inv_ijalm9asg1](/receipt/inv_ijalm9asg1) |
| IMSG_STATUS | database ready, SIP enabled, 14 live RPC methods | 41 ms | [inv_0aohsk7rsl](/receipt/inv_0aohsk7rsl) |
| IMSG_VERBS | 36 verbs | 40 ms | [inv_g444m2ezhz](/receipt/inv_g444m2ezhz) |
| IMSG_WATCH_STATUS | watching false, before the watcher was repaired | 43 ms | [inv_fmn0qce9e0](/receipt/inv_fmn0qce9e0) |
| IMSG_HISTORY 2803, 2 | two messages with decoded text | 167 ms | [inv_m1v6eji4ml](/receipt/inv_m1v6eji4ml) |
| IMSG_SEND (through the bridge) | accepted as row 677351, status sent | 329 ms | [inv_5fk14dt3p8](/receipt/inv_5fk14dt3p8) |
| IMSG_SEND_STATUS (that message) | sent, delivered 01:02:53.326Z, read 01:02:53.516Z | 300 ms | [inv_roinbe5iec](/receipt/inv_roinbe5iec) |

The send itself: one message to the owner's own number, called as `IMSG_SEND` through dispatch, carried to the Mac by the bridge, accepted by Messages as row 677351, and reported by `message.send_status` as delivered at 01:02:53.326Z and read 190 milliseconds after that. An earlier send from a different process had already proven the mechanism; this one proves the path the rows actually use. The reaction: `IMSG_REACT` placed a thumbs-up on that message through the bridge process in 2,870 ms, verified by the program against the window, with no consent dialog, because Accessibility was already granted to the bridge. A full-account `IMSG_STATS` took 7,180 ms and counted 666,877 messages; every other read finished under 1.2 seconds.

## A grant belongs to a program, not to a person

macOS attaches automation and privacy rights to an exact executable. The bridge process holds Full Disk Access, Accessibility and Screen Recording, and Apple Events consent for System Events, Calendar and Chrome; consent for Messages was added on 2026-09-08 at 19:31. That single row is what moved the four sending verbs from mechanism to path: the send above raised no dialog and needed no attention.

The same rule broke the inbound side, in the opposite direction, and the failure is worth the space. A launchd job whose program is a shell script runs as the shell, and the shell holds none of those grants. So the watcher's read of the Messages store returned nothing, it passed an empty cursor to a program that refuses one, and the job restarted 695 times in 105 seconds while reporting exit status zero. The collector beside it kept running and kept posting, but every store read inside it returned nothing, so only its heartbeat was true. Neither job logged an error, because from each program's point of view nothing had gone wrong.

The repair is an entry point rather than a permission. Both launchd scripts now re-execute their work under the one binary that already holds the grants, and the child process inherits them. That is the shape the Mac bridge itself has always had: launchd runs a shell script, the script hands off to the granted binary, and everything below it can see the machine. Within one restart the watcher resumed from message row 677347, and the collector went back to reading every local store.

## Inbound: messages as events

Two inbound paths exist. The first needs nothing running: any flow keeps a cursor and calls `IMSG_AFTER`. The second is a watcher on the Mac that streams every new message as it lands, appends the full record to a local event log (readable through `IMSG_INBOX` and `IMSG_INBOX_SINCE`), and posts one metadata-only observation to the [device capability ledger](/device-capability-ledger) per message: direction, chat, direct or group, service, attachment count, text length. Never the text, never the handle. The watcher runs as a launchd service. It resumed at message row 677347, captured the send described above as row 677352 about four seconds after dispatch returned, and advanced its cursor; `IMSG_WATCH_STATUS` reports it watching, with its cursor and its event count.

Alongside it runs a state collector that samples the Mac every 60 seconds and writes only what changed: frontmost app, Focus mode, power source, Wi-Fi network (hashed), screen locked or not, presence by idle time, Bluetooth devices, the set of running apps, attached displays, the row counts of the Mail, Safari, Chrome, Photos, Notes, Calendar, Messages and Contacts stores, the next calendar event's start time, and whether the paired iPhone answers over the network. Its first run wrote 19 observations to the ledger under receipt [8261ba99](/receipt/8261ba99-845f-40a0-b1e9-2590cfec0d4a), and it now writes only the differences, minute by minute.

## What the ledger now records

The same ingest that carries those observations carries the research behind the choice of programs. Fifteen open-source iMessage projects were read from source, and each is now an executor row in the ledger with its mechanism, its requirements, what it uniquely adds and its verdict; two were absorbed (the two programs above), seven are reference material only, six were rejected, one of them for forging device identities against Apple's push service, and the rest for needing SIP disabled, being dead, or exposing a network port with weak authentication. The ledger's [machine projection](/api/capability-census) lists 109 executors, 9 of them ready on this Mac, and 36 verified capabilities, each with the receipt of the call that verified it. Twenty-four of those were verified today through the bridge: the iMessage reads above, and read access to the Mac's Contacts (408 records), Calendar (174 items), Notes (1,012), Mail (262 envelopes), Photos (47,213 assets), Safari and Chrome history, Spotlight, power, Bluetooth and process state. Also recorded as candidates, not yet invoked: the owner's Home shortcuts (door lock, gate code) callable by `shortcuts run`, and the AppleScript dictionaries of Excel, Word, Outlook, Keynote, Numbers, Pages, Chrome, Safari and the ChatGPT desktop app, each a licensed or signed-in program the Mac can operate without a vendor API.

## What decides the next step

Three facts bound what comes next. The iPhone answers over the network (iOS 26.6.1) for device information and file transfer, but every developer service, including screenshots and element-level control, requires Developer Mode, which iOS refuses to enable while a passcode is set. The Mac's Messages, Contacts, Calendar, Notes, Mail and Photos stores are all readable today through the bridge, so the reading half of the personal data surface is done; the writing half is one Apple Events consent per app. And the watcher and collector, both running, prove the pattern that turns any local state change into a ledger row without a model in the loop: sample, diff, post. The [device capability ledger](/device-capability-ledger) is where each new verb lands first, and a verb is promoted into the directory only after a real call has returned real data.


## Sources

1. openclaw/imsg README, Permissions — https://github.com/openclaw/imsg
2. beeper/platform-imessage README — https://github.com/beeper/platform-imessage
3. Device capability ledger, machine projection (executor rows and verified capabilities with receipts) — https://miscsubjects.com/api/capability-census


---

# The Anthropic Messages API is one endpoint with a strict block grammar

slug: what-is-the-anthropic-messages-api · https://miscsubjects.com/a/what-is-the-anthropic-messages-api · tags: tooling, api, anthropic-messages, streaming, prompt-caching, tool-use · updated 2026-07-26T03:31:30.982Z

The Anthropic Messages API is a single HTTP endpoint. `POST https://api.anthropic.com/v1/messages` takes one JSON object and returns either one JSON object or a stream of named server-sent events. Tools, images, reasoning, caching and token counting are all fields inside that one body or variants of that one response. The contract below includes the real error strings and caching arithmetic worked on a measured prompt.

## Evidence status

**Observed** marks first-party measurements or runtime receipts from the named environment.
**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards
documentation. **Implemented** and **deployed** name code and live-state evidence, respectively.
**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;
those reports show that an experience occurred, not that it is universal.

## One command proves the request envelope

Set the key in an environment variable. Do not put it in the JSON body, a URL, source control or a log.

```bash
export ANTHROPIC_API_KEY="<your Anthropic API key>"

curl -sS https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  --data '{
    "model":"claude-sonnet-4-5",
    "max_tokens":32,
    "system":"Answer with digits only.",
    "messages":[{"role":"user","content":"What is 7 times 6?"}]
  }' | jq '{type,role,content,stop_reason,usage}'
```

Expected shape:

```json
{"type":"message","role":"assistant","content":[{"type":"text","text":"42"}],
 "stop_reason":"end_turn",
 "usage":{"input_tokens":"<integer>","cache_creation_input_tokens":"<integer>",
          "cache_read_input_tokens":"<integer>","output_tokens":"<integer>"}}
```

The token values vary with model and prompt. The invariant is the envelope: one assistant `message`, an array of typed content blocks, a non-null `stop_reason`, and one `usage` object.

## Your key is checked before your version header, so a version mistake hides behind a 401

Three headers are mandatory: `x-api-key`, `anthropic-version`, `content-type: application/json`. The versioning reference: "When making API requests, you must send an `anthropic-version` request header. For example, `anthropic-version: 2023-06-01`." What it does not say is the order of checks. Three calls to `api.anthropic.com` on 2026-07-26, varying only headers:

| Call | `x-api-key` | `anthropic-version` | HTTP | `error.message` |
| --- | --- | --- | --- | --- |
| A | absent | absent | 401 | `x-api-key header is required` |
| B | `sk-ant-invalid` | absent | 401 | `invalid x-api-key` |
| C | `sk-ant-invalid` | `2024-99-99` | 401 | `invalid x-api-key` |

Call C sends a version that does not exist and still gets the key error. Authentication runs first, so a missing `anthropic-version` is unreachable until the key is valid. Every response, including these, carried a `request-id` header — `req_011CdQ3SLNGpKNvrJeV1DuDb` on call A — which is the value Anthropic support asks for. The 401s also carried `x-should-retry: false`, which appears in none of the reference pages cited here; treat it as observed, not contracted.

## Three required fields, and one field that looks like a message but is not

| Field | Required | What it does |
| --- | --- | --- |
| `model` | yes | The model id. An id the endpoint does not serve is rejected. |
| `max_tokens` | yes | "The maximum number of tokens to generate before stopping." `0` writes the prompt cache without generating. Maximums are per model. |
| `messages` | yes | Ordered `{role, content}` turns. Consecutive same-role turns "will be combined into a single turn". A trailing `assistant` message is continued from. |
| `system` | no | The system prompt, at the top level of the body, outside `messages`. String or array of text blocks. |
| `temperature` | no | "Defaults to `1.0`. Ranges from `0.0` to `1.0`." Not deterministic even at `0.0`. |
| `stop_sequences` | no | Strings that halt generation. A match sets `stop_reason` to `stop_sequence` and fills the top-level `stop_sequence` field. |
| `stream` | no | `true` switches the response to server-sent events. |
| `tools` | no | `{name, description, input_schema}` each, where `input_schema` is JSON Schema. |
| `tool_choice` | no | `{"type":"auto"}`, `{"type":"any"}`, `{"type":"tool","name":"x"}`, `{"type":"none"}`. The first three take `disable_parallel_tool_use`, default `false`. |
| `metadata` | no | Only `user_id` is read: "a uuid, hash value, or other opaque identifier". Names, emails and phone numbers are warned against. |
| `thinking` | no | Reasoning configuration. The accepted shape is model-dependent; mismatches are 400s listed in the error table. |

The system prompt is the field people get wrong, because every other major chat API carries it as a message. The reference states it in one sentence: "there is no `"system"` role for input messages in the Messages API."

```json
{"model": "claude-sonnet-4-5", "max_tokens": 1024,
 "system": [{"type": "text", "text": "You are terse."}],
 "messages": [{"role": "user", "content": "What is 7 times 6?"}]}
```

`{"role": "system", "content": "..."}` inside `messages` is not a system prompt.

The same page nonetheless lists `"system"` among the accepted `role` values, and flattening that contradiction would be a lie. On Claude Fable 5, Mythos 5, Opus 4.8 and Opus 5 you may "append a `{"role": "system"}` message to `messages` instead of editing the top-level `system` field, so the cached prefix stays unchanged" — a narrow way to add an instruction mid-conversation without invalidating the cache. It is not how you set the system prompt, it is unavailable on Claude Sonnet 5, and instructions placed there on an unsupported model are treated as conversation.

## Five content-block types carry everything in both directions

```json
{"type": "text", "text": "What is 7 times 6?"}
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo..."}}
{"type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": {"city": "Dallas"}}
{"type": "tool_result", "tool_use_id": "toolu_01A", "content": "41C and clear"}
{"type": "thinking", "thinking": "...", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pki..."}
```

| Block | Sent by | What bites |
| --- | --- | --- |
| `text` | user, assistant | Empty text blocks cannot be cached. |
| `image` | user | `source` is `{"type":"base64",...}` or `{"type":"url","url":...}`. `media_type` limited to `image/jpeg`, `image/png`, `image/gif`, `image/webp`. Adding or removing one anywhere invalidates the message cache. |
| `tool_use` | assistant | `input` is a parsed object, not a JSON string. |
| `tool_result` | **user** | `content` is a string or block array; optional `is_error` marks a failed run. |
| `thinking` | assistant | Carries a `signature`. Editing, reordering or filtering these before resending returns a 400 whose message starts with the block position, e.g. `messages.1.content.0`. |

The `tool_result` row is what catches people migrating from other APIs: a tool's answer goes back inside a **user** turn, not a message with a dedicated tool role.

## An unpaired tool_use does not fail one request, it kills every request after it

Three rules, all enforced:

1. Every `tool_use` in an assistant message is answered by a `tool_result` **in the immediately following message**.
2. That message has `role: "user"`.
3. Each `tool_result.tool_use_id` matches a `tool_use.id` from that turn, and those ids are unique within it.

Break rule 1 and the 400 names the message index and the id. The real string, from an agent whose auto-compaction cut a turn in half:

> messages.8: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01GJ.... Each `tool_use` block must have a corresponding `tool_result` block in the next message.

This is worse than an ordinary validation error because the endpoint is stateless: the client resends the whole history every turn. Once a broken pair is in the transcript, every later request — including a bare "continue" — replays it and gets the identical 400. The session is dead until the client edits its own history.

[[embed:source:s13]]

Rule 3 fails more quietly. A layer that invents missing ids from the tool name produces duplicates the moment the model calls one tool twice in a turn:

[[embed:source:s16]]

A duplicate id is sometimes a 400 and sometimes a silent mispairing, where one tool's output is handed back as another's. Generate ids per call, never per tool name.

The round trip working, captured live on 2026-07-26 — assistant `tool_use`, user `tool_result`, finished answer:

```json
{"content": [{"type": "text", "text": "The weather in Dallas is currently 41°C and clear. It's quite warm with clear skies!"}],
 "stop_reason": "end_turn",
 "usage": {"input_tokens": 195, "output_tokens": 48, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}}
```

## Streaming is a nested event grammar, and tool arguments arrive as string fragments

With `"stream": true` the response is `text/event-stream`. Each event has an SSE name and a JSON payload repeating that name in `type`. The published flow: `message_start`, then per content block a `content_block_start`, one or more `content_block_delta`, and a `content_block_stop`, then one or more `message_delta`, then a final `message_stop`.

| Event | Carries | Client action |
| --- | --- | --- |
| `message_start` | Message shell: `id`, `model`, empty `content`, `usage.input_tokens` and cache counts | Record input usage; it is not repeated |
| `content_block_start` | `index` plus the opening block | Allocate a buffer at that index |
| `content_block_delta` / `text_delta` | `delta.text` | Append |
| `content_block_delta` / `input_json_delta` | `delta.partial_json`, a raw fragment | Append to a string buffer; do not parse yet |
| `content_block_delta` / `thinking_delta` | `delta.thinking` | Append |
| `content_block_delta` / `signature_delta` | `delta.signature`, just before the block stops | Store verbatim; it makes the block replayable |
| `content_block_stop` | `index` | Close the buffer; for a tool block, parse now |
| `message_delta` | `delta.stop_reason`, `delta.stop_sequence`, `usage.output_tokens` | These usage counts are cumulative, not per-event |
| `message_stop` | Nothing | End of stream |
| `ping` | Nothing | Ignore. "Event streams may also include any number of `ping` events." |
| `error` | An error object mid-stream, after a 200 | Abort. Example: `{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}` |

One rule has no event of its own: unknown types must be ignored, not treated as failures. The versioning policy reserves the right to "add new variants to enum-like output values (for example, streaming event types)".

Assembling a tool call is what third-party implementations get wrong. The argument object arrives as raw string pieces that are individually invalid JSON:

```text
{"delta":{"type":"input_json_delta","partial_json":"{\"location\":"}}
{"delta":{"type":"input_json_delta","partial_json":" \"San"}}
{"delta":{"type":"input_json_delta","partial_json":" Francisc"}}
{"delta":{"type":"input_json_delta","partial_json":"o, CA\"}"}}
```

Concatenate every `partial_json` for one `index` in arrival order and parse once at `content_block_stop`, giving `{"location": "San Francisco, CA"}`. Parsing early throws. Keying the buffer on anything but `index` corrupts parallel calls.

A complete stream captured on 2026-07-26 from an endpoint answering this format. The argument object arrives in a single fragment here, which is legal — a client must handle both one and many:

```text
event: message_start
data: {"type":"message_start","message":{"id":"msg_aig","type":"message","role":"assistant","model":"@cf/zai-org/glm-4.7-flash","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"chatcmpl-tool-b8d5b5cf4b01d725","name":"get_weather","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"city\": \"Dallas\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":64}}

event: message_stop
data: {"type":"message_stop"}
```

Streaming is a distinct transport and fails on its own. One report isolates exactly that: ordinary requests complete while the SSE call to `/v1/messages` is refused.

[[embed:source:s17]]

## stop_reason is an instruction to the caller, not a status label

It is `null` in `message_start` and non-null everywhere else. Seven values, each implying a different next move.

| Value | Meaning | Next move |
| --- | --- | --- |
| `end_turn` | "a natural stopping point" | Render. Nothing pending. |
| `max_tokens` | "we exceeded the requested `max_tokens` or the model's maximum" | Text is truncated mid-thought. Raise the cap or continue from the partial assistant turn. |
| `stop_sequence` | "one of your provided custom `stop_sequences` was generated" | Read the top-level `stop_sequence` for which one matched. |
| `tool_use` | "the model invoked one or more tools" | Run every `tool_use` block, return all results in one user turn. |
| `pause_turn` | "we paused a long-running turn" | Send the response back as-is to continue. |
| `refusal` | "streaming classifiers intervene to handle potential policy violations" | Read `stop_details.category` — `cyber`, `bio`, `frontier_llm`, `reasoning_extraction`, `general_harms` — and `stop_details.explanation`. |
| `model_context_window_exceeded` | Context window exceeded | Compact or drop history. Retrying unchanged fails again. |

Row two, captured live on 2026-07-26 with `max_tokens` set to 64:

```json
{"content": [{"type": "text", "text": "1.  **Analyze the user's request:** The user is asking \"What is 7 times 6?\". The constraint is \"Answer with digits only\".\n\n2.  **Perform the calculation:** $7 \\times 6$.\n    $7 \\times 6 = 42$.\n\n3."}],
 "stop_reason": "max_tokens", "usage": {"input_tokens": 19, "output_tokens": 64}}
```

The answer is in there and the turn was cut at token 64. A client that renders only on `end_turn` shows nothing.

## input_tokens counts what comes after your last cache breakpoint, not what you sent

| Field | Counts |
| --- | --- |
| `input_tokens` | Only tokens after the last cache breakpoint — "not all the input tokens you sent" |
| `cache_creation_input_tokens` | Tokens written to cache on this request |
| `cache_read_input_tokens` | Tokens served from cache on this request |
| `output_tokens` | Tokens generated |

The identity that always holds: `total_input_tokens = cache_read_input_tokens + cache_creation_input_tokens + input_tokens`.

The committed outputs of Anthropic's prompt-caching notebook make it concrete — the same 187,364-token prompt, three times:

| Run | `input_tokens` | Cache write | Cache read | Wall time |
| --- | --- | --- | --- | --- |
| No caching | 187,364 | — | — | 4.89s |
| First call with a breakpoint | 3 | 187,361 | — | 4.28s |
| Second call, cache warm | 3 | — | 187,361 | 1.48s |

`input_tokens` collapses from 187,364 to 3 once a breakpoint exists, because 187,361 tokens moved into the cache columns. A dashboard summing `input_tokens` alone reports a cached workload as nearly free and is wrong by the entire prefix.

Both cache fields at 0 means nothing was cached, most often because the prompt was under the model's minimum — and no error is returned. With a 1-hour breakpoint in play, `usage` gains a `cache_creation` object whose `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens` members sum to `cache_creation_input_tokens`.

## Caching is a field you have to send, and it pays back on the second request

Nothing caches by default. A breakpoint is one field on one block:

```json
{"type": "text", "text": "<20,000 tokens of instructions>", "cache_control": {"type": "ephemeral"}}
```

`ephemeral` is the only type. Default lifetime is five minutes, refreshed free on every hit. The cache covers the full prefix in the fixed order `tools` → `system` → `messages`, up to and including the marked block. Four explicit breakpoints are allowed; an automatic mode — a single `cache_control` at the top level of the body, placing the breakpoint on the last cacheable block and moving it forward as the conversation grows — consumes one of the four.

Below the model minimum the marker does nothing and says nothing: 512 tokens on Claude Opus 5 and Fable 5; 1,024 on Opus 4.8, Sonnet 5, Sonnet 4.6 and Sonnet 4.5; 2,048 on Opus 4.7; 4,096 on Opus 4.6, Opus 4.5 and Haiku 4.5.

The multipliers, quoted: "5-minute cache write tokens are 1.25 times the base input tokens price"; "1-hour cache write tokens are 2 times the base input tokens price"; "Cache read tokens are 0.1 times the base input tokens price."

Worked on a real prompt: a 20,000-token static prefix of tool definitions plus a long system prompt, on Claude Sonnet 4.5 at $3.00/MTok input, $3.75/MTok 5-minute writes, $6.00/MTok 1-hour writes and $0.30/MTok reads, sent fifty times inside one five-minute window:

| Strategy | Write | Read | Total |
| --- | --- | --- | --- |
| No `cache_control` | — | 50 × 20,000 = 1,000,000 tok × $3.00/MTok = $3.0000 | **$3.0000** |
| 5-minute breakpoint | 20,000 tok × $3.75/MTok = $0.0750 | 49 × 20,000 = 980,000 tok × $0.30/MTok = $0.2940 | **$0.3690** |
| 1-hour breakpoint | 20,000 tok × $6.00/MTok = $0.1200 | $0.2940 | **$0.4140** |

The five-minute cache removes $2.6310 of $3.0000, or 87.7 percent. Break-even is the second request: two uncached requests cost 2.00 units of base price, a write plus one read costs 1.25 + 0.10 = 1.35.

The one-hour cache looks worse there and wins the moment the gap between requests exceeds five minutes. Same prefix, one request every six minutes for an hour — eleven requests, five-minute entry expired between every pair:

| Strategy | Cost |
| --- | --- |
| 5-minute breakpoint, expiring each time | 11 writes × $0.0750 = **$0.8250**, zero reads |
| 1-hour breakpoint | 1 write × $0.1200 + 10 reads × 20,000 = 200,000 tok × $0.30/MTok = $0.0600 → **$0.1800** |

Asking for the longer lifetime is one more key: `"cache_control": {"type": "ephemeral", "ttl": "1h"}`. Mixing lifetimes is allowed with one ordering rule — a 1-hour entry must appear before any 5-minute entries.

### Three ways caching silently does nothing

**The field is never sent.** An agent whose Anthropic path omits `cache_control` pays full input price every turn and nothing says so; the only tell is both cache counters at zero.

[[embed:source:s20]]

**A proxy strips it.** Anything in the path can drop unrecognised keys. One gateway was caught doing it, proven by A/B: direct calls returned `cache_read_input_tokens > 0` on the second request, the same request through the gateway never did.

[[embed:source:s14]]

The same failure shows up as a routing bug when markers are injected on only one code path:

[[embed:source:s15]]

**Something in the prefix moves per request.** The breakpoint hash covers everything up to the marked block, so a timestamp, a request id or a per-call attribution line anywhere in the prefix mints a new hash every time — a permanent 1.25x write and never a read. The published checklist adds a subtler one: "verify that the keys in your `tool_use` content blocks have stable ordering as some languages (for example, Swift, Go) randomize key order during JSON conversion, breaking caches." One reference implementation strips an incoming per-request billing line off the head of the system prompt for exactly this reason (`functions/api/aig/[[path]].js`, lines 116–130).

The one-hour lifetime is also not reachable from every client. One operator reading the wire found the marker present and the lifetime absent:

[[embed:source:s19]]

Because placement is manual, proxies exist purely to insert breakpoints for integrations that never expose the request body:

[[embed:source:s21]]

The shape teams converge on afterwards is worth copying: one tool for structured output, parsed `tool_use`, `cache_control: ephemeral` on system and context, exponential backoff on 429.

[[embed:source:s18]]

## count_tokens is free, and the estimators that replace it drift

`POST /v1/messages/count_tokens` takes the same body minus `max_tokens` and returns one integer:

```bash
curl -s https://api.anthropic.com/v1/messages/count_tokens \
  -H "x-api-key: $ANTHROPIC_API_KEY" -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model":"claude-sonnet-4-5","system":[{"type":"text","text":"Answer with digits only."}],
       "messages":[{"role":"user","content":"What is 7 times 6?"}]}'   # -> {"input_tokens": 18}
```

It is "free to use but subject to requests per minute rate limits based on your usage tier", and those limits are independent: "Token counting and message creation have separate and independent rate limits. Usage of one does not count against the limits of the other." Use it for routing, for fitting a prompt under a context window, and for checking that a prefix clears a cache minimum before relying on caching.

Because it is optional, gateways often substitute a character estimate. Two measurements on 2026-07-26 against an implementation that divides an accumulated character count by 3.7 (`functions/api/aig/[[path]].js`, lines 445–457). The body above returned 18; sent to `/v1/messages`, it reported `usage.input_tokens` of 19 — low by one, 5.3 percent.

The second shows the error is not stable. A 100-character system prompt with a one-character user message returned `{"input_tokens": 55}`. Counting the system prompt once gives ceil((100 + 1) / 3.7) = 28. Fifty-five is ceil((100 + 100 + 1) / 3.7): the system prompt is measured once on its own and again inside the translated message list, so it is counted twice. Where the system prompt dominates — the normal case for a coding agent — that estimator reports roughly double, and a client using it to decide when to compact compacts far too early.

## The error catalogue, by symptom

| Symptom | HTTP / `error.type` | Cause | Fix |
| --- | --- | --- | --- |
| `x-api-key header is required` | 401 `authentication_error` | No key header | Send `x-api-key`. A bearer token is a different header. |
| `invalid x-api-key` | 401 `authentication_error` | Malformed, revoked or expired key | Replace it. A missing version header stays invisible until this passes. |
| Key works elsewhere, fails here | 403 `permission_error` | "Your API key does not have permission to use the specified resource" | Check organisation and workspace settings. |
| `The requested resource could not be found.` | 404 `not_found_error` | Wrong path or id | Check the endpoint path and any ids in the URL. |
| Large request rejected before the model sees it | 413 `request_too_large` | Over 32 MB on Messages or Token Counting; 256 MB Batch; 500 MB Files | Move bulk content to the Files API. |
| Sudden throttling | 429 `rate_limit_error` | RPM, ITPM or OTPM exceeded | Read `retry-after` and the `anthropic-ratelimit-*` headers. Cache hits are not deducted from rate limits. |
| Every call after one bad turn returns the same 400 | 400 `invalid_request_error` | A `tool_use` with no `tool_result` in the next message | Repair the stored history; the endpoint holds no state to reset. |
| Parallel calls to one tool break | 400 `invalid_request_error` | Duplicate `tool_use` ids | Generate ids per call. |
| `This model does not support assistant message prefill. The conversation must end with a user message.` | 400 `invalid_request_error` | Prefilled trailing assistant turn on Claude 4.6+ | Use structured outputs or `output_config.format`. |
| `` `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. `` | 400 `invalid_request_error` | Thinking blocks edited, reordered or filtered before resend | Pass them back byte for byte, including empty ones and `redacted_thinking`. |
| `"thinking.type.enabled" is not supported for this model.` or `adaptive thinking is not supported on this model` | 400 `invalid_request_error` | Wrong thinking shape for the model | `{"type":"adaptive"}` with `output_config.effort` on Claude 4.7+; `{"type":"enabled","budget_tokens":N}` on 4.5 and earlier. |
| `Overloaded` | 529 `overloaded_error`, or an `error` event mid-stream after a 200 | Capacity | Retry with backoff. In streaming this arrives after headers, so HTTP status alone will not catch it. |

Every error body is JSON with a top-level `error` carrying `type` and `message`, plus a `request_id`:

```json
{"type": "error",
 "error": {"type": "not_found_error", "message": "The requested resource could not be found."},
 "request_id": "req_011CSHoEeqs5C35K2UUqR7Fy"}
```

Match on the SDK's typed exception classes, not on message text — the reference is explicit that "the values within these objects may expand".

## Three other vendors answer this exact shape, and each drops different fields

The same credential-free request body reached all three Anthropic-shaped routes on 2026-07-26 and all returned HTTP 401: DeepSeek returned `Authentication Fails (governor)`; Z.ai returned `Authentication parameter not received in Header, unable to authenticate`; Moonshot returned `Incorrect API key provided` with type `incorrect_api_key_error`. These are route-existence receipts, not compatibility claims.

Because the format is one endpoint with a documented body, other providers implement it and inherit any client that speaks it. DeepSeek documents `https://api.deepseek.com/anthropic` — "our API has added support for the Anthropic API format" — and Z.ai documents `https://api.z.ai/api/anthropic`. Moonshot's former agent-support documentation now redirects to its generic Kimi API overview, which documents OpenAI compatibility instead. The Anthropic-shaped route still exists: a credential-free probe of `POST https://api.moonshot.ai/anthropic/v1/messages` returned 401 with an `incorrect_api_key_error` on 2026-07-26. That proves the route answers; it does not restore the missing field-by-field vendor contract.

Compatible is a range, not a boolean. DeepSeek publishes a field-by-field support matrix, and reading it is the fastest way to see which parts of the format are load-bearing:

| Field | DeepSeek status |
| --- | --- |
| `anthropic-version`, `anthropic-beta` headers | Ignored |
| `max_tokens`, `stop_sequences`, `stream`, `system`, `temperature`, `top_p`, `x-api-key` | Fully Supported |
| `top_k`, `container`, `mcp_servers`, `service_tier` | Ignored |
| `metadata` | `user_id` supported, others ignored |
| `cache_control` on tools, text, `tool_use`, `tool_result` | Ignored |
| Image, document and `search_result` blocks | Not Supported |

Two rows carry the whole caching story from the other side. `anthropic-version` ignored means the version header buys nothing. `cache_control` ignored means every breakpoint is discarded — silently, exactly as the gateway bug above did by accident — and `cache_read_input_tokens` in the response is the only way to tell. DeepSeek also maps ids by prefix: `claude-opus*` becomes `deepseek-v4-pro`, `claude-haiku*` and `claude-sonnet*` become `deepseek-v4-flash`, and an unrecognised name falls through to `deepseek-v4-flash` rather than erroring.

A working configuration that routes an Anthropic-format client to a non-Anthropic model, with a test suite over these behaviours, is in [Claude Code on Kimi, GLM or Grok through your own Cloudflare account](/a/claude-code-on-cloudflare-ai-gateway). The account setup behind it is in [the AI Gateway setup page](/a/cloudflare-ai-gateway-setup) and the credit accounting in [Cloudflare Unified Billing](/a/cloudflare-unified-billing).

## Fresh wire checks reproduce the contract

The complete harness is the same sequence printed below, run at 05:15–05:16 UTC on 2026-07-26. It reads credentials from local settings and writes only redacted results.

| Check | Fresh result |
| --- | --- |
| Non-streaming `POST /v1/messages`, `max_tokens: 64` | HTTP 200; 507 response bytes in 1.649598 s; `stop_reason: "max_tokens"`; 19 input and 64 output tokens |
| `POST /v1/messages/count_tokens` on the same prompt | HTTP 200; 19 response bytes in 0.041256 s; 18 input tokens — one below message usage |
| Forced streamed `get_weather` call | HTTP 200; 1,491 response bytes in 1.201516 s; 11 SSE frames; compacting six `input_json_delta` fragments produced `{"city":"Dallas"}` |
| No key and no version | HTTP 401; `x-api-key header is required`; `request-id` present; `x-should-retry: false` |
| Invalid key with no version | HTTP 401; `invalid x-api-key` |
| Invalid key with an invalid version | HTTP 401; `invalid x-api-key` — authentication still won the check order |

The exact local commands were `node /private/tmp/codex-messages-api/measure.mjs` and `node /private/tmp/codex-messages-api/measure-headers.mjs`. Neither prints a credential.

## Method for the six measurements above

All six were captured on 2026-07-26. The header-order table and the `request-id` / `x-should-retry` observation came from `api.anthropic.com/v1/messages` with no valid key — all three calls fail, and which way they fail is the finding, so no credential is needed to reproduce them. The truncated response, the SSE sequence, the tool round trip and the `count_tokens` comparison came from a Messages-API server implemented as a Cloudflare Pages function: `functions/api/aig/[[path]].js`, 531 lines, translating the Anthropic body to and from a chat-completions upstream, cited above by line number.

To rerun any of them, point `BASE` at `https://api.anthropic.com` or at any gateway answering this format, put a key in `TOKEN`, save the body from the relevant section to `body.json`, and send `curl -s -N -X POST "$BASE/v1/messages" -H "x-api-key: $TOKEN" -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' -d @body.json`.

## Sources

1. Messages API reference — https://platform.claude.com/docs/en/api/messages
2. API versioning — https://platform.claude.com/docs/en/api/versioning
3. Streaming Messages — https://platform.claude.com/docs/en/build-with-claude/streaming
4. Prompt caching — https://platform.claude.com/docs/en/build-with-claude/prompt-caching
5. Tool use overview — https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
6. Stop reasons and fallback — https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons
7. Token counting — https://platform.claude.com/docs/en/build-with-claude/token-counting
8. Errors — https://platform.claude.com/docs/en/api/errors
9. DeepSeek Anthropic API compatibility — https://api-docs.deepseek.com/guides/anthropic_api/
10. Use Claude Code with Z.ai models — https://docs.z.ai/scenario-example/develop-tools/claude
11. Kimi API overview — https://platform.kimi.ai/docs/api/overview
12. Anthropic prompt-caching notebook — https://github.com/anthropics/claude-cookbooks/blob/main/misc/prompt_caching.ipynb
13. Compaction can emit an unpaired tool_use, bricking the session with a permanent Anthropic 400 — https://github.com/valkyriweb/pi-mono/issues/406
14. feature: cache_control stripped when proxying to AWS Bedrock (Anthropic prompt caching broken) — https://github.com/agentgateway/agentgateway/issues/2628
15. Prompt caching may be silently inert for OpenRouter-routed agents (cache_control only injected on native-Anthropic path) — https://github.com/bobmatnyc/trusty-tools/issues/3388
16. Anthropic: synthesized tool_use IDs collide on parallel calls to the same tool — https://github.com/go-steer/core-agent/issues/367
17. [BUG] ConnectionRefused on /v1/messages streaming while simple API requests succeed (Windows, Bun) — https://github.com/anthropics/claude-code/issues/76802
18. ClaudeClient: /v1/messages with tool_use — https://github.com/amadoug2g/Sweep/issues/4
19. Comment on "Pro Max 5x quota exhausted in 1.5 hours" — 1h cache TTL — https://news.ycombinator.com/item?id=47745409
20. Comment on "Zerostack – a coding agent in pure Rust" — missing cache_control — https://news.ycombinator.com/item?id=48167243
21. Show HN: Autocache – Cut Claude API costs 90% (for n8n, Flowise, etc.) — https://news.ycombinator.com/item?id=45518040
22. A Messages-to-Chat-Completions translation layer — https://github.com/redacted/claude-code-cloudflare-gateway
23. Independent A/B measurement: cache_control direct to Bedrock versus through a proxy — https://github.com/agentgateway/agentgateway/issues/2628
24. First-party wire receipt: message creation and count_tokens, 2026-07-26 — https://miscsubjects.com/api/articles/what-is-the-anthropic-messages-api
25. First-party wire receipt: streamed forced tool call, 2026-07-26 — https://miscsubjects.com/api/articles/what-is-the-anthropic-messages-api
26. First-party wire receipt: authentication precedes version validation, 2026-07-26 — https://miscsubjects.com/api/articles/what-is-the-anthropic-messages-api
27. First-party compatibility probes for DeepSeek, Z.ai and Moonshot, 2026-07-26 — https://miscsubjects.com/api/articles/what-is-the-anthropic-messages-api


---

# Protocol API Structure: REST Surface, Objects, and Phase Machine

slug: protocol-api-structure · https://miscsubjects.com/a/protocol-api-structure · tags: system, protocol, api · updated 2026-07-17T02:41:01.604Z

## What this article is

This document describes the live REST surface and phase machine for miscsubjects.com.  
It is system documentation, not a compound catalogue entry.

## Who claims what

The build itself defines the contract.  
GET /api/protocol returns the authoritative machine map.  
API_QUICKMAP.md and PROTOCOL_SPEC.md are static mirrors of that contract.

## What is known

GET /api/protocol exposes the endpoint list, body schemas, and phase definitions.  
Two planes exist: /api/articles for CRUD and sub-resources, and /api/protocol for phase operations.  

Claims live in article.meta.claims[]. Sources live in meta.sources[] with prev/hash chain. All writes are append-only.  

POST /api/protocol/draft validates tiers, optionally verifies source URLs, and hash-chains sources.  
POST /api/protocol/populate ingests sources into an article without rewriting the body.  
POST /api/protocol/collaborate lets Kimi or Gemini append 1–3 claims.  
POST /api/protocol/score recomputes claim.weight and applies status:cut when below threshold.  
GET /api/protocol/next?role=writer hands out one open task atomically.  
POST /api/protocol/run executes one scheduler tick inside the 100-second request limit.  
GET /api/matrix/gaps lists missing roots and cross cells. POST /api/matrix/seed fills from the canonical catalog.  
Webhook POST /api/articles/{slug}/webhook accepts atomic appends of kind:claim|source|widget.

## What we do not know

Not every phase listed in PROTOCOL_SPEC.md has a deployed cron worker.  
Some phases still require manual POST /api/protocol/grow or dispatch tools.  
Rate limits and per-tenant quotas on gateway model calls are environment-dependent and not shown in the public contract.

## Limitations

Protocol slugs are immutable. Hard DELETE is blocked. Corrections use PATCH status:retracted or new revisions.  
Every phase transition must finish inside Cloudflare’s ~100-second cap. Batch work uses grow batch or cron ticks.

## Disclaimer

This API map is a research-ledger transport layer only. It grants no medical authority.

