Use when an AI agent needs to PAY an external service it does not have an account with — any x402 agent or MPP merchant — using the Nevermined Router. Covers discovering services in the Agent Services Catalog, creating a spending Delegation from an API key, funding the buyer wallet, pricing a call first with /api/v1/router/quote, making paid calls through /api/v1/router/route (or the streaming /proxy), reading the payment ledger, and the guardrails an autonomous buyer must respect. Complements the nevermined-payments skill, which is about RECEIVING payments and buying Nevermined plans.
---
name: nevermined-router
version: "0.1.5"
lastUpdated: "2026-09-26"
description: >
Use when an AI agent needs to PAY an external service it does not have an account
with — any x402 agent or MPP merchant — using the Nevermined Router. Covers
discovering services in the Agent Services Catalog, creating a spending Delegation
from an API key, funding the buyer wallet, pricing a call first with
/api/v1/router/quote, making paid calls through /api/v1/router/route (or the
streaming /proxy), reading the payment ledger, and the
guardrails an autonomous buyer must respect. Complements the nevermined-payments
skill, which is about RECEIVING payments and buying Nevermined plans.
metadata:
openclaw:
primaryEnv: NVM_API_KEY
requires:
env:
- NVM_API_KEY
envVars:
- name: NVM_API_KEY
required: true
description: >
Nevermined API key. Format `sandbox:...` for the sandbox environment,
`live:...` for production. Issued from https://nevermined.app under API
Keys. Must be a current-generation key — older keys are refused by the
Router with 403 BCK.ROUTER.0008.
---
# Nevermined Router — buy from any x402 or MPP service
> **Skill version**: 0.1.5 | **Last updated**: 2026-09-26 | **Canonical source (always latest):** https://github.com/nevermined-io/docs/tree/main/skills/nevermined-router
>
> **⚠️ Use the latest version.** If you have a cached copy, check its **Last updated** date against the canonical source and refresh if older.
>
> Human-readable twin of the Router documentation at https://nevermined.ai/docs/products/catalog/router/overview. Same facts, same error codes — if the two ever disagree, the docs site is authoritative and this skill has a bug.
## What this is for
You are an agent that needs something from a service you have **no account with, no API key for, and no billing relationship with**. The Router lets you pay it per request, from a budget a human capped in advance, and puts every spend on one ledger.
It works because a growing set of services quote their price **on the wire**: you call them, they answer `402 Payment Required` with what they want, you pay, you get the resource. The Router does the paying.
| | |
| --- | --- |
| **Use this skill when** | you need to buy a single call from an external x402 / MPP service |
| **Use `nevermined-payments` instead when** | you are *charging* callers, or buying a Nevermined **plan** with credits |
<a id="not-for"></a>
**This skill cannot help you with conventional SaaS APIs.** Exa, Firecrawl, Tavily and similar are billed out of band — a monthly plan, a long-lived key. They never quote a price for one call, so there is nothing on the wire for the Router to pay and no address to pay it to. The Router isn't missing a feature; the transaction it performs does not exist for those services. If a service answers `401` or `403` rather than `402`, it wants **authentication**, not payment — stop, and tell the user it needs an account.
That is not a dead end, just a different rail. Nevermined can still buy from such a provider **out of band** — purchasing API credits up front instead of paying per call. Exa is the worked example: a $7 x402 card-delegation purchase provisions or tops up an Exa API key, fully agent-driven — https://nevermined.ai/docs/integrations/exa. That flow belongs to the `nevermined-payments` skill and the Payments SDK. What you cannot do is put those calls through `/router/route`.
## The buy loop
Six steps. Steps 1–3 happen once; 4–6 repeat per purchase.
```
① API key ──▶ ② Delegation (budget) ──▶ ③ Fund the buyer wallet
│
┌─────────────────────────┘
▼
④ Discover a service ──▶ ⑤ POST /router/route ──▶ ⑥ Read the spend
(catalog) (pays + relays) (ledger)
```
Set your environment once:
```bash
export NVM_API_URL="https://api.sandbox.nevermined.app" # live: https://api.live.nevermined.app
export NVM_API_KEY="<your-api-key>"
```
Everything is plain HTTP with `Authorization: Bearer $NVM_API_KEY`. There is **no SDK for the Router yet** — that is deliberate here, because it means any agent in any language can drive it with an HTTP client. The one exception is the catalog, which is public and needs no key at all.
<a id="rail-availability"></a>
**The two rails are enabled independently per deployment, and the MPP rail is not on everywhere.** x402 (Base) is available by default. MPP (Tempo) requires the operator to allowlist the payment token for that chain, and the allowlist is **fail-closed** — where it is unset, *every* MPP service is refused with `400 BCK.ROUTER.0001 … not allowlisted`, before anything is signed.
So **a service being in the catalog does not mean your deployment can pay it.** The catalog describes services; it says nothing about how the deployment you are pointed at is configured. If MPP services fail with `0001 … not allowlisted` while x402 services pay fine, the rail is off where you are — that is a deployment setting, not something you can fix from the client, not a fault in the merchant, and not a reason to retry or to go looking for a different MPP service, which will fail identically. Ask the operator of your deployment, or stay on `protocol=x402`. See `references/errors.md`.
**Never send `NVM_API_KEY` to the service you are paying.** It authenticates you to Nevermined and nothing else. If a merchant needs its own auth, pass it in `headers` (mode B) — see `references/paying.md`.
---
## ① Get an API key — *needs a human once*
Issued from the Nevermined app. If you were given one, use it.
A key that predates the Router is refused with **`403 BCK.ROUTER.0008`**. The fix is to create a new key; newly issued keys work. Old keys keep working for credit-based flows, so nothing else needs rotating.
## ② Create a Delegation — *fully programmatic*
A **Delegation** is the budget: a hard cap in cents plus an expiry, enforced server-side on every single payment. Create it once, reuse the id.
```bash
curl -sX POST "$NVM_API_URL/api/v1/delegation/create" \
-H "Authorization: Bearer $NVM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"erc4337","currency":"usdc","spendingLimitCents":500,"durationSecs":604800}'
# → { "delegationId": "5e7481c3-e972-45bd-bdc5-a0b99c4de4a1" }
```
That is a $5.00 cap for 7 days. `provider: "erc4337"` is the crypto-funded Delegation both stablecoin rails require — a card-funded Delegation is refused on them.
<a id="never-widen"></a>
**You may create a Delegation. You must never widen one to get past a refusal.** The cap is the human's decision; a refusal is that decision taking effect. See [Guardrails](#guardrails).
Two guards can refuse this call before your fields are read, and **neither is retryable**:
- **`403 BCK.OAUTH.0030`** — the key was minted through an OAuth consent ceremony (`credits_purchase`, `account_access` or `commerce`) and may not create Delegations *or* use the paying routes. Use a plain API key issued by the account owner — or, if the key comes from a `commerce` grant, spend through `POST /api/v1/router/commerce/route` (and price a call through `POST /api/v1/router/commerce/quote`), which derive the Delegation from the grant instead of taking one from you.
- **`412 {"error":"consent_required","outdated":[…]}`** — the account's legal-document consent has lapsed. ⚠️ Its only `code` is the generic **`BCK.HTTP.412`**, which names the status and not the cause, so branch on `body.error === "consent_required"` — the one place "branch on `code`" needs a second field. A human must accept; report it and stop.
Full field list, recipient scoping, both guards in detail, and reading a Delegation's live state: `references/bootstrap.md`.
## ③ Fund the buyer wallet — *may need a human*
Both rails **pull**: the merchant takes funds from your own wallet. The Delegation authorizes the spend; it does not provide the money. Read the wallet address off the Delegation:
```bash
curl -s "$NVM_API_URL/api/v1/delegation/$NVM_DELEGATION_ID" \
-H "Authorization: Bearer $NVM_API_KEY"
# → { "providerPaymentMethodId": "0x8F60b3838e6C121FcDBdBc50e7B150F8560a670E", ... }
```
`providerPaymentMethodId` is the address to fund, with the payment asset, **on the network you intend to pay on**.
**Your deployment funds exactly one x402 network, fixed by its environment: sandbox → `base-sepolia`, live → `base`.** A merchant on the other chain is unpayable from where you are and fails with `400 BCK.ROUTER.0001 … no fundable option`, which reads like a broken service and is not. Check the environment before blaming the merchant — see `references/bootstrap.md`.
**Always read this address back from the live Delegation — never from a value you cached.** Funding a stale address is the most common cause of `402 BCK.ROUTER.0009`, and the error deliberately does not echo the address it checked, so it cannot tell you that is what happened.
If the wallet is empty and you cannot fund it yourself, that is a **stop condition**: report it to the human. Do not retry.
## ④ Discover a service
The **Agent Services Catalog** is one public JSON feed — no API key, no query parameters. Fetch it and filter on your side:
```bash
curl -s https://nevermined.app/catalog/ai-catalog.json \
| jq '[.services[] | select(.protocol == "x402" and .category == "Search & Research")
| {slug, title, priceLabel, endpoints: [.endpoints[] | {method, path: (.invokePath // .path), description}]}]'
```
```json
[
{
"slug": "superhighway",
"title": "Superhighway — Web Search for Agents",
"priceLabel": "$0.001",
"endpoints": [
{ "method": "POST", "path": "/search", "description": "Web search" },
{ "method": "POST", "path": "/news", "description": "Real-time news search" },
{ "method": "POST", "path": "/images", "description": "Image search" }
]
}
]
```
(An excerpt: the real result lists every match.) `/api/v1/catalog/services` and `/api/v1/catalog/categories` are **not a public API** — they return `403` by design. For server-side search, the Catalog MCP (`search_services`, `get_service`, `list_categories` at `https://mcp.live.nevermined.app/mcp`) is free and needs no key.
Two rules that will otherwise cost you a wasted payment:
1. **Only `protocol` of `x402` or `mpp` is payable through the Router.** Filter for them. Anything else in the catalog is listed for discovery, not for routing — see [above](#not-for).
2. **Pay a listed service by its `slug`, never by URL.** The feed carries no merchant URL on purpose: the Router resolves it server-side and refuses a raw-URL payment to a cataloged host (`409 BCK.ROUTER.0014`). The subpath to send is the endpoint's `invokePath` when present — **even `""`, which means "append nothing"** — and its `path` otherwise:
```js
const subpath = endpoint.invokePath ?? endpoint.path // NOT `||`: '' must stay ''
// edgar-search: path '/edgar-search/search', invokePath '' → send ''. Sending the path double-stacks it and 404s after the charge.
```
More filter recipes, categories, the Catalog MCP, and the ARD host document: `references/discovery.md`.
**From API 1.55**, server-side selection (`POST /api/v1/router/select` and MCP `route_by_intent`)
accepts exact opaque catalog slugs in `filters.require`, `filters.prefer` and `filters.exclude`.
`require` is the strict control: the Router selects that slug or fails closed with
`409 BCK.ROUTER.0031`; it never silently substitutes another service. `prefer` falls back to normal
ranking when its slug is not payable, while `exclude` removes its slug from consideration.
## ⑤ Make the paid call
Hand the Router the request you want made. It probes the service, **auto-detects** the protocol from the 402, pays, and relays the answer — one call, and you never see the 402.
```bash
curl -sX POST "$NVM_API_URL/api/v1/router/route" \
-H "Authorization: Bearer $NVM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"delegationId": "'"$NVM_DELEGATION_ID"'",
"slug": "superhighway",
"path": "/search",
"method": "POST",
"body": { "query": "nevermined router" },
"requestId": "search-nevermined-router-v1"
}'
```
A cataloged service is addressed by `slug` + `path` (the endpoint's `invokePath ?? path`). Send an absolute `url` instead only for an off-catalog x402 / MPP service.
```json
{
"status": 200,
"body": { "…": "the paid resource" },
"paid": true,
"payment": {
"paymentId": "b1f9c2e4-…",
"settlement": { "amount": "1000", "asset": "USDC", "network": "base", "approxCents": "1" },
"fee": { "bps": 0, "amount": "0", "cents": "0", "capChargedCents": "1" },
"txHash": "0xfc8af37b…",
"status": "Settled"
}
}
```
`status` and `body` are the merchant's own, unchanged. `paid: false` with no `payment` block means nothing was charged. For a catalog `slug`, the body of a free answer is withheld (`null`), except when you poll the result of an async job you already paid for through the same slug: see "Async services" in `references/paying.md`.
<a id="fee"></a>
**`settlement.approxCents` is the merchant leg, not your bill.** Nevermined charges a routing fee on top, disclosed in the **always-present `fee` object** (zeroed when no fee applied, so never branch on its absence): `fee.capChargedCents` is what this call **reserved** against your Delegation cap — `settlement.approxCents + fee.cents`. Sum `capChargedCents`, not `approxCents`, or your accounting drifts by exactly the fee.
⚠️ It is the **reserve at mint**, not the final figure: if a mode-B hop does not return `2xx` the fee half is given back (the merchant leg stays charged), so a running total over-reports on those calls. **`GET /api/v1/delegation/{id}` is the authority on what you have actually spent** — reconcile against `amountSpentCents` rather than your own sum. Full field list: `references/paying.md`.
<a id="requestid"></a>
**`requestId` is required, and it is an idempotency key — not a request counter.** Use **one stable id per logical purchase** and reuse it across retries of that purchase. Retrying a dropped call with the same id returns `409 BCK.ROUTER.0002` carrying the original `paymentId` — **not the resource** — instead of buying twice; a fresh id buys twice, on purpose. **Never answer that 409 by minting a fresh id**: that is the double-spend the key just prevented. If the purchase genuinely failed, report it. Derive it from the work you are doing (`"search-nevermined-router-v1"`), not from `uuid4()` per HTTP attempt — a fresh UUID on every retry is how an agent double-spends.
<a id="pending"></a>
**From API version 1.48** (a key pinned at or above it — new keys are pinned to the current version), a slow service no longer holds your connection. If it has not answered within **45 s**, `/route` answers `202 { paymentId, resultUrl, status: "Pending" }` (`/router/proxy` · `/router/svc`: `202` with `Location` and `X-Router-Payment-Id`, and only before the service starts responding). **The payment is made**; the call keeps running on the Router (up to its 120 s ceiling) and its fee is collected on a `2xx` or released otherwise, exactly as if you had waited. **Poll `GET $NVM_API_URL{resultUrl}`** with the same key: `202` while `Pending`, then `200` with `state: "Ready"` (`status` + `body`, as `/route` would have returned them) or `state: "Failed"` (`failureReason`), and `404 BCK.ROUTER.0030` once it has expired. **Never pay again for a `202`.** Every paid result is kept for 24 h after the call ends, for the paying account only — a paid `/route` response carries it as `payment.resultUrl` — except a `/proxy` · `/svc` response that already streamed to you. A same-`requestId` retry returns that `202`, then the retained result, instead of the 409 — on `/proxy` · `/svc` too, when the first call answered `202`. It still gets the 409 when nothing was kept (a `/proxy` · `/svc` response that already streamed to you), when the first call `Failed`, or when the id was reused for a different target. Older keys keep waiting and get the 409, though their results are kept too. Either way, keep the same id.
<a id="quote"></a>
**Price it first: `POST /api/v1/router/quote`** (deployments on API 1.48 or later). Send the same body as `/route` minus `requestId`, `maxTotalCents` and `protocol`, which are stripped rather than refused (`delegationId` stays optional). The Router makes the same unpaid request to the service that a payment would, selects the option a payment would select, prices it with the routing fee — and stops. **Nothing is signed, minted, recorded or reserved.** It answers `200` with `paymentRequired`, `optionSet`, `protocol` and the same `settlement` and `fee` objects `/route` returns. Then route with `maxTotalCents` set to the quote's `fee.capChargedCents`, so a price that rose in between is refused (`402 BCK.ROUTER.0018`) instead of paid.
**From API 1.55**, a payment-required quote also returns a short-lived opaque `quoteId` and
`expiresAt` **60 seconds** later. Pass that `quoteId` to `/route` with the exact quoted target,
method, headers, body, credential header and Delegation. It pays the sealed merchant challenge at
the exact fee-inclusive amount without accepting a changed call; keep `maxTotalCents` too as an
independent ceiling. An invalid/wrong-account id is `0029`, an expired id is `0032`, and any request,
Delegation, rail or amount mismatch is `0033` — all before a charge. Re-quote when the call changes
or the id expires. Clients pinned below 1.55 receive no `quoteId`/`expiresAt` and retain the legacy
re-probe-plus-ceiling flow.
- **It is not free of side effects.** The request really reaches the service, so a service that does not charge for it performs it — take care quoting a method with side effects. And a quote spends the same per-key and per-service rate budgets as a payment: **quote once per decision, don't poll.**
- `fee.capChargedCents` is whole cents rounded **up** from `fee.capChargedMicros` (exact, in 1/10,000 of a cent), and it is the figure `maxTotalCents` is compared against. The ceiling has whole-cent resolution, so any price up to that whole cent is still paid: a 2.04¢ quote paid with `maxTotalCents: 3` accepts up to 3.00¢.
- `optionSet: "delegation"` means the `delegationId` you sent was priced (a card Delegation pays over MPP-stripe, an organization-wallet Delegation pays in its own currency, a recipient allowlist is enforced); `"deployment"` means you sent none, so this is what a personal crypto Delegation would select. `paymentRequired: false` means the service did not ask for payment; `upstreamStatus` is its answer, and its body is never returned.
- A quote checks neither your remaining cap nor your wallet balance — the payment still does. `503 BCK.ROUTER.0028` (a read the price depends on failed) is retryable with backoff.
- **With an OAuth `commerce` credential**, quote on `POST /api/v1/router/commerce/quote` instead (it ships in the first API release after 1.49; until your deployment has it, the route answers `404`, so bound the price with `maxTotalCents` alone): same body and answer, but it prices the Delegation the grant is pinned to, so sending a `delegationId` is refused (`400 BCK.OAUTH.0034`). Then pay on `/router/commerce/route`.
Mode A (you call the merchant yourself), the streaming `/proxy` variant, and passing the merchant's own auth: `references/paying.md`.
## ⑥ Read what you spent
```bash
curl -s "$NVM_API_URL/api/v1/router/payments?delegationId=$NVM_DELEGATION_ID" \
-H "Authorization: Bearer $NVM_API_KEY"
```
Every payment, every protocol, one ledger. Filters, CSV export, and the aggregate summary: `references/ledger.md`.
---
<a id="guardrails"></a>
## Guardrails — read this before writing a retry loop
The Router signs payments from your wallet in response to instructions written by a merchant nobody vetted. It is deliberately suspicious, and **a refusal is the system working**.
**Four rules for an agent that spends without a human watching:**
1. **`402 BCK.ROUTER.0003` (over cap / expired) and `402 BCK.ROUTER.0009` (wallet short) are stop conditions.** They mean "out of budget" and "out of money". Report them to the human. Do not route around them.
2. **Never widen a Delegation, and never create a second one, in response to a refusal.** The cap is the user's decision, not a runtime obstacle. Creating a fresh Delegation to escape an exhausted one defeats the entire mechanism — it is the single worst thing you can do with this API.
3. **One `requestId` per purchase**, reused across retries of that purchase. See [above](#requestid).
4. **Retry only the codes the table below marks retryable: `0006` (500, summary read), `0007` (429), `0020` (an upstream 5xx/429), `0022` (500, selection not wired) and `0028` (503, quote).** On the paying path that means `0007` and `0020`. Everything else is a decision, and retrying it unchanged produces the same answer. Back off on `0007` (too many routed calls in flight) and on `0020` (the upstream service errored or throttled). **The HTTP status does not tell you whether to retry** — `0010` is a 500 you must not retry and `0011` is a 402 you must not retry. Read the code, not the status.
**Check the price before you commit.** `priceLabel` in the catalog is indicative — [quote the call](#quote) for its live, fee-inclusive price; on the response, `settlement.approxCents` is what the **merchant** charged and [`fee.capChargedCents`](#fee) is what your **cap** reserved — they differ whenever a routing fee applies. Budget is debited in whole cents rounded up, so a run of sub-cent calls still burns a cent each. For spend to date, read the Delegation, not a sum of responses.
**Delegations expire silently.** A long-running agent that worked yesterday and fails today with `0003` has very often just aged out — check `expiresAt` before assuming anything is broken.
| Code | Status | Meaning | Retry? |
| --- | --- | --- | --- |
| `BCK.ROUTER.0001` | 400 | Bad input: unsupported protocol, malformed/empty challenge, no fundable option, recipient outside the Delegation's scope, non-allowlisted asset, wrong-provider Delegation, missing `delegationId`. `details` names the specific problem. | No |
| `BCK.ROUTER.0002` | 409 | This `requestId` already minted a payment. The original `paymentId` is in the response — usually what you wanted. | No |
| `BCK.ROUTER.0003` | 402 | Delegation over cap, expired, exhausted, or revoked. | No — **stop** |
| `BCK.ROUTER.0004` | 404 | No Router payment with that id belongs to you. | No |
| `BCK.ROUTER.0005` | 409 | Payment not in a settleable state. Only `Issued` can be marked `Settled`. | No |
| `BCK.ROUTER.0006` | 500 | Transient failure building the payments summary. | **Yes** |
| `BCK.ROUTER.0007` | 429 | Too many concurrent routed requests in flight. | **Yes**, after backoff |
| `BCK.ROUTER.0008` | 403 | Legacy API key. Create a new one. | No |
| `BCK.ROUTER.0009` | 402 | Wallet doesn't hold enough of the asset on the target network. Nothing was signed. | No — **stop** |
| `BCK.ROUTER.0010` | 500 | Internal: the rail reported a charge amount the Router can't reserve against the cap. | No — **never blind-retry** |
| `BCK.ROUTER.0011` | 402 | Card rail: the charge needs cardholder 3-D Secure, and an agent has no browser to complete it. Nothing was charged and the seller got no usable credential. | No — **needs a human** |
| `BCK.ROUTER.0012` | 400 | The seller's 402 advertises an EIP-712 domain its own settlement token does not sign under, so the Router refuses to sign. Nothing signed, charged or reserved — an authorization under the wrong domain is unspendable anyway. Seller-side bug | No — **report it, pay elsewhere** |
| `BCK.ROUTER.0013` | 500 | Nevermined holds no EIP-712 signing domain for the token the funding filter selected — a gap in OUR canonical table, not the seller's bug and not your request. Nothing signed, charged or reserved | No — **report it to Nevermined** |
| `BCK.ROUTER.0014` | 409 | The target is a cataloged Nevermined service, whose upstream URL is deliberately hidden. The Router refuses to pay it by raw URL — mode A and a raw mode-B target both put the merchant's host on your wire, defeating the broker. The match is by HOST, so a co-hosted endpoint that is not itself listed is refused too — ask the vendor to list it, or contact Nevermined; hosts with no cataloged service are unaffected. | No — **use the slug**: `POST /router/route` with a `slug`, or `POST /router/svc/<catalog-slug>` (a `commerce` grant: `POST /router/commerce/route` with a `slug`). A refused **quote** is re-quoted by slug — `POST /router/quote`, or `/router/commerce/quote` for a grant — never paid |
| `BCK.ROUTER.0018` | 402 | Per-call `maxTotalCents` is below the fee-inclusive, whole-cent cap reserve. No charge or cap reserve, though signing may already have occurred; parse JSON-string `params` for `requiredTotalCents`. | No — raise the ceiling only if this call is intended; reuse the same `requestId` |
| `BCK.ROUTER.0019` | 400 | Streaming surfaces only (`/proxy` · `/svc`; `/route` returns the envelope status with `body: null`). A cataloged service returned a **non-retryable** status — a 4xx client error, or a rare 3xx the Router does not follow (a 402 re-challenge and a 429 are **not** this code). The upstream body is withheld (it can name the merchant host); this typed body preserves the **real** upstream status (on the HTTP status line and in JSON-string `params`). Build a valid request from the service's Catalog detail (`requestExample` / `responseFields`). | No — **fix the request first**, then retry with a fresh `requestId` |
| `BCK.ROUTER.0020` | 502 | Streaming surfaces only (`/proxy` · `/svc`; `/route` returns the envelope status with `body: null`). A cataloged service returned a server error (5xx) or rate-limited (429) — an upstream/transient condition, not your request. Body **and** headers are withheld (host oracle, including `Retry-After`); this typed body preserves the **real** status. The Router charges **no routing fee** for an undelivered call; whether the merchant leg itself charged is reported as `merchantSettlementObservedAt` (x402 only — `null` on a clean settlement and on both MPP rails, so `null` is not proof of no charge; read alongside `status`) on `GET /api/v1/router/payments`. | **Yes**, with backoff — reuse the same `requestId` only if no `X-Router-Payment-Id` came back; if one did, a payment is already recorded, so use a NEW id and reconcile via `GET /router/payments` |
| `BCK.ROUTER.0021` | 400 | The rail this service advertised carries its payment credential in a header you are **already using**. On the MPP rails that header is `Authorization`, which is also where your own merchant auth goes (`headers.Authorization` on `/route`, `X-Router-Upstream-Authorization` on `/proxy` · `/svc`). Rather than silently dropping yours on the paid hop, the Router refuses: **nothing was minted, no cap was reserved and no money moved**. JSON-string `params` names the contested header. | No — **name the header the service documents** for its credential: `credentialHeader` in the `/route` body, or the `X-Router-Credential-Header` request header on `/proxy` · `/svc` (a separate `Payment` header is the common one). If the service documents none, it wants the credential in `Authorization` itself and cannot also take your bearer there: drop your own auth for that call, or pay it over an x402 endpoint (whose credential travels in `PAYMENT-SIGNATURE`). Retrying unchanged fails identically |
| `BCK.ROUTER.0022` | 500 | Server-side service selection is not wired on this deployment (a configuration fault, not your request) — nothing was ranked or charged. Only reachable on a misconfigured deployment; never in a healthy environment. | **Yes**, later — it is a transient/config condition on our side; if it persists, quote `correlationId` when reporting it |
| `BCK.ROUTER.0023` | 502 | On an autoPay `POST /router/select` (or `/router/commerce/select`): the chosen service was paid but the downstream call did not complete cleanly **after** the payment was created, so the charge outcome is **indeterminate** — the merchant leg may or may not have settled. JSON-string `params` carries the `paymentId` when one was created. | No — do **not** retry with a fresh `requestId` (that could double-charge). Reconcile via `GET /api/v1/router/payments`, then reuse the **same** `requestId` to retry safely |
| `BCK.ROUTER.0024` | 413 | The request body exceeds the Router's size limit (about 5 MB). | No — reduce the request body before trying again |
| `BCK.ROUTER.0025` | 502 | The upstream reply was too large to deliver after a paid request. The payment outcome is indeterminate; it may have gone through. | No — this reply has no `X-Router-Payment-Id`; JSON-string `params` may carry `paymentId`. If absent, call `GET /api/v1/router/payments` with `delegationId` and `from` just before the call, then match `requestId` in the returned rows (newest 1000 maximum; there is no `requestId` filter). The row reads `status: Failed` because delivery failed, not because the merchant was uncharged. A non-null `merchantSettlementObservedAt` confirms x402 settlement; null does not prove no charge, including on MPP rails. Do not retry: the same `requestId` returns 409 `BCK.ROUTER.0002` with the original `paymentId` and cannot re-deliver the reply; a fresh id risks another charge. |
| `BCK.ROUTER.0026` | 415 | The Router cannot forward this request body. The streaming surfaces (`/router/svc/:slug`, `/router/proxy`) forward only JSON (`application/json`) or URL-encoded (`application/x-www-form-urlencoded`) bodies; any other type — `multipart/form-data` above all, but also `text/plain`, `application/octet-stream` or a vendor `+json` — and any body on GET/HEAD is refused. No payment was minted and no money moved. JSON-string `params` names the refused `contentType` (null when none was sent). | No — resend the body as JSON or a URL-encoded form the service accepts; a service that only takes a file upload cannot be paid through the Router yet, and retrying unchanged fails identically |
| `BCK.ROUTER.0027` | 413 | The request body is larger than the catalog endpoint accepts. The catalog records a `maxRequestBytes` per endpoint (on the service detail and in MCP `get_service`) — the Locus gateways (`*.mpp.paywithlocus.com`) take 8,000 bytes — and a slug-routed call (`/router/route`, `/router/quote`, `/router/svc/:slug`, `/router/proxy` with a slug) whose body is larger is refused before the service is contacted. No payment was minted and no money moved. JSON-string `params` carries `bodyBytes` and `maxRequestBytes`. | No — shrink the body below the limit or split the work, or pick a service that takes larger requests (`POST /router/select` with the same `body` skips endpoints whose limit is below it); retrying unchanged fails identically |
| `BCK.ROUTER.0028` | 503 | `POST /router/quote` could not price the call because a read it depends on failed (for example the settlement-token details on the payment network). Nothing is signed, minted or charged on the quote path. | **Yes**, with backoff — a quote never charges, so nothing needs unwinding. The same condition would also fail a payment, so do not route the call meanwhile; if it persists, quote `correlationId` |
| `BCK.ROUTER.0029` | 404 | The `quoteId` is invalid or belongs to another account. Nothing was signed, reserved or charged. | No — request a new quote and use its `quoteId`; do not retry the same invalid id |
| `BCK.ROUTER.0030` | 404 | No retained paid result for that paymentId under your account. A paid Router result is retained for 24h after the call ends, for the paying user only; it is not retained for a response that had already started streaming when the call completed, or for a payment never routed through `/router/route`, `/router/proxy` or `/router/svc`. The payment record itself is unaffected. | No — the result is gone (or was never retained); read the payment with `GET /api/v1/router/payments` |
| `BCK.ROUTER.0031` | 409 | The exact catalog slug in `filters.require` is unavailable, unhealthy, unpayable, excluded by this request, or cannot accept the body. The Router fails closed instead of substituting another service. | No — inspect `params.reason`; correct the slug or request, or deliberately remove `require` to allow fallback |
| `BCK.ROUTER.0032` | 410 | The `quoteId` expired before payment began. Nothing was signed, reserved or charged. | No — quote the same call again and decide against the new fee-inclusive total; do not retry the expired id |
| `BCK.ROUTER.0033` | 409 | The payment differs from the quote in its target, method, headers, body, credential header, delegation, rail, or exact fee-inclusive amount. Nothing was reserved or charged. | No — send the quoted call unchanged, or request a new quote for the changed call |
| `BCK.ROUTER.0034` | 502 | A paid request got no response from the service (timeout or connection failure) after the credential was sent — the service may already have redeemed the payment. `params` carries the `paymentId`. | No — reconcile via `GET /api/v1/router/payments/{paymentId}`; never retry with a fresh `requestId` (it could pay twice) |
| `BCK.ROUTER.0035` | 422 | The service's challenge asks for no payment (a zero amount — typically an auth-only "sign in with your wallet" challenge). Your request is not malformed; nothing was signed, reserved or charged. | No — use a service that charges for the call, or authenticate with the service directly |
| `BCK.ROUTER.0036` | 422 | Every catalog endpoint the call reaches is known to answer after the Router abandons a paid call (120 s), and the merchant charges on receipt — so paying would charge and deliver nothing. Refused before any payment (a retry of an already-paid `requestId` gets `0002` instead). `params.source` is `declared` (permanent) or `observed` (lifts at `params.liftsAt`). On a declared free-follow-up path it is raised only when the service answers `402`. | No — pick another endpoint or service; an `observed` refusal lifts at `liftsAt` |
| `BCK.OAUTH.0030` | 403 | This API key was OAuth-minted and may not create Delegations or use `/router/{payments,route,quote,select,proxy,svc}`. Use a plain account-owner key — or, for a `commerce` grant, `POST /router/commerce/route` to pay and `POST /router/commerce/quote` to price. | No |
| `BCK.OAUTH.0033` | 403 | A commerce route (`/commerce/route`, `/commerce/route/with-controls`, `/commerce/select`, `/commerce/quote`) needs a credential minted from a `commerce` grant pinned to a usable Delegation. A plain key uses the twin that names its own Delegation: `POST /router/route` to pay, `/router/select` to pick, `/router/quote` to price. A commerce credential that still sees it: re-run the authorization. | No |
| `BCK.OAUTH.0034` | 400 | `delegationId` sent on a commerce route; there it is derived from the grant. Remove it, or use the plain-key twins (`/router/route`, `/router/select`, `/router/quote`) to choose one. | No |
| `BCK.HTTP.412` | 412 | `{"error":"consent_required"}` on `POST /delegation/create` — the account's legal-document consent lapsed. The code is generic; branch on `body.error`. | No — **needs a human** |
**`0011` needs a human, not a retry.** The card issuer is demanding 3-D Secure and the Router has no browser to answer it. Nothing was charged. Do **not** loop: 3DS is often mandated per charge, so every attempt re-demands it and mints a fresh single-use card credential that is then abandoned. A later *human-driven* attempt may succeed — that is a decision, not a retry.
**`0010` is the one 500 you must never retry.** A payment credential **was already minted** before it failed — and because no payment record was written, your `requestId` will *not* suppress the retry. So a retry mints a **fresh** credential and then fails identically, because the cause is a deterministic defect in the rail's amount derivation, not a transient blip. Report it to the human.
Note `0006`, the retryable 500, is only ever raised by the payments *summary* read — never by a payment. **On the paying path `0007` and `0020` are the codes worth retrying (with backoff).** And seeing `0010` at all means a Nevermined-side regression: no rail emits a non-numeric amount today, so it is a bug report, not a condition to handle. (On the card rail the minted credential is a Stripe Shared Payment Token, left stranded with no revoke path until `min(challenge expiry, Delegation expiry, 89 days)`.)
Catalog errors: `BCK.CATALOG.0001` (404, no listed service with that slug — slugs are case-sensitive), `BCK.CATALOG.0002` (500, transient, retryable).
What the Router refuses outright — private/loopback/metadata targets, redirects, MPP `splits`, forged `X-Router-*` headers — and the relay limits: `references/errors.md`.
## Reference files
| You need… | Read |
| --- | --- |
| The Catalog feed, filter recipes, categories, the Catalog MCP, the ARD host document | `references/discovery.md` |
| Mode A vs mode B, `/proxy` streaming, merchant auth, full payloads | `references/paying.md` |
| Delegation fields, recipient scoping, wallet funding, networks | `references/bootstrap.md` |
| Every guardrail, every code, what is retryable and why | `references/errors.md` |
| Payment records, filters, CSV export, summary, reconciliation | `references/ledger.md` |
don't have the plugin yet? install it then click "run inline in claude" again.