# CLAUDE.md — `web/src/pages/api/`

Four universal endpoints + a small set of justified extras. Receiver namespace IS the API — new product features add receivers, not routes.

## The four universal endpoints

| Method | Path | Purpose | Auth |
|---|---|---|---|
| `POST` | `/api/signal/[...receiver]` | Fire-and-forget — record event, trigger workflow. 5-mode receiver grammar (`alice`, `alice:review`, `world:review+P0`, `all:review`, `sub:news:crypto`). | Viewer (passkey/session) + role-gated via `gateSignalByRole` |
| `POST` | `/api/ask/[...receiver]` | Synchronous — `{ outcome: 'result'\|'timeout'\|'dissolved'\|'failure', signalId, payload? }`. 30s max. | Viewer + GATEWAY_API_KEY |
| `POST` | `/api/mark/[edge]` · `/api/warn/[edge]` | Pheromone strength/resistance on any path. `edge = source>target` (URL-encoded; `→` accepted). Body `{ strength }`. | Viewer |
| `GET`/`PUT` | `/api/settings?scope=<scope>&workspace=<id>` | Any new setting — add a scope, not a file. Current scopes: `status`, `privacy`, `packs`, `thresholds`, `channels`, `list-ctas`, `detail-tabs`, `profile-completion`, `integrations`, `company-context`. | Passkey token + workspace ownership |

## The pattern: receiver IS the API

Adding a feature = adding a receiver to `signal`/`ask`, not a route. The receiver namespace mirrors the SDK receiver namespace (see `sdk/CLAUDE.md`). One URL family, infinite receivers, zero route churn.

```ts
// Wrong: POST /api/orders/create
// Right: POST /api/signal/orders:create  (or ask, if you need the result)
```

State lives on highways, not localStorage — read via `/api/export/highways?from=…` and classify by strength/resistance. See `.claude/rules/api.md`.

## Edge validation + envelope (declared receivers)

Receivers declared in `@oneie/sdk/receivers` (`RECEIVERS`) are validated at the
route edge before dispatch, via `lib/receiver-envelope.ts`:

- **error-as-affordance** — a bad payload returns `400 { error:'validation', field, expected, got, hint, example }` (the `example` is seeded from `receiver.examples`) so an agent can self-correct next turn.
- **idempotencyKey** — a retried key replays the first result from KV; the handler runs once.
- **simulate** — `{ simulate:true }` returns a projection and commits nothing.

`world:*` validate in `ask/[...receiver].ts` before `dispatchWorldReceiver`;
`grant-capability` validates in `signal/[receiver].ts`. **Undeclared receivers pass
through unvalidated** — the raw-string escape hatch, so a pinned SDK still reaches
new receivers and unknown signals fall through to nanoclaw unchanged.

## Justified extras (NOT product features)

| Family | Why exempt |
|---|---|
| `auth/`, `provision/`, `identity/`, `visitor*` | WebAuthn ceremony — stateful challenge exchange, fixed redirect URIs |
| `webhook/`, `composio/`, `integrations/` | Inbound from external — cannot invert the call |
| `chat.ts`, `tts.ts` | SSE/binary streaming responses |
| `export/{actors,groups,skills,highways,conversations,…}.ts` | Bulk dimension reads shaped for inbox/CRM — extend the family for new dims |
| `pay/`, `payments/`, `billing.ts`, `x402.ts` | Resource-bound RESTful flows with CDN cache + per-resource ACL |
| `pages/` | Resource-bound CRUD on chat-authored landing pages (`pages` table) — stable slug handle, per-resource tenant ACL via `authorizeWorkspace`; chat writes the same table via the channels `create_page` tool |
| `og/`, `health.ts`, `report.ts` | Fixed-URL contract (browser, monitors) |
| `avatar.ts` (POST) · `avatar/[slug].ts` (GET) | Binary upload + serve — multipart avatar → R2 `CONTENT` (`${slug}/avatar`), GET streams bytes with SVG-initials fallback. POST keys on `locals.slug` (self, no IDOR) then mirrors the URL onto the TypeDB actor (`world:update-contact`) + better-auth `user.image`. Mirrors `branding.ts`/`logo/[slug].ts`. |
| `mark/`, `warn/`, `fade.ts`, `follow.ts`, `forget.ts`, `select.ts`, `sub.ts` | Locked verb surface — one file per verb, never multiplied |

`fade`/`follow`/`forget` are locked-verb endpoints. They are not extensible — the 6 verbs (`signal · mark · warn · fade · follow · harden`) are LOCKED at root.

## Adding a new endpoint — checklist

Before creating a file in this folder, name which of the four universal endpoints cannot handle it and which justified-extra row it matches. If neither, route through `signal`, `ask`, `mark/warn`, or `settings?scope=`. No stubs — never ship a route that returns `[]` or `{ok:true}` as a placeholder.

## Response shape conventions

- **Success:** `Response.json(payload)` — payload is endpoint-specific; `ask` returns the 4-outcome envelope.
- **Client error:** `Response.json({ error: '<reason>' }, { status: 400|401|403 })`.
- **Domain error (problem+json):** `{ type, title, status }` with `Content-Type: application/problem+json` — used by `mark`/`warn` for malformed edges.
- **Service unavailable:** `{ error: 'no_db' }` `503` when a binding is missing.
- **Every handler:** `export const prerender = false` (these are Workers routes, never prerendered).
- **Tracing:** `generateTraceId()` from `lib/trace` → returned as `signalId` on `signal`/`ask`.

## See also

- `.claude/rules/api.md` — the contract (auto-loads here)
- `lib/substrate.ts` — `writeSignal`, `mark`, `pollOutcome`, `isToxic`
- `lib/viewer.ts` — viewer resolution + role gating
- Root `CLAUDE.md` — 6 dimensions, 6 verbs, 3 locked rules
