Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 59 additions & 16 deletions .github/workflows/publish-skill-clawhub.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,72 @@
name: Publish skill to ClawHub

# Publishes the `nevermined-payments` skill to ClawHub under the `nevermined-io`
# publisher — https://clawhub.ai/skills/nevermined — the same org used for the
# OpenClaw plugin. Runs on demand and whenever the skill changes on `main`.
# Publishes this repo's skills to ClawHub under the `nevermined-io` publisher —
# the same org used for the OpenClaw plugin. Runs on demand and whenever a skill
# changes on `main`.
#
# skills/nevermined-payments → https://clawhub.ai/skills/nevermined
# skills/nevermined-router → https://clawhub.ai/skills/nevermined-router
#
# Required repository secret:
# CLAWHUB_TOKEN — a ClawHub API token belonging to a user who owns the
# `nevermined-io` publisher (create at https://clawhub.ai, `clawhub token`).
#
# Versioning: the CLI auto-increments from the latest published version (the
# push trigger is path-filtered, so it only runs when the skill actually
# changes). To publish an explicit version, add `--version <x.y.z>` below.
# Versioning: on REPUBLISH the CLI auto-increments from the latest published
# version. On a brand-new slug there is nothing to increment from, and the CLI
# does NOT read the version out of SKILL.md frontmatter — `clawhub skill publish
# --help` documents `--version` as "defaults to 1.0.0 or next patch". So a first
# publish would land as 1.0.0 and permanently contradict the `version:` in the
# skill's own frontmatter (which the rendered skill header shows to users).
# Hence the optional per-skill `version` in the matrix below: set it for the
# FIRST publish of a new slug, then delete it and let auto-increment take over.
#
# The push trigger is path-filtered on `skills/**`, so a change to ONE skill
# still runs the matrix leg for the other — that leg is a no-op, because
# republishing an unchanged version is handled as "already published" below.
#
# Adding a skill: add one entry to the matrix. Nothing else needs to change.

on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'skills/nevermined-payments/**'
- 'skills/**'
- '.github/workflows/publish-skill-clawhub.yml'

permissions:
contents: read

concurrency:
group: clawhub-publish-nevermined
cancel-in-progress: false

jobs:
publish:
name: Publish nevermined skill to ClawHub
name: Publish ${{ matrix.slug }} to ClawHub
runs-on: ubuntu-latest
# Per-skill so the two legs never queue behind each other, and a rerun of one
# skill can't cancel an in-flight publish of the other.
concurrency:
group: clawhub-publish-${{ matrix.slug }}
cancel-in-progress: false
strategy:
# One skill failing to publish must not stop the other from publishing.
fail-fast: false
matrix:
include:
- path: skills/nevermined-payments
slug: nevermined
name: Nevermined Payments
- path: skills/nevermined-router
slug: nevermined-router
name: Nevermined Router
# First publish of a new slug only — pins it to the frontmatter
# version instead of the CLI's 1.0.0 default. Delete this line once
# it has published once; auto-increment takes over from there.
version: "0.1.0"
env:
SKILL_PATH: skills/nevermined-payments
SKILL_PATH: ${{ matrix.path }}
OWNER: nevermined-io
SLUG: nevermined
SLUG: ${{ matrix.slug }}
SKILL_NAME: ${{ matrix.name }}
SKILL_VERSION: ${{ matrix.version }}
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -62,8 +94,16 @@ jobs:
SOURCE_COMMIT: ${{ github.sha }}
SOURCE_REF: ${{ github.ref }}
run: |
# `if`, not `[ -n … ] && …`. Both behave identically *here*: the default
# shell is `bash -e`, and errexit ignores a failing non-final element of
# an && list. But that list's own status is 1 when SKILL_VERSION is
# unset, so the && form fails the step the moment anyone moves it to the
# last line. `if` has no such edge — verified both ways.
VERSION_ARG=()
if [ -n "${SKILL_VERSION:-}" ]; then VERSION_ARG=(--version "$SKILL_VERSION"); fi
clawhub --no-input skill publish "$SKILL_PATH" \
--owner "$OWNER" --slug "$SLUG" --name "Nevermined Payments" \
--owner "$OWNER" --slug "$SLUG" --name "$SKILL_NAME" \
"${VERSION_ARG[@]}" \
--source-repo "$SOURCE_REPO" --source-commit "$SOURCE_COMMIT" \
--source-ref "$SOURCE_REF" --source-path "$SKILL_PATH" \
--dry-run --json
Expand All @@ -74,9 +114,12 @@ jobs:
SOURCE_COMMIT: ${{ github.sha }}
SOURCE_REF: ${{ github.ref }}
run: |
VERSION_ARG=()
if [ -n "${SKILL_VERSION:-}" ]; then VERSION_ARG=(--version "$SKILL_VERSION"); fi
set +e
OUT=$(clawhub --no-input skill publish "$SKILL_PATH" \
--owner "$OWNER" --slug "$SLUG" --name "Nevermined Payments" \
--owner "$OWNER" --slug "$SLUG" --name "$SKILL_NAME" \
"${VERSION_ARG[@]}" \
--source-repo "$SOURCE_REPO" --source-commit "$SOURCE_COMMIT" \
--source-ref "$SOURCE_REF" --source-path "$SKILL_PATH" \
--json 2>&1)
Expand Down
11 changes: 11 additions & 0 deletions development-guide/build-using-nvm-skill.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
---
title: "AI Coding Skill"
description: "Import Nevermined payment knowledge into your AI coding assistant and start building monetizable agents in minutes."

Check warning on line 3 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L3

Did you really mean 'Nevermined'?

Check warning on line 3 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L3

Did you really mean 'monetizable'?
icon: "wand-magic-sparkles"
---

Use the **Nevermined AI Skill** to give your coding assistant deep knowledge of the Nevermined Payments SDK, the x402 payment protocol, and integration patterns for Express.js, FastAPI, Strands agents, MCP servers, and Google A2A.

Check warning on line 7 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L7

Did you really mean 'Nevermined'?

Check warning on line 7 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L7

Did you really mean 'Nevermined'?

Instead of copy-pasting docs, you import the skill once and your AI assistant knows how to wire up payments for you.

## Two skills, opposite directions

There are two, and which one you want depends on which side of the payment you're on.

| Skill | Your agent is… | Covers |
| --- | --- | --- |
| **`nevermined-payments`** | **receiving** money, or buying a Nevermined plan | SDK integration, middleware, plans, credits — the rest of this page |

Check warning on line 17 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L17

Did you really mean 'Nevermined'?
| **`nevermined-router`** | **spending** money at external services | Discovering services in the catalog, creating a spending Delegation, paying any x402 or MPP endpoint through the [Nevermined Router](/products/router/overview) |

Every install method below works for either — swap `nevermined-payments` for `nevermined-router` in the paths. Installing both is fine; they don't overlap.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — "Every install method below works for either — swap nevermined-payments for nevermined-router in the paths" holds for 2 of the 8 install tabs on this page. The PR adds only skills/nevermined-router/**; there is no router counterpart in .cursor/rules/, .windsurf/rules/, .clinerules/ or .amazonq/rules/ (each contains exactly one file, the payments one), and the GitHub Copilot and Codex CLI tabs point at .github/copilot-instructions.md and AGENTS.md — single payments-only files with no skill-name segment to swap at all.

The failure is silent, not loud. Every curl on this page uses -o with no -f/--fail:

curl -o .cursor/rules/nevermined-router.mdc \
  https://raw.githubusercontent.com/nevermined-io/docs/main/.cursor/rules/nevermined-router.mdc

raw.githubusercontent returns 404, curl exits 0, and the literal string 404: Not Found is written into the rule file. Cursor then loads it as a valid rule for every .ts/.js/.py file and the assistant has zero Router knowledge — with no error anywhere to say so. Same for Windsurf, Cline and Amazon Q. Copilot and Codex users get payments guidance while believing they installed Router guidance.

The "Supported Tools at a Glance" table further down (lines 215-223) still lists payments-only paths for all 7 tools, so the page contradicts its own new claim two screens later.

Two ways out:

  • Scope the sentence to the methods that actually work (Claude Code and the manual/GitHub-blob tab), and say plainly that the per-IDE condensed rules are payments-only for now.
  • Or add the condensed router rule files.cursor/rules/nevermined-router.mdc, .windsurf/rules/nevermined-router.md, .clinerules/nevermined-router.md, .amazonq/rules/nevermined-router.md — which is what CLAUDE.md asks for: "IDE-specific files (.cursorrules, .cursor/rules/, .github/copilot-instructions.md) contain condensed versions of the skill and should be updated when core patterns change." Then update the glance table too.

Either way, adding --fail (or -fsSL) to the curl commands on this page would turn a corrupt-file-on-disk into an error the user can see.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in #269 — thank you, this was a real trap and the silent-failure framing is what made it worth fixing properly rather than just softening the sentence.

Reproduced your failure mode before touching anything:

curl -s  -o probe.mdc  .../.cursor/rules/nevermined-router.mdc   → exit=0   file: [404: Not Found]
curl -sf -o probe2.mdc (same URL)                                → exit=22  file: not created

I took your second option rather than the first — made the claim true instead of retracting it. Condensed router rules now exist for Cursor, Windsurf, Cline and Amazon Q, generated from one shared body so they can't drift. Your CLAUDE.md citation is what decided it.

Two things your review didn't mention that fell out of doing it:

  1. The Windsurf tab documents a 6,000-character limit per rule file. My first draft was 6,057 and would have been over. Trimmed to 5,292–5,457 across all four.
  2. .gitignore had a blanket /.cursor, so .cursor/rules/nevermined-router.mdc would have been silently left out of the commit — shipping the exact 404 we're fixing. It only surfaced because the file was missing from git status. nevermined-payments.mdc survives it only because it was tracked before the rule existed. Narrowed to /.cursor/* + !/.cursor/rules/ and verified both directions.

For Copilot and Codex CLI you're right that there's nothing to swap, so the page is now explicit that those two are payments-only and points at the full skill. The glance table gained a Router column. --fail added to all six commands.

Guard against the whole class: the PR verifies that every raw.githubusercontent URL on that page resolves to a real, committable file (10/10, git check-ignore included so an ignored file can't pass as present).


## What's Included

The skill provides your coding assistant with:
Expand Down Expand Up @@ -89,7 +100,7 @@
</Tab>

<Tab title="Windsurf">
### Windsurf (Codeium)

Check warning on line 103 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L103

Did you really mean 'Codeium'?

Copy the rule file to your project's Windsurf rules directory:

Expand Down Expand Up @@ -217,15 +228,15 @@

<CardGroup cols={2}>
<Card title="Add payments to Express.js" icon="node-js">
"Add Nevermined x402 payment protection to my Express.js API. I want to charge 1 credit per request on the `/ask` endpoint."

Check warning on line 231 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L231

Did you really mean 'Nevermined'?
</Card>

<Card title="Protect a FastAPI endpoint" icon="python">
"Integrate Nevermined payments into my FastAPI app. Use dynamic credits based on the token count in the response."

Check warning on line 235 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L235

Did you really mean 'Nevermined'?
</Card>

<Card title="Build a paid MCP server" icon="laptop">
"Create an MCP server with Nevermined payments. Register a weather tool that costs 5 credits per call."

Check warning on line 239 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L239

Did you really mean 'Nevermined'?
</Card>

<Card title="Add payments to a Strands agent" icon="robot">
Expand All @@ -235,7 +246,7 @@

## Alternative: Use the MCP Search Server

If you prefer real-time documentation search over a static skill, you can connect your coding assistant to the Nevermined MCP documentation server:

Check warning on line 249 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L249

Did you really mean 'Nevermined'?

```json
{
Expand Down Expand Up @@ -274,6 +285,6 @@
</Card>

<Card title="MCP Integration" icon="laptop" href="/integrations/mcp">
Build monetizable MCP servers

Check warning on line 288 in development-guide/build-using-nvm-skill.mdx

View check run for this annotation

Mintlify / Mintlify Validation (neverminedag) - vale-spellcheck

development-guide/build-using-nvm-skill.mdx#L288

Did you really mean 'monetizable'?
</Card>
</CardGroup>
248 changes: 248 additions & 0 deletions skills/nevermined-router/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
---
name: nevermined-router
version: "0.1.0"
lastUpdated: "2026-08-03"
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, 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.0 | **Last updated**: 2026-08-03 | **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/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.

## 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.

**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).

Full field list, recipient scoping, 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 custodial 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**.

**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 public and unauthenticated — no API key:

```bash
curl -s "$NVM_API_URL/api/v1/catalog/services?protocol=x402&search=web+search&offset=5"
```

```json
{
"total": 9, "page": 1, "offset": 5,
"services": [{
"slug": "superhighway",
"title": "Superhighway — Web Search for Agents",
"protocol": "x402",
"targetUrl": "https://superhighway.walls.sh/search",
"priceLabel": "$0.001",
"network": "Base",
"endpoints": [
{ "path": "/search", "method": "POST", "priceLabel": "$0.001", "description": "Web search" },
{ "path": "/news", "method": "POST", "priceLabel": "$0.001", "description": "Real-time news search" }
],
"tags": ["search", "web", "news", "markdown"]
}]
}
```

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. **`targetUrl` is the *default endpoint's complete URL*, not a base.** Above it is `…/search`, and `endpoints[0].path` is *also* `/search`. Concatenating gives you `/search/search`. Resolve against the origin instead:

```js
const url = endpoint ? new URL(endpoint.path, service.targetUrl).toString()
: service.targetUrl
// '/news' + 'https://superhighway.walls.sh/search' → 'https://superhighway.walls.sh/news' ✓
```

Filters, the categories endpoint, the per-slug lookup, and the crawlable ARD feed: `references/discovery.md`.

## ⑤ 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"'",
"url": "https://superhighway.walls.sh/search",
"method": "POST",
"body": { "query": "nevermined router" },
"requestId": "search-nevermined-router-v1"
}'
```

```json
{
"status": 200,
"body": { "…": "the paid resource" },
"paid": true,
"payment": {
"paymentId": "b1f9c2e4-…",
"settlement": { "amount": "1000", "asset": "USDC", "network": "base", "approxCents": "1" },
"txHash": "0xfc8af37b…",
"status": "Settled"
}
}
```

`status` and `body` are the merchant's own, unchanged. `paid: false` with no `payment` block means the resource was free — the Router relayed it and charged nothing.

<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 the original payment instead of buying twice; a fresh id buys twice, on purpose. 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.

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. **Only `0006` (500) and `0007` (429) are retryable.** Everything else is a decision, and retrying it unchanged produces the same answer. Back off on `0007`; it means you have too many routed calls in flight.

**Check the price before you commit.** `priceLabel` in the catalog is indicative; `settlement.approxCents` on the response is what you were actually charged. Budget is debited in whole cents rounded up, so a run of sub-cent calls still burns a cent each.

**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** |

Catalog errors: `BCK.CATALOG.0001` (404, no listed service with that slug — slugs are case-sensitive), `BCK.CATALOG.0002` (500, transient, retryable), `BCK.CATALOG.0003` (400, `protocol` filter must be one of `x402`, `mpp`, `rest`, `a2a`, `other`).

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 |
| --- | --- |
| Catalog filters, categories, per-slug lookup, the ARD feed | `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` |
Loading