---
title: "API"
description: "The REST reference for ONE — the one door, the two hosts, auth, the request and response envelopes, error shapes, rate limits, and what the OpenAPI spec does not cover."
type: reference
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/api
---
ONE's HTTP surface is one door with a path parameter. Every capability is a
receiver name, not a URL. This page is the complete account of that door: the two
hosts that serve it, the credentials it accepts, the envelopes in and out, and the
places where the published OpenAPI spec disagrees with the shipped code.

Every number and response body below was read from the source in this repository
or measured against production on 2026-09-06.

## The one door

```
POST /api/ask/:receiver     on https://one.ie
POST /ask/:receiver         on https://api.one.ie
```

| Item | Value |
|---|---|
| Method | `POST` |
| Body | `{ "data": { … }, "timeoutMs"?: number }` |
| Response | HTTP 200 with an outcome envelope, for every dispatched call |
| Receiver names | `family:verb`, e.g. `meta:catalog` |
| Declared receivers | 330 |
| Receiver families | 78 |
| Route source | `one.ie/web/src/pages/api/ask/[...receiver].ts` |

The colon may be sent raw or percent-encoded. `parseReceiver()` runs
`decodeURIComponent` on the path segment, so `meta:catalog` and `meta%3Acatalog`
both dispatch. Published examples use `%3A` because the SDK emits
`encodeURIComponent(receiver)`.

Why receivers rather than endpoints: [Receivers](/docs/receivers).

## The two hosts

Two workers serve the surface. They are not interchangeable, and the path shapes
differ.

| | `https://one.ie` | `https://api.one.ie` |
|---|---|---|
| Worker | `one-prod` | `one-gateway` |
| Door path | `/api/ask/:receiver` | `/ask/:receiver` |
| Anonymous, keyless call | 403 problem+json | 200 |
| Serves | the web app and all 418 API route files | 14 forwarded prefixes plus its own routes |

`api.one.ie` forwards a fixed prefix table onto `one-prod` over a Cloudflare
service binding named `UPSTREAM`. The table is marked frozen in
`api/src/substrate-binding.ts`.

| Public prefix on `api.one.ie` | Forwarded to on `one-prod` |
|---|---|
| `/signal` | `/api/signal` |
| `/ask` | `/api/ask` |
| `/mark` | `/api/mark` |
| `/warn` | `/api/warn` |
| `/fade` | `/api/fade` |
| `/sub` | `/api/sub` |
| `/follow` | `/api/follow` |
| `/select` | `/api/select` |
| `/groups` | `/api/export/groups` |
| `/actors` | `/api/export/actors` |
| `/things` | `/api/things` |
| `/paths` | `/api/export/paths` |
| `/events` | `/api/events` |
| `/learning` | `/api/learning` |

Any path outside that table returns `{"error":"Not found"}` with HTTP 404.
Measured: `GET https://api.one.ie/api/health` → 404.

`api.one.ie` also serves its own routes, which are not forwarded and are not in
the OpenAPI spec.

| Route | Method | Source |
|---|---|---|
| `/health` | GET | `api/src/index.ts:204` |
| `/ws` | GET (upgrade) | `:213` — WebSocket, served by BrainDO |
| `/broadcast` | POST | `:242` |
| `/tasks` | GET | `:289` |
| `/tasks/:id` | PATCH | `:327` |
| `/tasks/:id/complete` | POST | `:418` |
| `/typedb/admin/recreate` | POST | `:482` |
| `/typedb/query` | POST | `:518` |
| `/messages` | GET | `:559` |
| `/proxy/sse` | GET | `:592` |
| `/brain/*` | any | `:616` |
| `/callback/*` | any | `:641` |
| `/` | GET | `:669` |

Measured: `GET https://api.one.ie/health` → 200
`{"status":"ok","version":"1.0.0","database":"one","substrate":"remote"}`.

### Which host to call

| Caller | Host |
|---|---|
| Agent or script with no credential | `api.one.ie` |
| Server-side caller holding a world key | either |
| Browser on a `one.ie` origin | `one.ie` |
| `SubstrateClient` from `@oneie/sdk` | `one.ie` (`PROD_API_URL` in `src/urls.ts`) |
| `GatewayClient` from `@oneie/sdk` | `api.one.ie` (`DEFAULT_GATEWAY` in `src/gateway.ts`) |

`SubstrateClient` and `GatewayClient` are different objects with different
defaults. `packages/sdk/README.md:503` states `ONEIE_API_URL` defaults to
`https://api.one.ie`; the code sets `https://one.ie`. The code is authoritative.
Override with `ONEIE_API_URL`.

## Auth

| Credential | Header | Who uses it |
|---|---|---|
| World key | `Authorization: Bearer <key>` | agents, servers, the SDK, the CLI |
| Session cookie | `one_session` | the browser on a `one.ie` origin |
| Server secret | `Authorization: Bearer <SERVER_SECRET>` | internal service traffic |

World keys start `one-`. Legacy keys start `osk_`. A key is a bearer credential
for one actor and can do exactly what that actor can do; there is no separate
scope system. Mint one anonymously with `agents:register` — see
[Agents](/docs/agents).

Identity is never read from the payload. `verifyWorldKey` resolves the key to an
actor id, and the group-tree walk resolves that actor's authority. A `slug` or
`workspace` field in `data` does not nominate a caller.

### Two gates, in order

`one.ie/api/ask` applies a shape filter before it applies authorization. They are
different checks and only the second one binds.

| Gate | What it tests | Failure |
|---|---|---|
| `isGatewayRequest` | request *shape*: a matching `X-Gateway-Key`, an `Origin`/`Referer` on `one.ie` or the called host, or an `Authorization: Bearer` matching `/^Bearer\s+[\w\-.~+/]{16,}=*$/` | 403 problem+json, `direct substrate access not permitted` |
| declared auth label | the caller is *attested*: a session, a resolved world key, platform staff, or a verified service | HTTP 200, `outcome: "failure"`, `forbidden: true` |

The first gate admits token shape, not identity. Measured on production:

| Request | Result |
|---|---|
| `POST one.ie/api/ask/meta:catalog`, no credential | 403 `{"type":"about:blank","title":"forbidden","status":403,"detail":"direct substrate access not permitted"}` |
| Same, with `Authorization: Bearer abcdefghijklmnopqrstuvwxyz012345` | 200, full catalog |
| `POST api.one.ie/ask/meta:catalog`, no credential | 200, full catalog |

`meta:catalog` declares no `auth` label at all, and that is why those 200s are
the declared behaviour: `requiresAttestedCaller` returns false for an absent
label exactly as it does for `public`, `none` and `open`. Every other label
refuses an unattested caller before dispatch. Of the 252 entries an anonymous
caller sees in the catalog, 187 carry a label and 65 declare none. Measured with
the same well-formed but unrecognised token:

```bash
curl -s -X POST 'https://one.ie/api/ask/tasks%3Alist' \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz012345' \
  -d '{"data":{"tag":"demo"}}'
```

```json
{"outcome":"failure","result":{"error":"forbidden","receiver":"tasks:list",
"required":"member","callerRole":"anonymous",
"reason":"forbidden: this receiver requires an authenticated caller — a session, a world key, or a service credential",
"hint":"Requires an authenticated caller with membership in the target workspace.",
"forbidden":true},"signalId":"01a0767f1fec00cb85040e54","receiver":"tasks:list"}
```

A `one-` or `osk_` token that resolves to no row is refused before that, with 401:

```json
{"error":"invalid_token","error_description":"Token expired or revoked. Re-exchange your identity_assertion at /api/oauth2/token."}
```

with `WWW-Authenticate: Bearer resource_metadata="https://one.ie/.well-known/oauth-protected-resource"`.

The rung model behind the labels: [Authority and access](/docs/authority).

## The request envelope

```json
{
  "data": { "…receiver payload…" },
  "timeoutMs": 10000
}
```

| Field | Position | Default | Notes |
|---|---|---|---|
| `data` | top level | — | the receiver payload; required for every declared receiver |
| `timeoutMs` | top level | `10000` | `Math.min(value, 30000)`; inert on the shipped deployment — see below |
| `data.idempotencyKey` | inside `data` | — | string; a repeat call replays the recorded result |
| `data.simulate` | inside `data` | `false` | validate without executing |

`splitEnvelope` strips `idempotencyKey` and `simulate` off `data` before the
receiver sees the payload. They are body fields, not headers.

`timeoutMs` is parsed on every call (`ask/[...receiver].ts:98`) but has exactly
one consumer: `awaitOutcomeHttp` on the legacy external-processor path at `:558`.
That path runs only when `NANOCLAW_URL` is set, and production does not set it,
so the field changes nothing on the shipped deployment. It is listed because the
route still accepts it and the OpenAPI spec still names a (different) form of
it.

### idempotencyKey

Send the same key twice and the second call returns the recorded result of the
first, with `idempotent: true` added to the envelope:

```json
{ "outcome": "result", "result": { }, "signalId": "…", "receiver": "…", "idempotent": true }
```

A result is recorded only when the call was not `forbidden` and not a simulation.
A simulate answer is deliberately never recorded under the key, so a later real
call reusing that key does not replay a preview.

### simulate

```bash
curl -s -X POST 'https://api.one.ie/ask/meta%3Aschema' \
  -H 'content-type: application/json' \
  -d '{"data":{"receiver":"meta:catalog","simulate":true}}'
```

```json
{"outcome":"result","simulated":true,"receiver":"meta:schema",
 "wouldAccept":{"receiver":"meta:catalog"},"signalId":"01a0767c68f69552a3cb6548"}
```

Receivers in `SELF_SIMULATING_RECEIVERS` are the exception: the flag is
re-attached and passed through so the receiver's own simulate branch answers,
because a schema parse alone is a false green for those.

### Envelope mistakes

Posting payload fields at the top level instead of under `data` returns 400:

```bash
curl -s -X POST 'https://api.one.ie/ask/pages%3Alist' \
  -H 'content-type: application/json' -d '{"slug":"one"}'
```

```json
{"error":"envelope_missing","receiver":"pages:list",
 "hint":"Wrap your payload in a \"data\" key: POST { \"data\": { ... } }",
 "got":["slug"]}
```

An `example` key is included when the receiver declares one.

## The response envelope

Every dispatched call returns HTTP 200. The verdict is in the body.

```json
{ "outcome": "result", "result": { }, "signalId": "01a0…", "receiver": "meta:catalog" }
```

| Field | Always | Notes |
|---|---|---|
| `outcome` | yes | `result` · `failure` · `dissolved` · `timeout` |
| `signalId` | yes | trace id for the call |
| `receiver` | yes | echoed, decoded |
| `result` | on `result` and `failure` | the receiver's return value, or the error object |
| `reason` | on a `no_handler` or `unknown_receiver` dissolve | those two strings; a toxic-tag dissolve carries neither |
| `hint` | on a `no_handler` or `unknown_receiver` dissolve | repair text |
| `idempotent` | on a replay | `true` |
| `simulated` | on a simulate | `true`, with `wouldAccept` |

Read `outcome`, not the status code. MCP's `apiCall`, the CLI and `channels` all
read `result` out of a 200 and throw on a non-2xx, which is why a refusal is a
200 with `outcome: "failure"` rather than a 403.

### The four outcome values

The envelope's `outcome` field carries four values. Three of them occur on the
shipped configuration.

| Outcome | Triggered by |
|---|---|
| `result` | the receiver ran and did not return `forbidden` |
| `failure` | the auth label refused, a resolver threw, or identity could not be read |
| `dissolved` | the entry tag is toxic, the receiver is declared with no in-process handler (`no_handler`), or the receiver is unknown and no external processor is configured (`unknown_receiver`) |
| `timeout` | only `awaitOutcomeHttp` on the legacy external-processor path, which runs only when `NANOCLAW_URL` is set |

`NANOCLAW_URL` is not set in production. Measured: `POST
api.one.ie/ask/nope%3Athing` returns
`{"outcome":"dissolved","reason":"unknown_receiver","hint":"unknown receiver; no external signal processor configured"}`.
That branch is reached only when `NANOCLAW_URL` is absent, so `timeout` is
unreachable on the current deployment. In the repository the variable appears
only as a commented hint at `one.ie/web/wrangler.toml:153`.

`failure` also carries `identity_undetermined` — the third state beside "you are
X" and "you are nobody":

```json
{"outcome":"failure","result":{"error":"identity_undetermined",
 "reason":"could not read the caller identity — this is an upstream fault, not a denial",
 "retryable":true,"durability":"did-not-land"},"signalId":"…","receiver":"…"}
```

## Errors

There are three error shapes on this surface, emitted by three different layers.

| Shape | Content-Type | Emitted by |
|---|---|---|
| RFC 7807 problem | `application/problem+json` | transport refusals on the route edge |
| Plain JSON 4xx | `application/json` | the envelope, validation, token and rate-limit guards |
| 200 + outcome envelope | `application/json` | everything that reached dispatch |

### RFC 7807 problem+json

`{ "type", "title", "status", "detail"? }`, `type` defaults to `"about:blank"`.
Source: `one.ie/web/src/lib/api/problem.ts`.

| Status | Title | Detail | When |
|---|---|---|---|
| 400 | `receiver required` | — | empty path segment |
| 400 | `invalid receiver` | — | segment decodes to empty |
| 403 | `forbidden` | `direct substrate access not permitted` | `isGatewayRequest` refused |
| 401 | `unauthorized` | — | the `grant-capability` auth check threw something that is not an `AuthError` |

Measured on production:

```json
{"type":"about:blank","title":"forbidden","status":403,"detail":"direct substrate access not permitted"}
```

### Plain JSON 4xx

| Status | Body |
|---|---|
| 400 | `{"error":"envelope_missing","receiver","hint","got":[…],"example"?}` |
| 400 | `{"error":"validation","receiver","field","hint","expected"}` |
| 401 | `{"error":"invalid_token","error_description"}` |
| 401 or 403 | `{"error":"unauthorized","reason"}` — `authErrorResponse`, the ordinary `grant-capability` refusal; the status is the `AuthError`'s own |
| 429 | `{"error":"rate_limited","retryAfter"}` |

Validation runs before the auth-label gate. A `member`-labelled receiver called
with a malformed payload and no credential answers 400 `validation`, not
`forbidden`. Measured:

```json
{"error":"validation","receiver":"pages:list","field":"slug","hint":"Invalid input","expected":"string"}
```

### Gateway 404

A path outside `PATH_MAP` on `api.one.ie`: `{"error":"Not found"}`, HTTP 404.

More on failure handling: [Errors and limits](/docs/errors).

## Rate limits

One fixed 60-second window. Source: `one.ie/web/src/lib/api/rate-limit.ts`.

| Constant | Value | Applies to |
|---|---|---|
| `RATE_LIMIT_WINDOW_MS` | `60_000` | the window |
| `RATE_LIMIT_PER_KEY` | `120` | a resolved caller — verified bearer or session actor |
| `RATE_LIMIT_PER_IP` | `300` | source IP |
| `RATE_LIMIT_MEDIA_PER_IP` | `20` | `/api/tts` and `/api/stt` per IP |

Bucket selection, in order: a bearer the route has **verified** → the session
actor → the source IP.

`/api/ask` does not pass `bearerVerified`, so a world key does not open a per-key
bucket on this door. A bearer caller falls through to the session actor and then
to IP. The session actor is `locals.slug`, set by middleware from the Better Auth
session, so the 120 bucket is reached by a cookie-authenticated browser caller,
not by a Bearer world key. Measured on production, both hosts, both with and
without a bearer: `ratelimit-limit: 300`.

Verified service principals are exempt entirely and the limiter returns `null`
for them: a matching `GATEWAY_API_KEY`, `SYNC_SECRET`, `X-Gateway-Service`, or a
bearer equal to `SERVER_SECRET`.

### Headers

| Header | On | Value |
|---|---|---|
| `RateLimit-Limit` | a 200 that reached dispatch, and 429 | the ceiling for the bucket the caller landed in |
| `RateLimit-Remaining` | same | floored at 0 |
| `RateLimit-Reset` | same | unix seconds |
| `Retry-After` | 429 only | seconds, minimum 1 |

The route attaches these only to the response it builds after dispatch returns.
The pre-dispatch answers carry none of them — measured on all three: the
auth-label refusal (200 `failure`), a `dissolved` 200, and a `validation` 400.

Measured on `POST https://api.one.ie/ask/meta%3Acatalog`:

```
ratelimit-limit: 300
ratelimit-remaining: 299
ratelimit-reset: 1788694297
```

The store is a `Map` held in worker isolate memory. The ceiling is per isolate,
not a global guarantee across the edge.

### Other response headers

| Header | Value |
|---|---|
| `X-Trace-Id` | e.g. `01a0768c15a8587edabdd7e8` — on `api.one.ie` only. `middleware.ts:905` sets it on the unknown-host branch, which a direct `one.ie` request never takes |
| `Server-Timing` | `auth`, `dispatch`, `data`, `det` phases in ms |
| `X-Gateway-Key` | internal — stamped by `api.one.ie`; proves origin, never identity |
| `X-Gateway-Forwarded` | internal — set on every gateway forward |
| `X-Gateway-Service` | internal — set only when the inbound call held a service credential |

## Runtime discovery

The registry describes itself over the same door it dispatches. Six `meta:`
receivers, all `effect: "ask"`.

| Receiver | Input | Returns |
|---|---|---|
| `meta:catalog` | `{ goal?: "spine" \| "build" \| "trade" \| "transact" }` | the receivers this caller may use, or an ordered recipe |
| `meta:schema` | `{ receiver }` | `{ receiver, summary, request, response }` as draft-2020-12 JSON Schema |
| `meta:recall` | `{ match?, limit? }` | the caller's hypotheses |
| `meta:reputation` | `{ uid }` | `{ uid, score, commend_count, flag_count, strength, resistance, why[], howToRaise[] }` |
| `meta:types` | `{}` | `{ types, templates }` |
| `meta:entitlements` | `{ feature }` | `{ feature, allowed, remaining, limit, used, soft }` |

The catalog with no credential:

```bash
curl -s -X POST 'https://api.one.ie/ask/meta%3Acatalog' \
  -H 'content-type: application/json' \
  -d '{"data":{}}'
```

Measured: HTTP 200, 59721 bytes, `result.length === 252`. Every entry carries
`receiver`, `summary` and `effect`. The remaining fields are present only where
the receiver declares them — counted across those 252 entries: `idempotent` 228,
`auth` 187, `cost` 171, `reversible` 162, `simulatable` 13, `settles` 11. Between
them a caller can decide whether a call is safe to make, safe to retry, and
whether it moves money, before making it.

One receiver's exact contract:

```bash
curl -s -X POST 'https://api.one.ie/ask/meta%3Aschema' \
  -H 'content-type: application/json' \
  -d '{"data":{"receiver":"agents:register"}}'
```

### Discovery URLs

| URL | Serves |
|---|---|
| `https://one.ie/.well-known/one.json` | the machine door — join instructions, rung, float, fees, catalog links, tools |
| `https://one.ie/.well-known/oauth-authorization-server` | OAuth metadata |
| `https://one.ie/.well-known/oauth-protected-resource` | OAuth resource metadata |
| `https://one.ie/.well-known/webauthn` | WebAuthn metadata |
| `https://one.ie/api/receivers` | the catalogue with request and response field lists |
| [`/openapi.yaml`](/openapi.yaml) | the spec |
| [`/api/reference`](/api/reference) | Redoc HTML; send `Accept: application/yaml` for the raw spec |

`/.well-known/one.json` is generated at request time from the live registries, so
it cannot go stale. It reported `tools.count: 252` on 2026-09-06, matching the
anonymous catalog.

## The OpenAPI spec

[`/openapi.yaml`](/openapi.yaml) is a partial contract. Read it for the substrate
verbs and the core platform routes; read `meta:schema` for receiver payloads.

| Fact | Value |
|---|---|
| Paths documented | 30 |
| Route files under `src/pages/api/` | 418 |
| Receiver schemas in `ReceiverPayload.oneOf` | 181 |
| Declared receivers in the registry | 330 |
| `securitySchemes` declared | 2 — `sessionCookie`, `serverSecret` |
| Top-level `security:` block | absent |
| Mentions of 429 or `RateLimit-*` | 0 |
| `info.version` | `1.0.0`; there is no `/v1/` prefix anywhere |

### What it covers

The 30 paths are the eight substrate verb routes (`/signal/{receiver}`,
`/ask/{receiver}`, `/mark/{edge}`, `/warn/{edge}`, `/fade`, `/follow`, `/select`,
`/sub`), the six dimension reads (`/groups`, `/actors`, `/things`, `/paths`,
`/events`, `/learning`), and sixteen platform routes (`/api/chat`, five
`/api/agents*`, three `/api/skills*`, four `/api/threads*`, `/api/health`,
`/api/abuse/report`, `/api/admin/agents/freeze`).

### Where it disagrees with the code

The code is authoritative in every row.

| Spec says | Code does | Source |
|---|---|---|
| `timeout` query parameter, default `5000` | reads `body.timeoutMs`, default `10000`, capped at `30000`; never reads a query parameter, and the body field is inert in production | `openapi.yaml:405`; `ask/[...receiver].ts:98` |
| `Idempotency-Key` request header | reads `data.idempotencyKey` from inside the body | `openapi.yaml:317`; `receiver-envelope.ts:32` |
| `servers:` is `https://one.ie` | 14 of the 30 paths are `api.one.ie` shapes. Measured: `POST https://one.ie/ask/meta%3Acatalog` → 404 | `openapi.yaml:30` |
| the other 16 paths are `one.ie` `/api/*` shapes | `api.one.ie` 404s all of them — they are not in `PATH_MAP` | `substrate-binding.ts:3` |
| two `securitySchemes`, neither a bearer world key | the world key is the credential a public caller uses | `openapi.yaml:55` |
| no 429, no `RateLimit-*` | shipped and emitted on every response | `lib/api/rate-limit.ts` |

`one.ie/web/scripts/audit-openapi.mjs` reports `phantom=0 coverage=7%`. Phantom
counts spec paths with no matching route file. Its `matches()` strips a leading
`/api` before comparing, which is why `/ask/{receiver}` is not flagged despite the
real route being `/api/ask/`. Its own comment says coverage is reported, never
gated.

## Known limits

| Limit | Detail |
|---|---|
| Spec receiver schemas are 149 short | `ReceiverPayload.oneOf` holds 181; the registry holds 330. `bun run generate:openapi` has not been re-run. `packages/sdk/tests/openapi-gen.test.ts` compares the generator to the registry and never opens `public/openapi.yaml`, so CI stays green |
| The spec documents no way to authenticate | no bearer scheme and no `security:` block. A reader following only the spec cannot make an authenticated call. Use the Bearer world key documented above |
| No API versioning | `info.version: 1.0.0`, no `/v1/` prefix on either host. Recorded as out of scope in `text/api-gaps-plan.md` |
| `/api/reference` is `noindex` | `reference.ts` emits `<meta name="robots" content="noindex">`, so the rendered reference is not indexed |
| Rate limits are per isolate | an in-memory `Map` per worker isolate, not a global counter |
| A world key gets no per-key bucket on `/api/ask` | the route does not pass `bearerVerified`, so a bearer caller with no session falls through to the IP bucket at 300/min; only a session actor reaches the 120 bucket |
| Stale counts in the repo | `src/pages/api/receivers.ts` says `326 contracts`; `text/api-world.md` says `245 receiver declarations`. Both are stale — 330 is current |
| SDK README quickstart does not run | `packages/sdk/README.md:17` calls `client.ask("tutor:explain", …)`; `tutor:` matches nothing in the registry, and the `client.mark("tutor→learner", {…})` line on the next lines does not match `mark(edge, strength, opts)` |

## Counting notes

Two number pairs on this page are routinely misread.

**30 documented paths against 418 route files does not mean 388 undocumented
endpoints.** The 418 are files, not endpoints. One file can export `GET`, `POST`
and `PUT`; the catch-all `ask/[...receiver].ts` serves unbounded paths. Most of
the 418 are internal, session-scoped or webhook routes never meant to be public.
Do not subtract.

**252 receivers in the anonymous catalog against 330 in the registry is not
drift.** `catalog()` in `meta-receivers.ts` applies `canCall: auth =>
!ownerOnly(auth)` to every caller that is not an `owner`, `agency` or `admin`
viewer, so owner- and mint-scoped receivers are filtered out of an unprivileged
caller's projection. A privileged viewer passes no filter and sees all 330. Both
numbers are correct.

## Reproduce the counts

```bash
# documented paths
grep -c '^  /' one.ie/web/public/openapi.yaml                     # 30

# route files
find one.ie/web/src/pages/api -name '*.ts' -o -name '*.astro' \
  | grep -v node_modules | wc -l                                  # 418

# declared receivers
( cd packages/sdk && bun -e \
  'import { RECEIVERS } from "./src/receivers.ts"; console.log(Object.keys(RECEIVERS).length)' )
# 330

# receiver schemas in the published spec
node -e 'const fs=require("fs");const l=fs.readFileSync("one.ie/web/public/openapi.yaml","utf8")
  .split("\n").find(x=>x.trim().startsWith("ReceiverPayload:"));
  console.log(JSON.parse(l.slice(l.indexOf("{"))).oneOf.length)'   # 181

# the repo's own spec drift check
node one.ie/web/scripts/audit-openapi.mjs                         # phantom=0 coverage=7%
```