# CoRelayer API — plain-Markdown reference

Version `1.0.0-draft.1`. Generated from the OpenAPI 3.1 document at
https://docs.co-relayer.com/openapi.yaml — that document is authoritative; this file is a reading
aid for agents, with schemas flattened one level.

61 operations. The browsable reference starts at https://docs.co-relayer.com/api/reference/corelayer-api.

## Servers

- `https://api.co-relayer.com` — MultiversX mainnet (chainID "1")
- `https://devnet-api.co-relayer.com` — Devnet staging (chainID "D"). It runs the same service and wallets as mainnet and swaps through the

## Errors

Every error is an RFC 9457 problem document whose `type` is
`https://docs.co-relayer.com/errors/<kebab-code>`. The machine-readable catalogue is at
https://docs.co-relayer.com/errors.json.

## meta

Health, discovery documents, network parameters.

### GET /healthz

operationId: `getHealthz`

Liveness of the API process.

Auth: `public`

Responses:
- `200` Process is alive. → Health

### GET /readyz

operationId: `getReadyz`

Whether this API host is ready to take traffic. It reports not ready while the host restores its state after a restart.

Auth: `public`

Responses:
- `200` Ready to take traffic. → Health
- `503` 

### GET /openapi.json

operationId: `getOpenApi`

The API's OpenAPI description, as JSON.

Auth: `public`

Responses:
- `200` OpenAPI 3.1 document.

### GET /.well-known/x402

operationId: `getX402Descriptor`

x402 resource descriptor at the conventional well-known path.

Auth: `public`

Responses:
- `200` Descriptor. → X402Descriptor

### GET /v1/network

operationId: `getNetwork`

Chain parameters the client needs to build a transaction, swap-venue state, measured latency per shard.

Chain constants are re-read from the gateway's `/network/config` every epoch. Latency figures are
measured values only; nothing is estimated.
`nativeAuth` carries a recent **shard-1** block hash for headless agents. A native-auth token must
embed a shard-1 block hash, because the API checks it against its own record of recent shard-1 blocks
without calling a gateway.
`contractPause` reflects the contract's `getPauseState()` and `swapVenue` reflects the exchange venue.
They are separate conditions with separate error codes: `CONTRACT_PAUSED` and `SWAP_VENUE_PAUSED`.

Auth: `public`

Responses:
- `200` Network view. → Network
- `429` 

## relay

Native relay API (assign, relay, intent status, per-intent SSE).

### POST /v1/relay/assign

operationId: `assignRelayer`

Assign one relayer in the sender's shard and issue a lease. Nothing is reserved yet.

Call it just before the user signs: the relayer address goes into the transaction, so the user signs
once. `renewFor` renews the lease for a transaction that is already signed, so a slow signer does not
have to sign again. `cancel` returns a lease for a cancel transaction, pinned to that nonce.
Rate limits: 10/s per IP, and 2/s per sender with a burst of 10.

The caller must prove it controls the sender, in one of two ways:
- a presence proof: the sender's own key signs the message
  `corelayer/assign/v1|<chainId>|<sender>|<serverTimeMs>`, where `serverTimeMs` is within
  +/- 30 000 ms of the server clock. The signature is a raw Ed25519 signature over the UTF-8 bytes of the
  message, without the MultiversX signed-message prefix that a wallet's `signMessage` adds. Set
  `proof.kind` to `sponsor` when a sponsor API key will pay for the relay, and to `key` otherwise. The
  sender's key signs both kinds;
- a native-auth bearer token whose address equals `sender`.

This route does not read `X-Api-Key`, and a sponsor API key never proves presence: it goes on
`POST /v1/relay`, where it selects the account that pays.

A missing proof gives `401 ASSIGN_PROOF_REQUIRED`; a bad or stale one gives `401 ASSIGN_PROOF_INVALID`.
The prepare routes of the free purchase flow issue their own `FREE` lease and need no proof.

Auth: `presence-proof`, `native-auth`

Request body (required):
AssignRequest (see /openapi.yaml)

Responses:
- `200` Assignment. → Assignment
- `400` 
- `401` `ASSIGN_PROOF_REQUIRED` or `ASSIGN_PROOF_INVALID` (`details.serverTimeMs` = signer clock), or a token error of the `Unauthorized` class. → Problem
- `403` 
- `409` `RESIGN_REQUIRED` (renewFor relayer no longer renewable) or `NOTHING_TO_CANCEL`. → Problem
- `429` 
- `503` `NO_RELAYER_AVAILABLE`, `SIGNER_UNAVAILABLE` or `SIGNER_FENCED`; carries `Retry-After`. → Problem

### POST /v1/relay

operationId: `relayTransaction`

Submit one user-signed Relayed-V3 transaction for co-signing and broadcast.

The idempotency key is `(tx.sender, tx.nonce)` plus the SHA-256 of the canonical signing bytes.
Posting the same bytes again returns the stored response with `duplicate: true` and never creates a
second reservation or a second broadcast. Unknown body members are rejected. `variants`,
`transactions` and `alternates` give `VARIANTS_NOT_SUPPORTED`: one request carries one transaction,
signed once.
Once the transaction is co-signed (state `COSIGNED`), the answer is always 200 or 202.
Authentication: the signed transaction is the credential when the sender has an account or is an
authorised sender on chain. Relaying for arbitrary senders needs a sponsor API key.

Optional **logical idempotency key** (header `Idempotency-Key` or body member `intentKey`; if you send
both, the header is used). It is stored per `(sender, key)` for
86 400 000 ms. The same key with different bytes or a different nonce, while the first intent is not
`DEAD`, gives `409 INTENT_ALREADY_SUBMITTED` carrying the first intent. After `DEAD` the key is free
again. The same key with the same bytes is an ordinary replay. The key can only reject a request, so
it never contradicts the idempotency key above. Send one key per user action, so that a retry after a
timeout cannot relay the same action twice.

Free relays: a short list of calls to the CoRelayer contract is relayed free of charge. `GET /v1/pricing`
publishes it as `howToBuy.freeOperations`. A free call to the contract while it is paused (`paused` or
`deposits_paused`, sender is not the owner) gives `503 CONTRACT_PAUSED` before anything is co-signed.

Auth: `tx`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)

Request body (required):
RelayRequest (see /openapi.yaml)

Responses:
- `200` Co-signed and acknowledged by at least one gateway (state BROADCAST or later). → RelayResponse
- `202` Co-signed, no gateway acknowledgement within 800 ms (state COSIGNED). Re-broadcast continues server-side. → RelayResponse
- `400` 
- `401` 
- `402` `NO_ENTITLEMENT`. Carries a `PAYMENT-REQUIRED` header that points at `POST /v1/x402/topup`. → Problem
- `403` 
- `409` 
- `410` `RELAYER_RETIRED` (only when no lease flow exists; otherwise `RESIGN_REQUIRED`). → Problem
- `413` 
- `422` 
- `429` 
- `500` 
- `503` 

### GET /v1/relay/{id}

operationId: `getRelay`

Intent status by intent id (`<sender>:<nonce>`) or by transaction hash.

Resolver in front of `GET /v1/intents/{sender}/{nonce}` (authoritative) and `GET /v1/tx/{hash}`. Same response schema.

Auth: `public`

Parameters:
- `id` (path, IntentId | TxHash, required) — Intent id `erd1...:41` or a 64-hex transaction hash.

Responses:
- `200` Intent. → Intent
- `404` 
- `429` 

### GET /v1/relay/{id}/events

operationId: `streamRelay`

Server-sent events for one intent until it is terminal and final, or 120 000 ms elapsed.

Event name `intent`, data = `Intent`. `id:` is the per-intent sequence. The server closes the
stream after the first event with `final: true`, after a terminal `DEAD`/`REJECTED`, or after
120 000 ms (the client then falls back to polling `GET /v1/relay/{id}`).

Auth: `public`

Parameters:
- `id` (path, IntentId | TxHash, required)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Event stream.
- `404` 
- `429` 

### GET /v1/intents/{sender}/{nonce}

operationId: `getIntent`

Authoritative, cross-host intent status keyed by (sender, nonce).

Auth: `public`

Parameters:
- `sender` (path, Address, required)
- `nonce` (path, integer (int64), required)

Responses:
- `200` Intent. → Intent
- `404` 
- `429` 

### GET /v1/tx/{hash}

operationId: `getTx`

Convenience lookup by transaction hash. Terminal state is still decided by (sender, nonce) and chain facts.

Auth: `public`

Parameters:
- `hash` (path, TxHash, required)

Responses:
- `200` Intent. → Intent
- `404` 
- `429` 

### POST /v1/quote

operationId: `quoteRelay`

RU weight and price of a transaction (signed or unsigned). No reservation, no signer call.

Auth: `public`

Request body (required):
QuoteRelayRequest (see /openapi.yaml)

Responses:
- `200` Relay quote (pricing-family types, decimal strings). → RelayQuote
- `400` 
- `422` 
- `429` 

### POST /v1/validate

operationId: `validateTransaction`

Run the relay checks (static checks, signatures, account and nonce, simulation) without a lease, a reservation or a co-signature.

Backs the MCP tool `validate_transaction`. Rate limit 2/s per IP because it may trigger a gateway simulation.

Auth: `public`

Request body (required):
ValidateRequest (see /openapi.yaml)

Responses:
- `200` Validation report. `ok=false` lists the problems that `POST /v1/relay` would return. → ValidateResponse
- `400` 
- `429` 

## pricing

Machine-readable pricing and quotes.

### GET /v1/pricing

operationId: `getPricing`

Live machine-readable pricing document (contract views, cache <= 6 000 ms).

The same document as the static file `https://co-relayer.com/pricing.json` and the MCP resource
`corelayer://pricing`. Its complete JSON Schema is `https://co-relayer.com/schemas/pricing-v1.json`.
Money, RU, gas and atto values are decimal strings here.

Auth: `public`

Parameters:
- `audience` (query, "human" | "agent", optional) — Filter `tiers[]` by audience.

Responses:
- `200` Pricing document. → PricingDocument
- `429` 

### GET /v1/pricing/tariff-history

operationId: `getTariffHistory`

Append-only tariff history mirrored from the contract view `getTariffHistory`.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of tariff entries, newest first.
- `400` 

## purchase

Builders of unsigned contract calls (deposit, subscribe, flags, senders).

### POST /v1/subscribe/prepare

operationId: `prepareSubscribe`

Quote a plan purchase and build the unsigned contract call with a relayer and lease pre-filled.

Returns a price quote (valid for 120 000 ms, `maxPrice == priceMicroUsdc`), one unsigned transaction
(`depositAndSubscribe` when USDC must be paid, `subscribe` when credits cover the price) and the
assignment to submit it with. The purchase transaction is relayed free of charge.
The quote is **stateless**: `quoteMac` is an HMAC over its canonical JSON, and `quoteId` is derived
from the MAC. The same inputs inside one TTL bucket give the same id on either API host, and nothing
is stored. `503 CONTRACT_PAUSED` while the CoRelayer contract is paused (it is deployed paused), and
`503 SWAP_VENUE_PAUSED` while the exchange venue is paused. Nothing is built in either case.

Auth: `public`

Request body (required):
PrepareSubscribeRequest (see /openapi.yaml)

Responses:
- `200` Quote + unsigned transaction + assignment. → PreparedPurchase
- `400` 
- `409` 
- `422` 
- `429` 
- `503` 

### POST /v1/deposit/prepare

operationId: `prepareDeposit`

Build the unsigned `deposit` / `depositFor` call (USDC -> credits).

Auth: `public`

Request body (required):
PrepareDepositRequest (see /openapi.yaml)

Responses:
- `200` Unsigned transaction + assignment. → PreparedTransaction
- `400` 
- `422` 
- `429` 
- `503` 

### POST /v1/flags/prepare

operationId: `prepareSetFlags`

Build unsigned `setAutoRenew`, `setPayg` or `releaseEscrow` calls for an existing account (relayed free within the flag-call limits).

Flag calls are relayed free of charge, up to 10 per 24 h and 30 per 30 days per account.
`setPayg` is encoded with four arguments: `enabled, budget, auto_topup, max_payg_price`.
`renew: true` builds `renew(address)`, the dashboard's "Renew now". It is free only when the argument
is the sender's own account, so `address` is both the sender and the argument.
When the free allowance is used up, the response carries `free: false` and `ru`, and the call is
billed as a normal relay. An address that cannot be billed gets `429 RATE_LIMITED` with
`details.scope = free_flow`.
`503 CONTRACT_PAUSED` while the contract's global pause is set.

Auth: `public`

Request body (required):
PrepareFlagsRequest (see /openapi.yaml)

Responses:
- `200` One unsigned transaction per requested change, consecutive nonces, one assignment. → PreparedTransactions
- `400` 
- `402` `NO_ENTITLEMENT` (details.reason NO_ACCOUNT) - flag calls are co-signed only for addresses that already have an on-chain account. → Problem
- `422` 
- `429` 
- `503` 

### POST /v1/senders/prepare

operationId: `prepareSenders`

Build unsigned `addSenders` / `removeSenders` calls.

`addSenders` is **never** relayed free. It is billed to the account as a normal relay and carries the
on-chain per-sender fee, which makes mass-creating senders expensive. `removeSenders` has no fee and
is relayed free: it is how a payer cuts off a compromised sender key. The response members `free`,
`ru` and `feeMicroUsdc` say which case applies; show them instead of working the rule out yourself.

`addSenders(max_fee, senders...)`: the route fills the first argument `max_fee` with the quoted fee,
so `PreparedTransaction.feeMicroUsdc` **equals** `contractCall.args[0]`. It is the ceiling the user
signs. A higher on-chain fee at execution reverts with `ERR_PRICE_ABOVE_MAX`, reported as
`409 PRICE_ABOVE_MAX`. An increase of `sender_fee_ru` takes effect only 172 800 000 ms after it is set
(`SENDER_FEE_NOTICE_MS`). `GET /v1/pricing` does not publish a pending `sender_fee_ru`: the fee this route quotes in `feeMicroUsdc` is the one in force when it answers, and it is the ceiling the user signs.

Auth: `public`

Request body (required):
PrepareSendersRequest (see /openapi.yaml)

Responses:
- `200` Unsigned transaction + assignment + fee preview. → PreparedTransaction
- `400` 
- `422` 
- `429` 

## x402

x402 v2 purchase flow, paymentFlow "upfront".

### GET /v1/x402/supported

operationId: `getX402Supported`

x402 v2 `supported` document - kinds, extensions and `signers` (our active relayers per CAIP-2 pattern).

Auth: `public`

Responses:
- `200` Supported kinds. → X402Supported

### POST /v1/x402/topup

operationId: `x402Topup`

x402 v2 purchase of credits (`deposit` / `depositFor`). Unpaid request -> 402 challenge, paid retry -> settle before serve.

These rules hold on both API hosts, and the same rules apply to `/v1/x402/purchase`:
- `accepted` is verified by **recomputing `extra.quoteMac`**, never by a lookup, because the challenge
  may have been issued by the other API host.
- The payment record keyed by `extensions["payment-identifier"].id` is written to the database
  **before** anything is co-signed. If the database is unreachable the answer is
  `503 UPSTREAM_UNAVAILABLE` and nothing is co-signed. The same id with the same payload returns the
  stored result; the same id with another payload gives `402 PAYMENT_INVALID`.
- `503 CONTRACT_PAUSED` / `503 SWAP_VENUE_PAUSED` are answered before any co-signature.
- `asset`, `network` and `payTo` are the values of the serving network (`USDC-c76f1f` / `multiversx:1`
  on mainnet, `USDC-350c4e` / `multiversx:D` on devnet staging).

Auth: `x402`

Parameters:
- `undefined` (undefined, unspecified, optional)

Request body (required):
X402TopupRequest (see /openapi.yaml)

Responses:
- `200` Payment executed successfully on chain and the `deposit` event was mirrored. → X402PurchaseResult
- `202` Co-signed and broadcast, completion exceeds the HTTP budget (8 000 ms). Poll `GET /v1/x402/payments/{paymentId}`. → X402Pending
- `400` 
- `402` 
- `429` 
- `503` 

### POST /v1/x402/purchase

operationId: `x402Purchase`

x402 v2 purchase of a plan in one payment (`depositAndSubscribe(tier_id, months, max_price, ref)`).

The beneficiary is always the payer (contract rule). `ref` = the quote id, bound under the payer's
signature. Quote verification, payment-identifier persistence and the pause answers follow the
cross-host rules listed under `POST /v1/x402/topup`.

Auth: `x402`

Parameters:
- `undefined` (undefined, unspecified, optional)

Request body (required):
X402PurchaseRequest (see /openapi.yaml)

Responses:
- `200` Payment executed and `subscribed` event mirrored. → X402PurchaseResult
- `202` Pending, poll. → X402Pending
- `400` 
- `402` 
- `429` 
- `503` 

### GET /v1/x402/payments/{paymentId}

operationId: `getX402Payment`

Poll a pending x402 payment by its `payment-identifier`.

Auth: `public`

Parameters:
- `paymentId` (path, string, required)

Responses:
- `200` Final result (success or failure); `PAYMENT-RESPONSE` header is set. → X402PurchaseResult
- `202` Still pending. → X402Pending
- `404` 

## registry

Off-chain mirror of the on-chain relayer registry (convenience, not authority).

### GET /v1/relayers

operationId: `listRelayers`

Off-chain mirror of the relayer registry. Balances are never exposed here.

The contract view `getRelayerState` is the authority. Agents should verify a relayer through a gateway that CoRelayer does not run (https://docs.co-relayer.com/concepts/verify-a-relayer).

Auth: `public`

Parameters:
- `shard` (query, Shard, optional)
- `state` (query, RelayerStateName, optional)

Responses:
- `200` Registry mirror. → RelayerRegistry

### GET /v1/relayers/{address}

operationId: `getRelayerPublic`

One relayer with its state history and public service statistics.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Relayer. → RelayerPublicDetail
- `404` 

## status

Component status and incident feed.

### GET /v1/status

operationId: `getStatus`

Component status, measured latency per shard, 90-day daily availability, float status label per shard, open incidents.

The same document is published every 60 000 ms as `https://status.co-relayer.com/feed/status.json`.
Next to it are `/feed/incidents.json` (the first page of `GET /v1/incidents`) and
`/feed/telemetry.json` (`latency30d` + `availabilityDaily90`). They are three files because they have
different cache lifetimes, and the status page renders each one on its own.
Synthetic probe traffic is excluded from every public counter. Percentages are measured over past
traffic and carry no service-level guarantee.
`floatPerShard` is a status label, never a balance.

Auth: `public`

Responses:
- `200` Status. → Status

### GET /v1/incidents

operationId: `listIncidents`

Public incident feed, newest first.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `shard` (query, Shard, optional)
- `status` (query, IncidentStatus, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of incidents.
- `400` 

### GET /v1/incidents/{incidentId}

operationId: `getIncident`

One public incident with all updates.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Incident. → Incident
- `404` 

## account

Public mirror of on-chain account state plus the derived quota.

### GET /v1/me

operationId: `getMe`

The accounts the token's address can open in the dashboard.

Lists the accounts the token's address owns or is an authorised sender of. Each account's `role` picks
the full dashboard (`role: owner`) or the read-only authorised-sender mode (`role: sender`).

Auth: `native-auth`

Responses:
- `200` Identity view of the token address. → Me
- `401` 
- `403` 
- `429` 

### GET /v1/account/{erd}

operationId: `getAccount`

Mirror of the on-chain account record (credits, escrow, plan blocks, flags). Pure chain mirror, therefore public.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Account mirror. → Account
- `404` 

### GET /v1/account/{erd}/quota

operationId: `getQuota`

Derived service state of an account ("halted" lives here, never on-chain). Evaluated in shard-1 chain time.

`{erd}` may also be an authorised sender. The response then describes the account that would be
billed for that sender, and `resolvedFrom` says how that account was found.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Quota. An address without an account answers 200 with `state = NO_ACCOUNT`. → Quota
- `429` 

### GET /v1/account/{erd}/senders

operationId: `listSenders`

On-chain authorised senders of the account (mirror of `senderAdded` / `senderRemoved`).

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of senders, oldest authorisation first (the serving order).
- `404` 

### GET /v1/account/{erd}/purchases

operationId: `listPurchases`

Money timeline of the account - deposits, plan purchases, renewals, skipped renewals, escrow moves, PAYG settlements, sender fees, grants.

Built only from finalised contract events, so it is public. Newest first.

Auth: `public`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `kind` (query, PurchaseKind[], optional) — Comma-separated filter.
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of purchase events (`format=csv` streams all matching rows without a cursor).
- `404` 

## dashboard

Private read models of an account (usage, latency, notices, served-by relayers, outages).

### GET /v1/account/{erd}/notices

operationId: `listNotices`

Pollable notices - account notices plus global ones (tariff, RU schedule, incidents).

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `sinceSeq` (query, integer (int64), optional) — Return notices with `seq` greater than this value (agents poll with the last seen seq).
- `unacknowledged` (query, boolean, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of notices, newest first.
- `401` 
- `403` 

### POST /v1/account/{erd}/notices/ack

operationId: `ackNotices`

Acknowledge notices up to a sequence number (idempotent).

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)

Request body (required):
- `upToSeq` (integer (int64), required)

Responses:
- `204` Acknowledged.
- `401` 
- `403` 

### GET /v1/account/{erd}/relayers

operationId: `listServingRelayers`

"Their relayers" under just-in-time assignment = the relayers that served this account, with counts and on-chain state.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Relayers that served the account in the window (default last 30 days).
- `401` 
- `403` 

### GET /v1/account/{erd}/outages

operationId: `listAccountOutages`

Public incidents that overlapped the account's traffic, with the account's own impact numbers.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of outages.
- `401` 
- `403` 

### GET /v1/usage

operationId: `listUsage`

Full transaction history of an account - every terminal intent incl. `DEAD` - with per-transaction latency breakdown in ms.

The account's transaction history, one row per transaction hash. It holds **every terminal intent,
including `DEAD` ones** (with `deadReason`). `billed` says whether the transaction was billed; the
billing members are null when it was not.
Rows are keyed by transaction hash. A `DEAD` intent that was replaced by a cancel or a re-sign shares
its `(sender, senderNonce)` slot with the replacement that executed, and only the executed one is
billed. The replaced transaction stays in the history with `status: dead` and `billed: false`.
`includeInFlight=true` puts the account's non-terminal intents first (flagged `inFlight`), plus the
`REJECTED` submissions of the last 30 days (`state: REJECTED`, `rejectCode`, never a signature).
Order: `executedBlockTsMs` descending (the terminal timestamp for rows without a block), then `txHash`.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `account` (query, Address, required)
- `sender` (query, Address, optional)
- `relayer` (query, Address, optional)
- `status` (query, UsageStatus[], optional) — Comma-separated terminal statuses (`dead` = terminal without execution, never billed).
- `state` (query, IntentState[], optional) — Comma-separated intent states; only meaningful with `includeInFlight=true` (in-flight and `REJECTED` rows).
- `shard` (query, Shard[], optional) — Comma-separated sender shards.
- `minInclusionMs` (query, integer, optional) — Only rows whose `latency.inclusionMs` is at least this value (slow-transaction filter).
- `q` (query, string, optional) — Prefix match on `txHash`, `sender`, `receiver` or `function` (3 to 64 characters, treated as text).
- `billingClass` (query, BillingClass, optional)
- `periodId` (query, integer, optional)
- `apiKeyId` (query, string, optional)
- `includeInFlight` (query, boolean, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of usage rows.
- `400` 
- `401` 
- `403` 

### GET /v1/usage/summary

operationId: `getUsageSummary`

Usage versus cap - period totals plus a bucketed time series (RU, tx count, fail count, latency percentiles), top senders and receivers.

RU and money totals count **billed rows only** (`billed = true`). `DEAD` and `REJECTED` rows appear
in `totals.txCountByStatus` and nowhere else, so the totals always equal the sum of the
`GET /v1/usage` rows with `billed = true`.
Grouped latency analytics are served by `GET /v1/usage/latency`.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `account` (query, Address, required)
- `periodId` (query, integer, optional) — Default = current period.
- `bucket` (query, "hour" | "day", optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Summary. → UsageSummary
- `401` 
- `403` 

### GET /v1/usage/latency

operationId: `getUsageLatency`

Latency analytics of an account - percentiles, server-side histogram bins and sampled points per group (shard, relayer, sender or gateway).

Served from pre-aggregated per-minute and per-hour latency histograms, grouped by `relayer`, `sender`
or `first_ack_gateway`. `samples[]` holds at most 48 sampled points per group, for a strip plot.
Only measured values are returned: a group without data is absent, never estimated. Synthetic probes
are excluded.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `account` (query, Address, required)
- `window` (query, "1h" | "24h" | "7d" | "30d", optional) — Look-back window ending now. `1h` and `24h` read the 1-minute rollup, `7d` and `30d` the 1-hour rollup.
- `groupBy` (query, "shard" | "relayer" | "sender" | "gateway", optional)

Responses:
- `200` Grouped latency. → UsageLatency
- `400` 
- `401` 
- `403` 
- `429` 

### GET /v1/usage/{txHash}

operationId: `getUsageRow`

One row of the transaction history: the billing details when it was billed, the latency breakdown, the settlement batch and the gateway acknowledgements.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Usage row. → UsageRow
- `401` 
- `403` 
- `404` 

### GET /v1/usage/{txHash}/proof

operationId: `getUsageProof`

Merkle inclusion proof of a ledger row against the on-chain `usage_root` of its settlement batch.

Auth: `native-auth`, `sponsor-key`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Proof. Verifiable with SHA-256 only, against the `usageSettled` event of `batchId`. → UsageProof
- `401` 
- `403` 
- `404` 
- `409` Row exists but is not yet committed in a root (next root-only batch is at most 3 600 000 ms away). → Problem

## keys

Sponsor API keys.

### GET /v1/account/{erd}/keys

operationId: `listApiKeys`

Sponsor API keys of the account (secrets are never returned again).

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Keys.
- `401` 
- `403` 

### POST /v1/account/{erd}/keys

operationId: `createApiKey`

Mint a sponsor API key. The secret is shown exactly once.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Request body (required):
ApiKeyCreate (see /openapi.yaml)

Responses:
- `201` Created. → ApiKeyCreated
- `401` 
- `403` 
- `409` 
- `422` 

### PATCH /v1/account/{erd}/keys/{keyId}

operationId: `updateApiKey`

Change label or policy of a key.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Request body (required):
ApiKeyUpdate (see /openapi.yaml)

Responses:
- `200` Updated key. → ApiKey
- `401` 
- `403` 
- `404` 
- `422` 

### DELETE /v1/account/{erd}/keys/{keyId}

operationId: `revokeApiKey`

Revoke a key. Effective on every host within 2 000 ms; in-flight reservations complete and bill the account.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `204` Revoked.
- `401` 
- `403` 
- `404` 

## notifications

Webhooks, e-mail preferences, SSE stream.

### GET /v1/account/{erd}/webhooks

operationId: `listWebhooks`

Webhook endpoints of the account.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Webhooks.
- `401` 
- `403` 

### POST /v1/account/{erd}/webhooks

operationId: `createWebhook`

Register an HTTPS webhook endpoint. The signing secret is shown exactly once.

The URL must use https on port 443 and resolve to a public unicast address, which is checked again on every connection. Redirects are not followed. Any other URL gives `WEBHOOK_URL_NOT_ALLOWED`.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Request body (required):
WebhookCreate (see /openapi.yaml)

Responses:
- `201` Created, includes `secret`. → WebhookCreated
- `401` 
- `403` 
- `409` 
- `422` 

### GET /v1/account/{erd}/webhooks/{webhookId}

operationId: `getWebhook`

One webhook endpoint.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Webhook. → Webhook
- `401` 
- `403` 
- `404` 

### PATCH /v1/account/{erd}/webhooks/{webhookId}

operationId: `updateWebhook`

Change URL, event types or enabled flag.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Request body (required):
WebhookUpdate (see /openapi.yaml)

Responses:
- `200` Updated webhook. → Webhook
- `401` 
- `403` 
- `404` 
- `422` 

### DELETE /v1/account/{erd}/webhooks/{webhookId}

operationId: `deleteWebhook`

Delete a webhook endpoint.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `204` Deleted.
- `401` 
- `403` 
- `404` 

### POST /v1/account/{erd}/webhooks/{webhookId}/test

operationId: `testWebhook`

Send a signed `webhook.test` event now and return the delivery result.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Delivery attempt. → WebhookDelivery
- `401` 
- `403` 
- `404` 
- `429` 

### POST /v1/account/{erd}/webhooks/{webhookId}/rotate-secret

operationId: `rotateWebhookSecret`

Issue a new signing secret. The old secret stays valid for 86 400 000 ms; deliveries carry both signatures meanwhile.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` New secret, shown once. → WebhookCreated
- `401` 
- `403` 
- `404` 

### GET /v1/account/{erd}/webhooks/{webhookId}/deliveries

operationId: `listWebhookDeliveries`

Delivery log of the last 30 days, newest first.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Page of deliveries.
- `401` 
- `403` 
- `404` 

### GET /v1/account/{erd}/notifications

operationId: `getNotificationPrefs`

Notification preferences (thresholds, e-mail channel, per-kind switches).

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Preferences. → NotificationPrefs
- `401` 
- `403` 

### PUT /v1/account/{erd}/notifications

operationId: `putNotificationPrefs`

Replace notification preferences. A new e-mail address stays `pending` until its verification link is opened.

Auth: `native-auth`

Parameters:
- `undefined` (undefined, unspecified, optional)

Request body (required):
NotificationPrefsUpdate (see /openapi.yaml)

Responses:
- `200` Stored preferences. → NotificationPrefs
- `401` 
- `403` 
- `422` 

### POST /v1/stream/tickets

operationId: `createStreamTicket`

Single-use ticket for clients that cannot send an Authorization header (native EventSource). TTL 30 000 ms.

Auth: `native-auth`, `sponsor-key`

Request body (required):
- `account` (Address, required)
- `topics` (StreamTopic[], optional)

Responses:
- `201` Ticket.
- `401` 
- `403` 
- `429` 

### GET /v1/stream

operationId: `streamAccount`

Server-sent events for one account (intents, quota, account mirror, notices, incidents, pricing).

Auth: send the `Authorization` header from a streaming HTTP client. From a browser `EventSource`,
use a single-use `ticket` query parameter instead. A native-auth token is never accepted in a URL.
Event names are the `StreamTopic` values plus `reset`, and every client must handle `reset`.
`data:` is one `StreamEvent` JSON object. `id:` is `<hostId>.<bootEpoch>.<seq>`: `hostId` is `a` or
`b` (the core host), `bootEpoch` is the start time of that API process in Unix ms, and `seq` counts
this account's events on that host.
`Last-Event-ID` replays up to 300 000 ms or 1 000 events. The server sends `event: reset` first
when the `Last-Event-ID` is older than that window, comes from the other API host, or comes from
before a server restart. On `reset`, reload your data. Each host numbers its own events, so an ID
from one host cannot be resumed on the other.
The server coalesces at 4 to 10 Hz and sends a comment line every 15 000 ms as keep-alive.

Auth: `native-auth`, `sponsor-key`, `ticket`

Parameters:
- `account` (query, Address, optional) — Required with header auth; implied by the ticket otherwise.
- `topics` (query, StreamTopic[], optional) — Comma-separated; default all.
- `undefined` (undefined, unspecified, optional)

Responses:
- `200` Event stream of `StreamEvent` objects.
- `401` 
- `403` 
- `429` 

## compat

Drop-in facade for OpenClaw / Moltbot starter-kit agents. Unversioned and frozen.

### GET /health

operationId: `compatHealth`

Compat health probe of the OpenClaw relayer interface.

Auth: `public`

Responses:
- `200` OK.

### GET /relayer/address/{userAddress}

operationId: `compatRelayerAddress`

Deterministic, long-lived relayer address for a sender (rendezvous hash over the shard's active relayers). No lease.

Auth: `public`

Parameters:
- `userAddress` (path, Address, required)

Responses:
- `200` Relayer address.
- `404` Invalid address. Body keeps the kit's `error` string and is also a valid problem document. → CompatError

### POST /relay

operationId: `compatRelay`

Compat relay. Accepts a transaction naming any of our active or draining same-shard relayers. First receipt only.

The signed transaction is the only credential. Limits are stricter than on `POST /v1/relay`: 1 RU/s
per sender with a burst of 5, `gasPrice == min`, simulation always on, `gasLimit <= 60 000 000`, and
no replacement modes. The handler takes an internal lease of 5 000 ms inside the request.
Only the statuses 200/400/403/404/429/500 are emitted. `challengeNonce` is accepted and ignored.

Auth: `tx`

Request body (required):
- `transaction` (TransactionPlain, required)
- `challengeNonce` (string, optional) — Ignored. Kept so unmodified kit clients validate.

Responses:
- `200` Co-signed. `txHash` is returned as soon as the commit point is passed.
- `400` Any validation failure, and `RELAYER_RETIRED` (body explains clearing `.relayer_cache.json`). → CompatError
- `403` `NO_ENTITLEMENT` or `SENDER_NOT_AUTHORIZED`. → CompatError
- `429` `QUOTA_EXHAUSTED`, `RATE_LIMITED`, `GAS_BUDGET_EXCEEDED`, `TOO_MANY_IN_FLIGHT`. `error` starts with "Quota exceeded" for quota cases. → CompatError
- `500` Everything else (including 503-class conditions of the native path). → CompatError
