feat(skills): add the nevermined-router agent skill - #267
Conversation
A machine-readable twin of the Router documentation, so an AI agent can discover an external service and buy from it autonomously with nothing but an API key. Companion to `nevermined-payments`, which covers the opposite direction (receiving payments, buying Nevermined plans). SKILL.md carries the six-step buy loop — API key → Delegation → funded wallet → discover → pay → account — plus the guardrails an autonomous buyer must respect. Five reference files cover discovery, paying, bootstrap, errors and the ledger. Everything is verified against the API source and probed against the live sandbox rather than paraphrased, including three things that cost real money if an agent gets them wrong: - `targetUrl` is the default endpoint's COMPLETE URL, not a base. For `superhighway` it is `…/search` while `endpoints[0].path` is also `/search`, so concatenating yields `/search/search`. Resolve with `new URL(endpoint.path, targetUrl)` instead. - `requestId` is an idempotency key, not a request counter. A fresh uuid4 per HTTP attempt — the default reflex — is how an agent double-spends. - Budget is debited in whole cents rounded up, so 1000 calls at $0.001 costs $10.00, not $1.00. The skill is explicit that a refusal is the system working: 0003 and 0009 are stop conditions, and widening or re-minting a Delegation to escape one defeats the whole mechanism. Also: - publish-skill-clawhub.yml becomes a matrix over both skills, with per-skill concurrency and fail-fast disabled so one failing publish can't block the other. Adding a skill is now one matrix entry. - build-using-nvm-skill.mdx explains which of the two skills you want. Refs nevermined-io/nvm-monorepo#2595, epic nevermined-io/nvm-monorepo#2268 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
aaitor
left a comment
There was a problem hiding this comment.
Approving. This is the most accurate agent-facing documentation I've reviewed in this repo — every factual claim I could check against origin/main source is exactly right, including the three "costs real money" gotchas the PR calls out. The skill is well-structured (SKILL.md + 5 focused references), the guardrail framing ("a refusal is the system working", never widen a Delegation, one requestId per purchase) is exactly what an autonomous buyer needs, and the workflow matrix change is clean. No HIGH or MEDIUM findings.
Verification I ran — I reviewed inline rather than dispatching the agent panel: for a docs PR the load-bearing lens is factual accuracy against the API source, and a security/db/tdd panel adds nothing to Markdown. So I checked the claims directly against nevermined-io/nvm-monorepo@origin/main (4c98d817):
- Error catalogue — 12/12 exact. All nine
BCK.ROUTER.0001–0009and threeBCK.CATALOG.0001–0003matchlibs/helpers/src/lib/output/api-errors.tson HTTP status and retryability:0001=400,0002=409,0003=402,0004=404,0005=409,0006=500 (retryable),0007=429 (retryable),0008=403,0009=402; catalog 404/500/400. The "only0006/0007are retryable" rule matches theretryableflags. - All 6 endpoints exist and are
@Public()inrouter.controller.ts:POST route,ALL proxy,POST payments,POST payments/:id/settled,GET payments,GET payments/summary. - "409 carries the original
paymentId" — confirmed inrouter.service.ts:126,314("Return the original paymentId so a mode-B retry can reconcile"). - "Budget debited in whole cents, rounded up" ($0.001×1000 = $10.00) — confirmed: both
atomicToCents(x402-exact.ts) andamountToCents(mpp-tempo.ts) use ceiling division(x + n − 1)/n, with the exact "sub-cent charge still reserves ≥1 cent" comment. requestId ≤ 256—@MaxLength(256)in bothcreate-payment.dto.tsandproxy-request.dto.ts. Delegationcreatefields —provider/currency/spendingLimitCents(@Min(1))/durationSecs(@Min(1)) required,allowedRecipients/maxTransactionsoptional. Matches the bootstrap table.- Env-naming gotcha clean — no
staging/staging_sandbox/.devleakage; onlyapi.sandbox.nevermined.app/api.live.nevermined.app(correct.app, per the prod-URL convention). - Workflow — matrix over both skills, per-leg
concurrency+fail-fast: false, and the publish step captures the real exit code (STATUS=$?, not a masked pipe) with an idempotent "already published" no-op. Correct. - Over-engineering pass: lean already, ship. The cross-file repetition (stop-conditions, the
requestIdrule) is deliberate point-of-need redundancy for agents that load one reference at a time, not debt. The 2-entry matrix is the DRY choice over duplicated jobs. Net removable: ~0. - CI: 2 checks passing; Mintlify preview 🟢.
One watch-item for merge (not blocking) — left inline: the versioning comment says the CLI "auto-increments from the latest published version," but nevermined-router is a brand-new slug with no prior version. The merge-commit dry-run leg will confirm the first publish resolves a version; if it doesn't, seed it with --version 0.1.0 on the router matrix entry once.
Addresses aaitor's review note on #267. He flagged that the versioning comment claims the CLI "auto-increments from the latest published version", which cannot hold for a brand-new slug, and asked me to confirm rather than assume. Confirmed against the CLI itself — `clawhub skill publish --help` documents `--version` as "defaults to 1.0.0 or next patch". So the first publish does not fail (his stated worry), but it also does not read the version out of SKILL.md frontmatter: it would land as 1.0.0 while the skill's own frontmatter and its rendered header say 0.1.0, and those would disagree permanently. 0.1.0 is the honest number — the payments skill is still 0.x, and this skill has one acceptance criterion (a real end-to-end paid call) not yet verified. So his suggested remedy is right, for a different reason than either of us gave. Adds an optional per-skill `version` to the matrix, set only on the router leg, to be deleted after the first publish. Threaded in with `if`, not `[ -n … ] && …`: both are equivalent in this position (errexit ignores a failing non-final element of an && list — I asserted the opposite first and my own test disproved it), but the && list's status is 1 when the version is unset, so it would fail the step if ever moved to the last line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Watch-item closed with evidence — the first publish resolved correctly. From the merge-commit run (30817857071), router leg: Both legs green — The seed line has done its job. Per the comment I left on it, the next change to this skill should drop |
eruizgar91
left a comment
There was a problem hiding this comment.
Reviewed the new nevermined-router skill (6 files, 1083 lines), the ClawHub matrix workflow, and the install-page edit through three lenses (defects / repository invariants / conventions + history), then verified every candidate finding adversarially.
Verdict: REQUEST_CHANGES — one HIGH. The install page now promises that every install method works for the new skill by swapping a path, but the router counterparts of the per-IDE rule files don't exist, and the page's own curl commands write the 404 body to disk without failing. Findings are inline.
Conventions were read from CLAUDE.md and AGENTS.md, both of which govern the changed paths.
Checked and deliberately discarded:
- The matrix workflow's "no-op" claim. Traced through the pinned
clawhub@^0.22CLI:cmdPublishfingerprints the files and short-circuits withstatus: "unchanged"(exit 0) before both auto-increment and the--dry-runbranch, so the untouched leg neither reddens the run nor bumps a version. The unguarded dry-run step and the guarded final step are both fine. Job-levelconcurrencywith${{ matrix.slug }}is valid. architecture.mdnot updated. The tracker is a dead letter — it self-reports "Last Updated: January 2026", and #264 (the entire 7-page Router section) and #266 (Exa) both skipped it. Not holding this PR to a rule the repo doesn't run.erc4337-only phrasing inpaying.md.products/router/rails-mpp.mdx:96says the card rail is feature-flagged off, sandbox-only, "don't build against it in production yet" — so the simplification is defensible.- The Exa paragraph at
SKILL.md:49. Real tension with #266 — that paragraph's only stated outcome is "tell the user it needs an account", with no off-ramp tonevermined-payments, whilesolutions/api-providers.mdx:13in this same branch calls Exa the live reference where "no human touched it". But the Router exclusion itself is correct (Exa isn't in the catalog, isn't routable) and the billing description is fair about Exa-the-vendor, so this landed below the bar for a blocking comment. Worth a sentence pointing at/integrations/exaif you're touching the file anyway. - The catalog
offset=5example, and the fact that the catalog surface has no docs-site counterpart yet (#264 deferreddiscover-services).
No lint/typecheck/build findings — those run separately.
| | **`nevermined-payments`** | **receiving** money, or buying a Nevermined plan | SDK integration, middleware, plans, credits — the rest of this page | | ||
| | **`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. |
There was a problem hiding this comment.
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 whatCLAUDE.mdasks 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.
There was a problem hiding this comment.
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:
- 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.
.gitignorehad a blanket/.cursor, so.cursor/rules/nevermined-router.mdcwould have been silently left out of the commit — shipping the exact 404 we're fixing. It only surfaced because the file was missing fromgit status.nevermined-payments.mdcsurvives 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).
| "protocol": "x402", | ||
| "resourceUrl": "https://agent.example/paid", | ||
| "requestId": "order-1234", | ||
| "target": { "x402Version": 1, "accepts": [ /* verbatim from the 402 */ ] } |
There was a problem hiding this comment.
LOW — The mode A worked example pairs a v1 request with a v2 response. Line 166 mints with "x402Version": 1, the response block at lines 184-185 comes back "x402Version": 2 with credential.name: "PAYMENT-SIGNATURE" — and the mapping table 14 lines later says v2 → PAYMENT-SIGNATURE, v1 → X-PAYMENT. Both blocks sit inside the same numbered step ("### 2 · Mint the credential"), so a reader takes them as one request/response pair and learns the wrong version→header mapping.
No runtime harm: line 203 already says "Read the name off the response rather than hardcoding it", and an agent that follows that attaches the right header regardless. This is a comprehension defect in a file that gets copied verbatim into agent context, in the one step where the header name decides whether an already-minted, already-reserved payment is accepted.
One-token fix: drop "x402Version": 1 from line 166 (it defaults to 2 per the table at line 174), or flip the response block to v1/X-PAYMENT.
There was a problem hiding this comment.
Fixed in #269. You're right, and I'd introduced it.
I took the second of your two options — flipped the response block to v1 rather than dropping "x402Version": 1 from the request — so the step reads as one coherent v1 round trip (x402Version: 1 → name: "X-PAYMENT"), and added a line right after saying a v2 target returns PAYMENT-SIGNATURE instead. That keeps the request block illustrating the non-default case, which the field table immediately above deliberately calls out ("defaults to 2; set 1 for x402-express") — dropping the field would have lost that.
Agreed on severity: no runtime harm, since line 203 already says to read the name off the response. But "comprehension defect in a file copied verbatim into agent context" is exactly the right bar for this file, and the step where the header name decides whether an already-minted, already-reserved payment is accepted is the wrong place to make a reader infer the mapping.
…269) * fix(skills): add router IDE rule files and stop silent 404 installs Follow-up to #267, addressing @eruizgar91's REQUEST_CHANGES review, which landed ~8 minutes after that PR merged. HIGH — the install page claimed "every install method below works for either, swap nevermined-payments for nevermined-router in the paths". That held for 2 of 8 tabs. There were no router counterparts under .cursor/rules/, .windsurf/rules/, .clinerules/ or .amazonq/rules/, and Copilot/Codex load a single file with no skill-name segment to swap at all. The failure was silent: every curl on the page used -o with no --fail, so a 404 exits 0 and writes the literal string "404: Not Found" into the rule file. Cursor then loads that as a valid rule and the assistant has zero Router knowledge, with nothing anywhere saying so. Reproduced before fixing: curl -o -> exit 0, file contains "404: Not Found"; curl --fail -> exit 22, no file. Rather than retract the sentence, made it true: - Adds condensed router rules for Cursor, Windsurf, Cline and Amazon Q, all generated from one shared body so they cannot drift, and all under the 6,000-char Windsurf limit the page documents (5,292-5,457; the first draft was 6,057 and had to be trimmed). - Scopes the claim honestly for Copilot and Codex CLI, which genuinely cannot be swapped, and points those users at the full skill. - Adds --fail to all six curl commands, and router download commands to the four tabs that support them. - Updates the "Supported Tools at a Glance" table, which still listed payments-only paths and contradicted the new claim two screens later. Root cause of the missing Cursor file, caught pre-commit: .gitignore had a blanket `/.cursor`, so the new rule would have been silently left out of the commit and shipped as the very 404 this PR fixes. nevermined-payments.mdc only survives it because it was tracked before the rule existed. Narrowed to `/.cursor/*` + `!/.cursor/rules/` so published rules track normally while local Cursor state stays ignored; verified both directions. LOW — the mode A worked example paired a v1 request with a v2 response, teaching the wrong version->header mapping in the one step where the header name decides whether an already-minted, already-reserved payment is accepted. Now a coherent v1 round trip, with a line noting what v2 returns instead. Also, per @aaitor on #267: drops the one-shot `version: "0.1.0"` seed now that the first publish has happened, so auto-increment takes over (leaving it would re-try 0.1.0 forever, hit the "already published" no-op, and freeze the skill). Bumps the skill to 0.1.1 to match. The SKILL_VERSION plumbing stays for the next new slug. And @eruizgar91's noted tension on the Exa paragraph: it now points at the out-of-band rail (/integrations/exa, a $7 card-delegation purchase of Exa API credits) instead of dead-ending at "tell the user it needs an account". Verified: every raw.githubusercontent URL on the install page resolves to a real, committable file (10/10); mintlify broken-links clean; workflow YAML parses with both matrix legs on auto-increment. Refs nevermined-io/nvm-monorepo#2595 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(skills): fold in @eruizgar91's discarded-but-correct review points He approved and explicitly rated these below the blocking bar. Two are genuine inaccuracies in prose I wrote, so they're worth fixing rather than shipping. 1. Codex CLI is NOT "nothing to swap". Verified: its tab already documents an `.agents/skills/` install, which takes a skill DIRECTORY. Only Copilot is truly single-file. Split the bullet, added the router `cp` line to that tab, and corrected the glance table from "No" to "Yes — via `.agents/skills/`; not via AGENTS.md". 2. `lastUpdated` now moves with the version (0.1.1 / 2026-08-04), matching how the sibling skill treats the pair. Also fixes the stale IDE-surface inventory in CLAUDE.md that he flagged as out-of-scope. It named a `.cursorrules` that does not exist and omitted `.windsurf/rules/`, `.clinerules/` and `.amazonq/rules/` entirely — the same rule I cited when justifying this PR, so leaving it wrong would mislead the next person the same way. Replaced with a verified table (both skills present in all four rule dirs; Copilot + AGENTS.md single-file), the `.gitignore` re-include rationale, and a pointer to keep the install page and its glance table in sync. Also corrects "7 reference files" — payments has 12, router has 5. Every number and path in that table was checked against the tree, not carried over. Deliberately NOT folded in, agreeing with his assessment: the condensed rule files stop at the Exa dead-end without the out-of-band off-ramp (an omission in a size-capped surface, and Windsurf is already at 5,240 of 6,000 chars), and the CLAUDE.md v2-headers rule governs the Payments SDK server surface rather than the Router paying a merchant whose version is the merchant's choice. mintlify broken-links: clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements nevermined-io/nvm-monorepo#2595. Part of epic nevermined-io/nvm-monorepo#2268 (Router go-live) and the direct enabler for the autonomous-buy demo, nevermined-io/nvm-monorepo#2596.
What this adds
skills/nevermined-router/— the machine-readable twin of the Router docs section, so an AI agentcan discover an external service and buy from it autonomously, given nothing but an API key.
It is the companion to
nevermined-payments, pointed the opposite way:nevermined-paymentsnevermined-routerHow it was written
Every fact is verified against the API source or probed against the live sandbox — not paraphrased
from prose. The catalog example is a real row (
superhighway) pulled from a running environment,not invented.
Three of those checks turned up things that cost real money if an agent gets them wrong, and
each is called out where an agent will actually hit it:
targetUrlis the default endpoint's complete URL, not a base. Forsuperhighwayit ishttps://superhighway.walls.sh/searchwhileendpoints[0].pathis also/search— so theobvious
targetUrl + endpoint.pathyields/search/search. Verified across all 9 rows on a liveenvironment: some
targetUrls are full endpoint URLs, some are bare origins, andnew URL(endpoint.path, targetUrl)is correct for both.requestIdis an idempotency key, not a request counter. A freshuuid4()per HTTP attempt —the default reflex, and what most examples of retry logic teach — is precisely how an agent
double-spends. The skill says to derive it from the work instead.
Budget is debited in whole cents, rounded up. So "1000 calls at $0.001 = $1.00" is wrong: it
is $10.00. Sub-cent calls each burn a full cent.
It also documents the mode A / mode B asymmetries that are easy to get backwards:
protocolisadvisory and auto-detected in mode B but required and authoritative in mode A;
requestIdis required in mode B and optional in mode A.
Guardrails
The skill is explicit that a refusal is the system working.
BCK.ROUTER.0003(over cap) and0009(wallet short) are stop conditions to report, not obstacles to route around; only0006and0007are retryable. It states plainly that widening a Delegation — or minting a second one toescape an exhausted one — defeats the entire mechanism, since that is the failure mode an
autonomous buyer is most likely to reason its way into.
Also in this PR
publish-skill-clawhub.ymlbecomes a matrix over both skills, publishingnevermined-routertoclawhub.ai/skills/nevermined-router. Per-skillconcurrencyso the legsnever queue behind each other, and
fail-fast: falseso one failing publish cannot block theother. The path filter widens to
skills/**, which means a change to one skill also runs theother's leg — a no-op, because the existing "already published" branch handles it. Adding a third
skill is now one matrix entry.
build-using-nvm-skill.mdxgains a short section explaining which of the two skills you want;the existing per-tool install steps work for either by swapping the directory name.
Testing
mintlify broken-links→ no broken links foundreferences/*.mdcross-links resolvehttps://nevermined.ai/docs/products/router/overview(linked from SKILL.md) → 200Note for reviewers
The skill deliberately says the Router cannot pay conventional SaaS APIs (Exa, Firecrawl,
Tavily…), because they never quote a per-request price on the wire. That is measured, not assumed:
across every
rest/otherservice in the curated catalog set, none returns a 402, none emits apayment header, and none serves a real x402 manifest — the ones that answer meaningfully return
401/403. Those rows are loaded unlisted for exactly this reason(nevermined-io/nvm-monorepo#2600).
🤖 Generated with Claude Code