⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
x402-skill-md
The toolkit for `skill.md` — the agent-facing contract file an x402 service ships at its
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
Not yet listed on ClawHub or SkillsMP
// README
What a skill.md is
The file an AI agent reads to learn how to use and pay for an HTTP service — in one fetch, with no SDK and no human. It is Markdown, so a model can read it directly; it has a defined section grammar, so a parser can check it.
OpenAPI already describes shapes. It does not describe what a call costs, in what asset, on which chains, to which address — and for an agent with a wallet and no account, that is the load-bearing information.
Use it three ways
As a CLI
npx x402-skill-md generate openapi.json -o skill.md
npx x402-skill-md validate skill.md
npx x402-skill-md rules
PASS score 100/100 (0 errors, 0 warnings, 0 infos)
service: x402-domains endpoints: 2 rails: evm + solana
No findings. This document is conformant.
validate exits 1 on errors, so it drops straight into CI:
- run: npx x402-skill-md validate skill.md --strict
As a library
npm i x402-skill-md
import { openapiToSkillMd, validateSkillMd, RULES } from "x402-skill-md";
const { skillMd, warnings } = openapiToSkillMd(
JSON.parse(await readFile("openapi.json", "utf8")),
{ baseUrl: "https://api.example.com", contact: "[email protected]" },
);
const report = validateSkillMd(skillMd);
report.valid; // true
report.service.dualRail; // true
report.findings; // [] — each finding carries rule, detail, fix, line
Zero runtime dependencies in the library path: the parser, validator and generator are plain TypeScript.
As a paid service
| Route | Price | What lands in the 200 body |
|---|---|---|
POST /generate | $0.01 | The generated skill.md, plus a validation report for it — one call gives you a file you can commit |
POST /validate | $0.002 | The full report: every finding with its rule id, location, what is wrong in this document, and the exact fix |
GET /rules | free | The whole rule catalogue as JSON |
GET /spec | free | The format specification |
GET / · /health · /skill.md · /.well-known/x402 · /openapi.json | free | Discovery |
GET /rules is free on purpose: you should be able to see what you are being judged against
before paying to be judged.
Why x402 for this
Validation is the definition of a spiky, per-artifact need — you check a skill.md when you
write one, then not again for a month. A subscription is absurd for that, and so is an API
key. But the library is also just free on npm, so the honest answer is: the paid service
exists for agents. An agent that generated an OpenAPI document and wants a skill.md for it
cannot npm install inside its own reasoning loop; it can make an HTTP request and pay a
cent. Humans should use the CLI.
The format in one paragraph
Markdown with a defined section grammar: one H1, a summary paragraph, a service card with a
base URL, a ## Payment section documenting both an EVM and a Solana rail, one
## `METHOD /path` — $price section per endpoint with a parameter table and a real JSON
response example, and a ## Errors section that mentions the 402.
Dual rail is mandatory, not optional
A conformant skill.md documents an EVM rail and a Solana rail (rules SM005/SM006, both
error). The reasoning:
- An agent's wallet lives on exactly one chain. A single-rail service is invisible to every agent on the other one, and it cannot learn that cheaply.
- The 402 challenge already carries an
acceptsarray. Two entries cost one extra object; the client picks. There is no protocol reason to publish one. - Discovery indexers filter on rails. A single-rail listing halves your reachable market for no gain.
The spec also requires the facilitator to be declared per rail, because facilitators are
chain-specific: the reference x402.org facilitator settles Base Sepolia only and will not
settle a Solana payment. A document that names one facilitator for both rails is telling
clients something false.
Rules
23 rules across three severities. valid is true when there are zero errors,
independent of score; the score (100 − 12/error − 4/warning − 1/info) is a quality signal.
| count | meaning | |
|---|---|---|
error | 10 | not conformant — an agent may fail to use the service |
warning | 10 | conformant but lossy — an agent has to guess |
info | 3 | a suggestion |
Every finding carries a fix, not just a verdict:
{
"rule": "SM006",
"severity": "error",
"title": "No Solana payment rail documented",
"detail": "There is no Payment section, so no Solana rail is documented.",
"fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay."
}
Full catalogue: npx x402-skill-md rules, GET /rules, or §9 of the spec.
Generating from OpenAPI
Pricing is read from, in order of preference:
x-x402: { price: "$0.001" }on the operation — the exact, declarative way- the
maxAmountRequiredin a 402 response example, converted from USDC base units options.defaultPrice- otherwise, an operation with no 402 response is treated as free
Anything guessed is reported in warnings. The generator never invents a price silently —
a wrong price in a published contract is worse than a missing one.
openapiToSkillMd(doc, {
baseUrl: "https://api.example.com", // overrides servers[0].url
contact: "[email protected]", // overrides info.contact.email
defaultPrice: "$0.001",
frontmatter: true, // emit the YAML block
rails: [ /* defaults to the suite's dual-rail pair */ ],
});
Quickstart (the service)
git clone https://github.com/nirholas/x402-skill-md.git
cd x402-skill-md
npm install
cp .env.example .env # already filled with working defaults
npm run dev # http://localhost:4029
curl -i -s -X POST localhost:4029/validate -H 'content-type: text/markdown' -d '# x'
# HTTP/1.1 402 Payment Required
# { "x402Version": 1, "accepts": [ {…base-sepolia…}, {…solana…} ] }
PRIVATE_KEY=0xyourTestnetKey npm run client
Dual-rail payment: Base or Solana
Every 402 lists two payment requirements. The client picks whichever chain it can sign on; the server settles either through that rail's facilitator and returns the same artifact.
| EVM rail | Solana rail | |
|---|---|---|
| network | base-sepolia (default) / base | solana (default) / solana-devnet |
| asset | USDC 0x036CbD…dCF7e (sepolia) | USDC mint EPjFWdd5…TDt1v |
| payTo | 0x40252CFDF8B20Ed757D61ff157719F33Ec332402 | WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW |
| facilitator | https://x402.org/facilitator | https://facilitator.payai.network |
Those are the suite's public receive addresses and the server runs with them out of the box.
Set PAY_TO_ADDRESS / SOLANA_PAY_TO_ADDRESS to be paid yourself. The Solana network fee is
sponsored by the facilitator's fee
// HOW IT'S BUILT
// FILES
47 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
skill.md9.1 KB88382244f732upstream ↗
--- skillMd: "1.0" name: x402-skill-md baseUrl: http://localhost:4029 manifest: /.well-known/x402 openapi: /openapi.json contact: [email protected] --- # x402-skill-md The toolkit for `skill.md` — the agent-facing contract file an x402 service ships at its root, so a model can learn what a service does, what each call costs, on which chains it can pay, and what comes back, in one fetch. This service generates a conformant `skill.md` from an OpenAPI document, and validates an existing one against 23 rules, returning a per-finding fix rather than a verdict. It also **defines the format**: the specification lives at `GET /spec` and the machine-readable rule catalogue at `GET /rules`, both free. - **Base URL:** `http://localhost:4029` (self-host) — replace with your deployment's origin. - **Manifest:** `GET /.well-known/x402` - **OpenAPI:** `GET /openapi.json` - **Specification:** `GET /spec` · <https://nirholas.github.io/x402-skill-md/spec> - **Also a library and CLI:** `npm i x402-skill-md` · `npx x402-skill-md generate openapi.json` - **Contact:** [email protected] ## Payment This service speaks **x402** (HTTP 402 Payment Required), protocol version 1, scheme `exact`. Every paid route answers an unpaid request with a 402 whose `accepts` array carries **two rails — USDC on Base (EVM) and USDC on Solana. Your client picks whichever it can sign.** | rail | network | asset | payTo | facilitator | |---|---|---|---|---| | evm | `base-sepolia` | USDC `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | `0x40252CFDF8B20Ed757D61ff157719F33Ec332402` | `https://x402.org/facilitator` | | solana | `solana` | USDC `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | `WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW` | `https://facilitator.payai.network` | Facilitators are per rail, not per service: the reference `x402.org` facilitator settles Base Sepolia only, so the Solana rail points at PayAI's. Override with `FACILITATOR_URL` and `SOLANA_FACILITATOR_URL`. ```json { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ { "scheme": "exact", "network": "base-sepolia", "maxAmountRequired": "10000", "resource": "http://localhost:4029/generate", "description": "Generate a conformant skill.md from a posted OpenAPI document", "mimeType": "application/json", "payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "maxTimeoutSeconds": 60, "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "extra": { "name": "USDC", "version": "2" } }, { "scheme": "exact", "network": "solana", "maxAmountRequired": "10000", "resource": "http://localhost:4029/generate", "description": "Generate a conformant skill.md from a posted OpenAPI document", "mimeType": "application/json", "payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW", "maxTimeoutSeconds": 60, "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "extra": { "name": "USD Coin", "decimals": 6, "feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4" } } ] } ``` - **Asset:** USDC (6 decimals) on both rails. `maxAmountRequired` is in base units — `"10000"` is $0.01. - **Invocation contract:** every accept also carries `outputSchema.input` (how to build the request — method, query/path params, JSON body fields) and `outputSchema.output` (the JSON Schema of the 200 body). Both are elided above for readability and both are generated from `openapi.json`, so an agent can plan and call the route from the challenge alone. - **How to pay:** any x402 client. `x402-fetch` + `viem` on the EVM rail; on Solana build the SPL `transferChecked` (the network fee is sponsored by `extra.feePayer`, so you need no SOL), sign it, and base64 the envelope into `X-PAYMENT`. - **Receipt:** paid responses carry `X-PAYMENT-RESPONSE` (base64 JSON with `rail`, `network`, `transaction`, `payer`, `amount`) and repeat it inline as `receipt`. Payment is per request. No account, no key, no minimum. ## `POST /generate` — $0.01 Turn an OpenAPI 3.x document into a conformant `skill.md`. The generated file comes back in this response body, together with a validation report for it, so one call gives you something you can commit. **Parameters** | name | in | type | required | notes | |---|---|---|---|---| | *(body)* | body | object | yes | The OpenAPI document itself, **or** `{ "openapi": <doc>, "options": {…} }` | | `options.baseUrl` | body | string | no | Overrides `servers[0].url` | | `options.contact` | body | string | no | Overrides `info.contact.email` | | `options.defaultPrice` | body | string | no | Price for paid operations with no declared amount, e.g. `"$0.001"` | | `options.frontmatter` | body | boolean | no | Emit the YAML frontmatter block. Default `true` | | `options.rails` | body | array | no | Rails to document. Defaults to the suite's dual-rail pair | Pricing is read from, in order: `x-x402.price` on the operation; the `maxAmountRequired` in a 402 response example, converted from USDC base units; `options.defaultPrice`; otherwise the operation is treated as free if it declares no 402. Anything guessed is reported in `warnings` — the generator never invents a price silently. **Response 200** ```json { "skillMdVersion": "1.0", "skillMd": "---\nskillMd: \"1.0\"\nname: x402-podcasts\n---\n\n# x402-podcasts\n…", "service": { "name": "x402-podcasts", "baseUrl": "http://localhost:4028", "endpoints": 6, "paid": 2 }, "warnings": [], "validation": { "valid": true, "score": 100, "summary": { "errors": 0, "warnings": 0, "infos": 0 }, "findings": [] }, "generatedAt": "2026-08-07T12:00:00.000Z", "receipt": { "success": true, "rail": "evm", "network": "base-sepolia", "transaction": "0x…" } } ``` ## `POST /validate` — $0.002 Check a `skill.md` against all 23 rules. The report names every finding, where it is, what is wrong *in this document*, and the exact remedy. **Parameters** | name | in | type | required | notes | |---|---|---|---|---| | *(body)* | body | string \| object | yes | The file as `text/markdown`, or JSON `{ "skillMd": "…" }` | **Response 200** ```json { "skillMdVersion": "1.0", "valid": false, "score": 88, "summary": { "errors": 1, "warnings": 0, "infos": 0 }, "service": { "name": "x402-github-bounty", "baseUrl": "http://localhost:4027", "contact": "[email protected]", "endpoints": [{ "method": "POST", "path": "/bounties", "price": "$0.01" }], "rails": { "evm": true, "solana": true }, "dualRail": true }, "findings": [ { "rule": "SM007", "severity": "error", "title": "No endpoints documented", "detail": "No level-2 heading matches the endpoint grammar.", "fix": "Document at least one endpoint as `## `METHOD /path` — $price` (or `— free`)." } ], "checkedAt": "2026-08-07T12:00:00.000Z", "receipt": { "success": true, "rail": "solana", "network": "solana", "transaction": "5Qm…" } } ``` `valid` is `true` when there are **zero errors**, independent of `score`. The score (100 minus 12 per error, 4 per warning, 1 per info) is a quality signal; `valid` is the gate. ## Free routes | route | returns | |---|---| | `GET /` | service card: rails, prices, format version, library and CLI hints | | `GET /health` | `{ ok, uptime, skillMdVersion }` | | `GET /rules` | the full rule catalogue as JSON — id, severity, title, fix | | `GET /spec` | the SKILL-MD format specification, `text/markdown` | | `GET /skill.md` | this file | | `GET /.well-known/x402` | machine-readable resource manifest | | `GET /openapi.json` | OpenAPI 3.1 | `GET /rules` is deliberately free: you should be able to see what you are being judged against before paying to be judged. ## The format, in one paragraph A `skill.md` is Markdown with a defined section grammar: one H1, a summary paragraph, a service card with a base URL, a `## Payment` section documenting **both an EVM and a Solana rail**, one `` ## `METHOD /path` — $price `` section per endpoint with a parameter table and a real JSON response example, and a `## Errors` section that mentions the 402. Dual rail is mandatory, not optional: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay at all. Full specification: `GET /spec`. ## Errors | status | body `error` | meaning | |---|---|---| | 400 | `BAD_REQUEST` | empty body, or neither markdown nor `{ skillMd }` / an OpenAPI document | | 400 | `NOT_AN_OPENAPI_DOCUMENT` | the posted JSON has no `paths` object | | 400 | `GENERATE_FAILED` | the document could not be converted; `message` says why | | 400 | `VALIDATE_FAILED` | the file could not be parsed at all | | 402 | — | payment required or rejected; body is the dual-rail challenge with `error` explaining why | Note that a *non-conformant* `skill.md` is a **200 with `valid: false`**, not an error — you paid for the report and the report is the artifact. Only an unparseable request is a 4xx. ## Data source Self-contained. There is no upstream API, no key, and no fixture mode: generation and validation are pure functions of what you post, so the same input always produces the same output. Nothing you send is stored.README.md12 KB22c46a9435efupstream ↗
<h1 align="center">x402-skill-md</h1> <p align="center"> <b>The <code>skill.md</code> toolkit — the format, a generator, a validator.</b><br> A library, a CLI, and an x402-paid service. Defines the agent-facing contract file the whole suite ships. </p> <p align="center"> <a href="LICENSE"><img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a> <a href="https://x402.org"><img alt="x402" src="https://img.shields.io/badge/protocol-x402-0052ff.svg"></a> <img alt="rails" src="https://img.shields.io/badge/USDC-Base%20%2B%20Solana-2775ca.svg"> <img alt="format" src="https://img.shields.io/badge/SKILL--MD-1.0-6b46c1.svg"> <a href="https://nirholas.github.io/x402-skill-md/"><img alt="docs" src="https://img.shields.io/badge/docs-Pages-24292f.svg"></a> </p> --- ## What a `skill.md` is The file an AI agent reads to learn how to **use and pay for** an HTTP service — in one fetch, with no SDK and no human. It is Markdown, so a model can read it directly; it has a defined section grammar, so a parser can check it. OpenAPI already describes shapes. It does not describe **what a call costs, in what asset, on which chains, to which address** — and for an agent with a wallet and no account, that is the load-bearing information. **[Read the specification →](docs/spec.md)** ## Use it three ways ### As a CLI ```bash npx x402-skill-md generate openapi.json -o skill.md npx x402-skill-md validate skill.md npx x402-skill-md rules ``` ``` PASS score 100/100 (0 errors, 0 warnings, 0 infos) service: x402-domains endpoints: 2 rails: evm + solana No findings. This document is conformant. ``` `validate` exits 1 on errors, so it drops straight into CI: ```yaml - run: npx x402-skill-md validate skill.md --strict ``` ### As a library ```bash npm i x402-skill-md ``` ```ts import { openapiToSkillMd, validateSkillMd, RULES } from "x402-skill-md"; const { skillMd, warnings } = openapiToSkillMd( JSON.parse(await readFile("openapi.json", "utf8")), { baseUrl: "https://api.example.com", contact: "[email protected]" }, ); const report = validateSkillMd(skillMd); report.valid; // true report.service.dualRail; // true report.findings; // [] — each finding carries rule, detail, fix, line ``` Zero runtime dependencies in the library path: the parser, validator and generator are plain TypeScript. ### As a paid service | Route | Price | What lands in the 200 body | |---|---|---| | `POST /generate` | **$0.01** | The generated `skill.md`, plus a validation report for it — one call gives you a file you can commit | | `POST /validate` | **$0.002** | The full report: every finding with its rule id, location, what is wrong *in this document*, and the exact fix | | `GET /rules` | free | The whole rule catalogue as JSON | | `GET /spec` | free | The format specification | | `GET /` · `/health` · `/skill.md` · `/.well-known/x402` · `/openapi.json` | free | Discovery | `GET /rules` is free on purpose: you should be able to see what you are being judged against before paying to be judged. ## Why x402 for this Validation is the definition of a spiky, per-artifact need — you check a `skill.md` when you write one, then not again for a month. A subscription is absurd for that, and so is an API key. But the library is also just free on npm, so the honest answer is: **the paid service exists for agents.** An agent that generated an OpenAPI document and wants a `skill.md` for it cannot `npm install` inside its own reasoning loop; it can make an HTTP request and pay a cent. Humans should use the CLI. ## The format in one paragraph Markdown with a defined section grammar: one H1, a summary paragraph, a service card with a base URL, a `## Payment` section documenting **both an EVM and a Solana rail**, one `` ## `METHOD /path` — $price `` section per endpoint with a parameter table and a real JSON response example, and a `## Errors` section that mentions the 402. ### Dual rail is mandatory, not optional A conformant `skill.md` documents an EVM rail **and** a Solana rail (rules SM005/SM006, both `error`). The reasoning: - An agent's wallet lives on exactly **one** chain. A single-rail service is invisible to every agent on the other one, and it cannot learn that cheaply. - The 402 challenge already carries an `accepts` **array**. Two entries cost one extra object; the client picks. There is no protocol reason to publish one. - Discovery indexers filter on rails. A single-rail listing halves your reachable market for no gain. The spec also requires the **facilitator to be declared per rail**, because facilitators are chain-specific: the reference `x402.org` facilitator settles Base Sepolia only and will not settle a Solana payment. A document that names one facilitator for both rails is telling clients something false. ## Rules 23 rules across three severities. `valid` is `true` when there are **zero errors**, independent of score; the score (100 − 12/error − 4/warning − 1/info) is a quality signal. | | count | meaning | |---|---|---| | `error` | 10 | not conformant — an agent may fail to use the service | | `warning` | 10 | conformant but lossy — an agent has to guess | | `info` | 3 | a suggestion | Every finding carries a fix, not just a verdict: ```json { "rule": "SM006", "severity": "error", "title": "No Solana payment rail documented", "detail": "There is no Payment section, so no Solana rail is documented.", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay." } ``` Full catalogue: `npx x402-skill-md rules`, `GET /rules`, or [§9 of the spec](docs/spec.md). ## Generating from OpenAPI Pricing is read from, in order of preference: 1. `x-x402: { price: "$0.001" }` on the operation — the exact, declarative way 2. the `maxAmountRequired` in a 402 response example, converted from USDC base units 3. `options.defaultPrice` 4. otherwise, an operation with no 402 response is treated as **free** Anything guessed is reported in `warnings`. The generator never invents a price silently — a wrong price in a published contract is worse than a missing one. ```ts openapiToSkillMd(doc, { baseUrl: "https://api.example.com", // overrides servers[0].url contact: "[email protected]", // overrides info.contact.email defaultPrice: "$0.001", frontmatter: true, // emit the YAML block rails: [ /* defaults to the suite's dual-rail pair */ ], }); ``` ## Quickstart (the service) ```bash git clone https://github.com/nirholas/x402-skill-md.git cd x402-skill-md npm install cp .env.example .env # already filled with working defaults npm run dev # http://localhost:4029 ``` ```bash curl -i -s -X POST localhost:4029/validate -H 'content-type: text/markdown' -d '# x' # HTTP/1.1 402 Payment Required # { "x402Version": 1, "accepts": [ {…base-sepolia…}, {…solana…} ] } PRIVATE_KEY=0xyourTestnetKey npm run client ``` ## Dual-rail payment: Base **or** Solana Every 402 lists **two** payment requirements. The client picks whichever chain it can sign on; the server settles either through that rail's facilitator and returns the same artifact. | | EVM rail | Solana rail | |---|---|---| | network | `base-sepolia` (default) / `base` | `solana` (default) / `solana-devnet` | | asset | USDC `0x036CbD…dCF7e` (sepolia) | USDC mint `EPjFWdd5…TDt1v` | | payTo | `0x40252CFDF8B20Ed757D61ff157719F33Ec332402` | `WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW` | | facilitator | `https://x402.org/facilitator` | `https://facilitator.payai.network` | Those are the suite's public receive addresses and the server runs with them out of the box. Set `PAY_TO_ADDRESS` / `SOLANA_PAY_TO_ADDRESS` to be paid yourself. The Solana network fee is sponsored by the facilitator's fee payer, so a payer needs USDC only — no SOL. ## Real backend / API keys **None.** Generation and validation are pure functions of what you post: no upstream API, no key, no fixture mode, and nothing you send is stored. The same input always produces the same output, which is also why the library and the service cannot disagree. ## For AI agents - **[`skill.md`](skill.md)** — this service's own contract file, and a worked example of the format. It scores 100/100 against its own validator, which is the least it could do. - **`GET /.well-known/x402`** — machine-readable manifest of both paid resources with input and output schemas and both rails. - **`GET /rules`** — the catalogue, free, so an agent can self-check before paying. - **MCP** — [`examples/mcp-tool.md`](examples/mcp-tool.md) exposes `generate_skill_md` and `validate_skill_md` as Model Context Protocol tools. - **Client** — [`examples/agent-client.ts`](examples/agent-client.ts) runs the full flow: rules → 402 → generate → validate a deliberately broken file to show the fixes. ## Docs Full site: **<https://nirholas.github.io/x402-skill-md/>** - **[Specification](docs/spec.md)** — the format itself, normative - [Tutorial](docs/tutorial.md) — CLI → library → paid service → CI - [API reference](docs/api.md) — every field of every response - [For agents](docs/agents.md) — discovery, payment, MCP, listing ## Support Questions, bugs, or a spec correction: **[email protected]** or open an issue. Part of the [x402 Suite](https://github.com/nirholas/x402-suite). ## License Apache-2.0 — see [LICENSE](LICENSE). ## Star History [](https://www.star-history.com/#nirholas/x402-skill-md&Date) <!-- three.ws:growth --> ## Support the project If x402-skill-md saves you time, **[star it on GitHub](https://github.com/nirholas/x402-skill-md)**. Stars are how other developers and AI agents find the repositories worth trusting, and they cost you one click. Know someone who would use it? [Post on X](https://twitter.com/intent/tweet?text=x402-skill-md%3A%20The%20skill.md%20toolkit%3A%20generate%20and%20validate%20agent-discoverable%20skill%20files%20(the%20agentres.dev&url=https%3A%2F%2Fgithub.com%2Fnirholas%2Fx402-skill-md) · [Share on Bluesky](https://bsky.app/intent/compose?text=x402-skill-md%3A%20The%20skill.md%20toolkit%3A%20generate%20and%20validate%20agent-discoverable%20skill%20files%20(the%20agentres.dev%20https%3A%2F%2Fgithub.com%2Fnirholas%2Fx402-skill-md) · [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fgithub.com%2Fnirholas%2Fx402-skill-md) · [Submit to Hacker News](https://news.ycombinator.com/submitlink?u=https%3A%2F%2Fgithub.com%2Fnirholas%2Fx402-skill-md&t=x402-skill-md%3A%20The%20skill.md%20toolkit%3A%20generate%20and%20validate%20agent-discoverable%20skill%20files%20(the%20agentres.dev) · [Share on Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fgithub.com%2Fnirholas%2Fx402-skill-md&title=x402-skill-md%3A%20The%20skill.md%20toolkit%3A%20generate%20and%20validate%20agent-discoverable%20skill%20files%20(the%20agentres.dev) ## Built for AI agents too Coding agents and LLM tooling can read this repo directly: [AGENTS.md](./AGENTS.md), [llms.txt](./llms.txt), [llms-full.txt](./llms-full.txt). Point an agent at `https://github.com/nirholas/x402-skill-md` and it has the context it needs. ## More from the same author - [All repositories by nirholas](https://github.com/nirholas/nirholas#readme): the full catalog, grouped by topic - [three.ws](https://three.ws): the platform for 3D AI agents with Solana wallets, a skill marketplace and x402 payments - Questions or ideas: [open an issue](https://github.com/nirholas/x402-skill-md/issues) or [start a discussion](https://github.com/nirholas/x402-skill-md/discussions) ## Contributors [](https://github.com/nirholas/x402-skill-md/graphs/contributors) <!-- /three.ws:growth -->.env.example2.9 KBb561b36ca3b4upstream ↗
# ── Payment rails (dual-rail x402: USDC on Base *and* Solana) ─────────────── # Every paid route advertises BOTH rails in its 402 challenge; the paying # client picks one. These are the suite's public receive addresses — the # server runs with them out of the box. Replace them to get paid yourself. # EVM (Base / Base Sepolia) USDC receive address PAY_TO_ADDRESS=0x40252CFDF8B20Ed757D61ff157719F33Ec332402 # Solana USDC receive address SOLANA_PAY_TO_ADDRESS=WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW # ── Networks ──────────────────────────────────────────────────────────────── # EVM rail: base-sepolia (testnet, default) or base (mainnet). #NETWORK=base-sepolia # Solana rail: mainnet-beta (default) or devnet. #SOLANA_NETWORK=mainnet-beta # Solana RPC used by the browser checkout helper. #SOLANA_RPC_URL=https://api.mainnet-beta.solana.com # ── Facilitator ───────────────────────────────────────────────────────────── # The x402 facilitator that verifies + settles payments on the EVM rail. #FACILITATOR_URL=https://x402.org/facilitator # Facilitator for the Solana rail. The reference x402.org facilitator only # settles base-sepolia, so the Solana rail defaults to PayAI's public # facilitator — no API key required. #SOLANA_FACILITATOR_URL=https://facilitator.payai.network # Facilitator sponsor account that pays the SOL network fee, so payers need # only USDC (no SOL). Override if your facilitator uses a different sponsor. #SOLANA_FEE_PAYER=2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4 # ── Server ────────────────────────────────────────────────────────────────── # HTTP port (default 4029). #PORT=4029 # Public base URL, used to build the absolute `resource` in 402 challenges. # Set this when running behind a proxy or on a public host. #PUBLIC_BASE_URL=https://skill-md.example.com # ── Upstream data ─────────────────────────────────────────────────────────── # None. Generation and validation are pure functions of what you post: no # upstream API, no key, no fixture mode, and nothing you send is stored. # ── Client examples only (never read by the server) ───────────────────────── # Wallet private key used by examples/agent-client.ts to PAY for calls. # Fund it with Base Sepolia USDC: https://faucet.circle.com #PRIVATE_KEY=0x... # Base URL the example client talks to. #BASE_URL=http://localhost:4029.github/FUNDING.yml106 Bfda9433edd19upstream ↗
# Adds a Sponsor button to the repo. Links to the product, not to any token. custom: - https://three.ws.github/ISSUE_TEMPLATE/bug.yml613 B3d2ff4483e49upstream ↗
name: Bug report description: Something does not work as documented labels: [bug] body: - type: input id: version attributes: label: x402-skill-md version or commit placeholder: v1.0.0 or a commit SHA validations: required: true - type: input id: env attributes: label: Runtime and OS placeholder: Node 22, macOS 15 - type: textarea id: what attributes: label: What happened, and what did you expect? validations: required: true - type: textarea id: repro attributes: label: Smallest reproduction render: shell.github/ISSUE_TEMPLATE/config.yml494 B284a4bbba4b7upstream ↗
blank_issues_enabled: false contact_links: - name: Ask a question or share what you built url: https://github.com/nirholas/x402-skill-md/discussions about: Open-ended questions belong in Discussions. - name: three.ws platform support url: https://three.ws about: Questions about the hosted platform rather than this repository. - name: Security report url: https://github.com/nirholas/x402-skill-md/security/advisories/new about: Report a vulnerability privately..github/ISSUE_TEMPLATE/feature.yml325 Bd7d4f2974d3bupstream ↗
name: Feature request description: Suggest a change or addition labels: [enhancement] body: - type: textarea id: problem attributes: label: What problem are you trying to solve? validations: required: true - type: textarea id: proposal attributes: label: What would you like to see?.github/PULL_REQUEST_TEMPLATE.md153 Bfcbde8255921upstream ↗
## What changed and why ## How it was checked - [ ] Tests pass locally (or the change is documentation only) - [ ] Docs updated where behavior changed.gitignore43 B8e2ff1069432upstream ↗
node_modules/ dist/ .env *.log data/*.jsonAGENTS.md2.1 KBb1e17184ea7cupstream ↗
# AGENTS.md Operating notes for AI coding agents (Claude Code, Codex, Cursor, Copilot and others) working in this repository. Everything here is derived from the files actually in the tree, so trust it over guesses, and update it when the facts change. ## What this repository is The skill.md toolkit: generate and validate agent-discoverable skill files (the agentres.dev pattern) from OpenAPI specs. Defines the SKILL-MD format used across the x402 Suite. - Homepage: https://nirholas.github.io/x402-skill-md/ - Source: https://github.com/nirholas/x402-skill-md - Primary language: TypeScript - License: Apache License 2.0 (see the LICENSE file) ## Repository layout - `docs/` - `examples/` - `public/` - `src/` - `README.md` - `LICENSE` - `package.json` ## Setup ```bash npm install ``` ## Commands | Task | Command | |---|---| | dev | `npm run dev` | | start | `npm start` | | build | `npm run build` | Run the test and lint commands above before you consider a change finished. If a command fails on code you did not touch, say so in your report instead of silently skipping it. ## Conventions - TypeScript runs in strict mode; do not loosen `tsconfig.json` to make an error go away. - `.env` files are gitignored; never commit credentials, and read configuration from environment variables. - Commit messages follow Conventional Commits (`type(scope): summary`), matching the existing history. - Read the surrounding code before adding to it, and match its naming, file organisation and error-handling style. - Keep `README.md` accurate: if a change alters behaviour, commands or configuration, update the docs in the same commit. - Do not leave TODO comments, stub functions, placeholder data or commented-out code behind. Finish what you start or leave it out. - Small, focused commits with a subject line that describes the change, not the act of committing. ## Where to raise things - Bugs and feature requests: https://github.com/nirholas/x402-skill-md/issues - Questions and ideas: https://github.com/nirholas/x402-skill-md/discussions - Security issues: report privately at https://github.com/nirholas/x402-skill-md/security/advisories/new, never in a public issue.CITATION.cff457 B3ae455d5209fupstream ↗
cff-version: 1.2.0 message: If you use this software, please cite it as below. title: "x402-skill-md" abstract: "The skill.md toolkit: generate and validate agent-discoverable skill files (the agentres.dev pattern) from OpenAPI specs. Defines the SKILL-MD format used across the x402 Suite." license: Apache-2.0 repository-code: "https://github.com/nirholas/x402-skill-md" url: "https://three.ws" authors: - name: three.ws website: "https://three.ws"CLAUDE.md11 B43c994c2d361upstream ↗
@AGENTS.mdCODE_OF_CONDUCT.md453 B4954bc6c1094upstream ↗
# Code of Conduct This project follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). In short: be respectful, assume good faith, keep feedback about the work, and do not harass anyone. Maintainers may remove comments, commits or contributions that break these rules and may block repeat offenders. Report conduct concerns privately to [email protected]. Reports are handled in confidence.CONTRIBUTING.md1.1 KBbddcf037cef6upstream ↗
# Contributing to x402-skill-md Thanks for helping. Small, focused changes land fastest. ## Ways to contribute - **Report a bug** with the [bug form](https://github.com/nirholas/x402-skill-md/issues/new?template=bug.yml). Include what you ran, what you expected and the smallest reproduction you can. - **Propose a feature** with the [feature form](https://github.com/nirholas/x402-skill-md/issues/new?template=feature.yml). Describe the problem first, then the change. - **Ask a question or share what you built** in [Discussions](https://github.com/nirholas/x402-skill-md/discussions). - **Fix something** with a pull request. ## Local setup ```bash git clone https://github.com/nirholas/x402-skill-md.git cd x402-skill-md npm install ``` ## Pull requests - One change per pull request, with a test where the change is testable. - Describe what changed and why in plain language. - Do not add dependencies for something a few lines of code can do. - Security-sensitive reports go through [SECURITY.md](SECURITY.md), not a public issue. By contributing you agree that your work is licensed under Apache-2.0, the license of this project.docs/.nojekyll0 Be69de29bb2d1upstream ↗
docs/agents.md8.5 KBa48d8142c3c0upstream ↗
# For AI agents This service is about the file *other* services publish for you. If you have ever fetched a `skill.md` and been able to use a paid API without reading its docs, this is the format that made that work — and this is the tooling that keeps those files honest. ## 1. Discover **`GET /rules`** — free. The whole rule catalogue: 23 rules, each with an id, a severity and a fix. Fetch this *before* writing or judging a `skill.md`; it turns validation from a diagnosis into a checklist. **`GET /spec`** — free. The [normative format specification](spec.md), as Markdown. Give it to a model and it can write a conformant file from scratch. **`GET /skill.md`** — this service's own contract file, which is also a worked example of the format. It scores 100/100 against its own validator. **`GET /.well-known/x402`** — the machine-readable manifest: ```jsonc { "x402Version": 1, "name": "x402-skill-md", "skillMdVersion": "1.0", "spec": "/spec", "rules": "/rules", "rails": [ { "rail": "evm", "network": "base-sepolia", "payTo": "0x40252CF…2402", "facilitator": "https://x402.org/facilitator" }, { "rail": "solana", "network": "solana", "payTo": "WwwuGbqH…T3WwW", "facilitator": "https://facilitator.payai.network" } ], "resources": [ { "resource": "POST /generate", "price": "$0.01", "inputSchema": {…}, "outputSchema": {…} }, { "resource": "POST /validate", "price": "$0.002", "inputSchema": {…}, "outputSchema": {…} } ], "freeResources": [ { "resource": "GET /rules", "price": "free" }, { "resource": "GET /spec", "price": "free" } ] } ``` ## 2. Pay Every paid route answers an unpaid request with 402 and a **dual-rail** `accepts` array: USDC on Base, USDC on Solana, same price, same artifact. Pick whichever chain your wallet lives on. **EVM rail:** ```ts import { wrapFetchWithPayment } from "x402-fetch"; const pay = wrapFetchWithPayment(fetch, wallet); // viem wallet client const res = await pay(`${BASE}/generate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ openapi: myOpenApiDoc }), }); const { skillMd, warnings, validation } = await res.json(); ``` **Solana rail:** ```ts import { prepareSolanaCheckout, encodeX402Payment } from "@three-ws/x402-payment-modal/server"; const accept = challenge.accepts.find((a) => a.network.startsWith("solana")); const { tx_base64 } = await prepareSolanaCheckout({ accept, buyer: pubkey }); const { x_payment } = encodeX402Payment({ accept, signedTxBase64: await wallet.signTransaction(tx_base64), resourceUrl: url, }); await fetch(url, { method: "POST", headers: { "X-PAYMENT": x_payment, … }, body }); ``` Those helpers only *build and encode* the payment client-side. Verification and settlement happen server-side through the rail's own facilitator — and the facilitators differ per rail, which is exactly the kind of thing the format makes services declare. `accept.extra.feePayer` sponsors the SOL network fee, so an agent holding only USDC can pay. ### Reading the contract before you pay Every entry in `accepts` carries an `outputSchema` with two halves, so an agent can judge whether a call is worth its price and then make it correctly — without fetching the OpenAPI document first: - **`outputSchema.input`** — `{ type: "http", method, queryParams?, pathParams?, bodyType?, bodyFields? }`. Each value is the JSON Schema for that query parameter, path segment or request-body field. - **`outputSchema.output`** — the JSON Schema of the 200 body you receive once payment settles. Both halves are generated from `openapi.json`, so the runtime challenge and the published spec cannot drift apart. Both rails advertise the identical contract: which wallet you pay with never changes what the endpoint takes or returns. You can probe a paid route safely: the paywall answers before any validation or existence check, so an unpaid request with a synthetic id or an empty body still returns the full challenge rather than a 404 or a 400. Read the price and the contract first, decide, then pay. ### Protocol version This service speaks **x402 v1** (`x402Version: 1`) — the version every client shipped in this repo, and in the examples above, is written against. v2 relocates the invocation contract to `extensions.bazaar.schema` and identifies networks with CAIP-2 ids; agentcash prefers it, and moving is a planned upgrade once the clients here can speak both. Until then, read `accepts[]` and ignore `extensions`. ## 3. What you get The 200 body **is** the purchase: - `POST /generate` → the `skill.md` text, plus a validation report for it - `POST /validate` → the report: findings, fixes, score, and what the validator understood about the service Plus `X-PAYMENT-RESPONSE`, a base64 receipt naming the rail, network and transaction. ### Rules worth encoding in your agent - **`valid: false` is a 200, not an error.** You bought the report. Read `findings`, fix the file, do not retry the call. This is the single most common agent mistake here. - **Fetch `/rules` first — it is free.** Most non-conformance is avoidable rather than diagnosable. Paying to be told you forgot a Payment section is a waste of a cent. - **`valid` is the gate, `score` is a signal.** `valid` is true at zero errors regardless of score. Do not block on a score threshold. - **Read `fix`, not just `title`.** Every finding carries the exact remedy. `detail` says what is wrong in *this* document; `fix` says what a conformant one does instead. - **Check `warnings` after generating.** That is where the generator admits what the OpenAPI document did not carry — a missing summary, an undeterminable price. A generated file is a good 80%; the warnings tell you which 20% you have to write. - **Never publish a generated file unchecked.** `generate` returns `validation` precisely so you do not have to pay twice to find out. - **Prefer the library if you can.** `npm i x402-skill-md` does all of this for free, offline, deterministically. The paid routes exist because an agent cannot always install a package mid-task. ## 4. Reading someone else's skill.md The reverse direction, and the reason the format exists. When you find a service's `skill.md`, the validator's `service` block tells you in one call whether it is usable: ```ts const report = validateSkillMd(await (await fetch("https://svc.example/skill.md")).text()); report.service.dualRail; // can I pay at all, on either chain? report.service.rails.solana; // specifically: my chain? report.service.endpoints; // [{ method, path, price }] — what does it cost? report.valid; // is this file trustworthy enough to act on? ``` A file that fails SM006 tells you the service takes EVM only — which, if your wallet is on Solana, means stop here rather than discovering it after a round trip. ## 5. MCP integration [`examples/mcp-tool.md`](https://github.com/nirholas/x402-skill-md/blob/main/examples/mcp-tool.md) is a complete Model Context Protocol server exposing `skill_md_rules` (free), `generate_skill_md` and `validate_skill_md`. The wallet lives in the MCP process, so its balance is the agent's spending cap. ```json { "mcpServers": { "x402-skill-md": { "command": "npx", "args": ["tsx", "/path/to/mcp-skill-md.ts"], "env": { "PRIVATE_KEY": "0x…", "X402_SKILL_MD_URL": "https://your-host" } } } } ``` Expose the free `skill_md_rules` tool too. A model that has read the rules writes a better file than one that pays to be corrected. ## 6. Getting listed Deploy publicly, then submit the origin to the x402 discovery surfaces. Each reads `/.well-known/x402`: | where | what it does | how | |---|---|---| | [x402scan.com](https://x402scan.com) | indexes live x402 endpoints and their settlement volume | submit your origin; it crawls `/.well-known/x402` | | **x402 Bazaar** | the protocol's own resource directory, queried by agents at runtime | register through the facilitator's `list` API | | [agentic.market](https://agentic.market) | marketplace of agent-payable services | submit the origin plus your `skill.md` URL | | [x402-skill-registry](https://github.com/nirholas/x402-skill-registry) | the suite's own searchable registry of paid agent skills | `POST /register` with a signed listing | Before submitting anything, validate your own `skill.md`: ```bash npx x402-skill-md validate skill.md --strict ``` An indexer that finds a non-conformant file will list you badly, or not at all — and an agent that finds one will not know whether it can pay you. Questions or a spec correction: **[email protected]**.docs/api.md11 KBc58b2c988c65upstream ↗
# API reference Base URL: `http://localhost:4029` when self-hosting. Machine-readable equivalents: [`openapi.json`](https://github.com/nirholas/x402-skill-md/blob/main/openapi.json) and [`/.well-known/x402`](https://github.com/nirholas/x402-skill-md/blob/main/public/.well-known/x402). The format itself is specified in [spec.md](spec.md). Prices are USDC. Every paid route accepts **both** rails — Base (EVM) and Solana — and returns the purchased artifact in the 200 body. Everything here is also available for free as a library and CLI: `npm i x402-skill-md`, `npx x402-skill-md`. The HTTP service exists for agents that cannot install anything. --- ## `POST /generate` **Price:** $0.01 · **Returns:** the generated `skill.md` plus a validation report for it ### Request body Either the OpenAPI document itself, or a wrapper: ```json { "openapi": { "openapi": "3.1.0", "info": { … }, "paths": { … } }, "options": { "baseUrl": "https://api.example.com", "contact": "[email protected]" } } ``` | option | type | default | effect | |---|---|---|---| | `baseUrl` | string | `servers[0].url`, else `http://localhost:4021` | the base URL written into the document | | `contact` | string | `info.contact.email` / `info.contact.url` | the contact line | | `defaultPrice` | string | — | price for paid operations with no determinable amount | | `frontmatter` | boolean | `true` | emit the YAML frontmatter block | | `rails` | array | the suite's dual-rail pair | rails to document; see [spec §5.3](spec.md) | | `manifestPath` | string | `/.well-known/x402` | manifest path in the service card | | `openapiPath` | string | `/openapi.json` | OpenAPI path in the service card | ### How pricing is determined In order of preference: 1. `x-x402.price` (or `x-402.price`) on the operation — `"$0.001"` 2. the `maxAmountRequired` in a 402 response **example**, converted from USDC base units 3. `options.defaultPrice` 4. otherwise: an operation with **no 402 response** is treated as `free` Every guess produces an entry in `warnings`. The generator never invents a price silently. ### Response 200 ```json { "skillMdVersion": "1.0", "skillMd": "---\nskillMd: \"1.0\"\nname: x402-podcasts\n---\n\n# x402-podcasts\n…", "service": { "name": "x402-podcasts", "baseUrl": "http://localhost:4028", "endpoints": 6, "paid": 2 }, "warnings": [], "validation": { "valid": true, "score": 100, "summary": { "errors": 0, "warnings": 0, "infos": 0 }, "findings": [] }, "generatedAt": "2026-08-07T12:00:00.000Z", "receipt": { "success": true, "rail": "evm", "network": "base-sepolia", "transaction": "0x…" } } ``` | field | meaning | |---|---| | `skillMd` | the file, ready to write to disk and commit | | `service.endpoints` / `service.paid` | how many operations were found, and how many are paid | | `warnings` | anything guessed or missing from the source document | | `validation` | the generator's own output run through the validator — a generated file should be conformant, and if it is not, the OpenAPI was thin | ### Errors | status | `error` | cause | |---|---|---| | 400 | `BAD_REQUEST` | body is not a JSON object | | 400 | `NOT_AN_OPENAPI_DOCUMENT` | the posted JSON has no `paths` object | | 400 | `GENERATE_FAILED` | conversion threw; `message` says why | | 402 | — | payment required or rejected | --- ## `POST /validate` **Price:** $0.002 · **Returns:** the full validation report ### Request body Either raw markdown with `content-type: text/markdown` (or `text/plain`), or JSON: ```json { "skillMd": "# my-service\n\nDoes a thing.\n\n## Payment\n…" } ``` Limit 2 MB. ### Response 200 **A non-conformant document is a 200, not an error.** You paid for the report, and the report is the artifact. Only an unparseable *request* is a 4xx. ```json { "skillMdVersion": "1.0", "valid": false, "score": 0, "summary": { "errors": 8, "warnings": 3, "infos": 2 }, "service": { "name": "broken-service", "baseUrl": null, "contact": null, "endpoints": [], "rails": { "evm": false, "solana": false }, "dualRail": false }, "findings": [ { "rule": "SM002", "severity": "error", "title": "Missing summary paragraph", "detail": "The text under the title is only 10 characters — too short to tell an agent what this service does.", "fix": "Put one prose paragraph directly under the H1 saying what the service does and what an agent gets from it.", "line": 1 }, { "rule": "SM006", "severity": "error", "title": "No Solana payment rail documented", "detail": "There is no Payment section, so no Solana rail is documented.", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay." } ], "checkedAt": "2026-08-07T12:00:00.000Z", "receipt": { "success": true, "rail": "solana", "network": "solana", "transaction": "5Qm…" } } ``` ### Fields | field | type | meaning | |---|---|---| | `valid` | boolean | **zero errors.** Independent of `score` — this is the gate | | `score` | 0–100 | 100 − 12 per error − 4 per warning − 1 per info, floored at 0. A quality signal | | `summary` | object | counts by severity | | `service.name` | string \| null | the H1 | | `service.baseUrl` | string \| null | the URL found on a "Base URL" line | | `service.contact` | string \| null | the first email in the document | | `service.endpoints` | array | `{ method, path, price }` for every conformant endpoint heading | | `service.rails` | `{ evm, solana }` | whether each rail was found in the Payment section | | `service.dualRail` | boolean | both — the thing rules SM005/SM006 exist to enforce | | `findings` | `Finding[]` | see below | | `checkedAt` | ISO 8601 | when the check ran | #### `Finding` | field | type | meaning | |---|---|---| | `rule` | string | stable rule id, e.g. `SM006`. Never reused, even if a rule is retired | | `severity` | `error` \| `warning` \| `info` | see [spec §9](spec.md) | | `title` | string | the rule's name | | `detail` | string | what is wrong **in this document** | | `fix` | string | the remedy, verbatim from the catalogue | | `line` | integer | 1-based, when the problem has a location | ### Errors | status | `error` | cause | |---|---|---| | 400 | `BAD_REQUEST` | empty body, or neither markdown nor `{ skillMd }` | | 400 | `VALIDATE_FAILED` | the file could not be parsed at all | | 402 | — | payment required or rejected | --- ## `GET /rules` — free The whole catalogue. Free on purpose: you should be able to see what you are being judged against before paying to be judged, and most problems are cheaper to avoid than to diagnose. ```json { "skillMdVersion": "1.0", "count": 23, "rules": [ { "id": "SM006", "severity": "error", "title": "No Solana payment rail documented", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section…" } ] } ``` Same data as `npx x402-skill-md rules --json`. ## `GET /spec` — free The [format specification](spec.md), as `text/markdown`. ## Other free routes | route | returns | |---|---| | `GET /` | service card: rails, prices, format version, library and CLI hints | | `GET /health` | `{ ok: true, uptime: <seconds>, skillMdVersion }` | | `GET /skill.md` | this service's own contract file — a worked example of the format | | `GET /.well-known/x402` | resource manifest with prices, rails and schemas | | `GET /openapi.json` | OpenAPI 3.1 | --- ## Library API The HTTP routes are thin wrappers over exported functions: ```ts import { openapiToSkillMd, // POST /generate validateSkillMd, // POST /validate parseSkillMd, // the structure behind the report formatReport, // the CLI's human rendering RULES, // GET /rules DEFAULT_RAILS, SKILL_MD_VERSION, } from "x402-skill-md"; ``` | export | signature | |---|---| | `openapiToSkillMd` | `(doc, options?) => { skillMd, service, warnings }` | | `validateSkillMd` | `(raw: string) => ValidationReport` | | `validateParsed` | `(doc: ParsedSkill) => ValidationReport` | | `parseSkillMd` | `(raw: string) => ParsedSkill` | | `findSection` | `(doc, title) => ParsedSection \| undefined` | | `formatReport` | `(report) => string` | `ParsedSkill` carries `frontmatter`, `title`, `intro`, `sections`, `endpoints`, `malformedEndpointHeadings` and `codeBlocks` — enough to build your own tooling without re-parsing Markdown. --- ## Payment ### The 402 body ```jsonc { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ /* one PaymentRequirements per rail */ ] } ``` `PaymentRequirements`: | field | example | notes | |---|---|---| | `scheme` | `"exact"` | the only scheme accepted | | `network` | `"base-sepolia"` / `"solana"` | switched by `NETWORK` and `SOLANA_NETWORK` | | `maxAmountRequired` | `"10000"` | base units; USDC has 6 decimals, so this is $0.01 | | `resource` | `"http://localhost:4029/generate"` | absolute URL being purchased | | `description` | `"Generate a conformant skill.md…"` | shown by wallets and the checkout modal | | `mimeType` | `"application/json"` | what the 200 will be | | `payTo` | `0x40252CF…` / `WwwuGbqH…` | receive address for that rail | | `maxTimeoutSeconds` | `60` | how long the authorisation stays valid | | `asset` | USDC address / SPL mint | the token you pay in | | `extra` | `{ name, version }` / `{ name, decimals, feePayer }` | EIP-712 domain on EVM; the fee sponsor on Solana | Facilitators are **per rail** — `FACILITATOR_URL` for EVM (default `https://x402.org/facilitator`, which settles Base Sepolia only) and `SOLANA_FACILITATOR_URL` for Solana (default `https://facilitator.payai.network`). ### The receipt Paid responses set `X-PAYMENT-RESPONSE` to base64 JSON and repeat it inline as `receipt`: ```json { "success": true, "rail": "evm", "network": "base-sepolia", "transaction": "0xabc…", "payer": "0xYourWallet", "amount": "10000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "resource": "http://localhost:4029/generate" } ``` ### 402 reasons | `error` | meaning | |---|---| | `X-PAYMENT header is required` | first, unpaid attempt — normal | | `invalid X-PAYMENT header: …` | not base64, or not a valid x402 payload | | `unsupported rail: this endpoint does not accept exact on <network>` | you signed on a rail this server does not take | | `payment rejected: <reason>` | the facilitator's `invalidReason` | | `settlement failed: <reason>` | verified but could not be broadcast | A facilitator outage returns `502 facilitator_unreachable` or `502 settlement_error` rather than a 402, so a retry loop cannot mistake an outage for a price.docs/favicon.svg423 B61d08fcdcc6fupstream ↗
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="x402"> <defs> <linearGradient id="g" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#0052ff"/> <stop offset="1" stop-color="#14a37f"/> </linearGradient> </defs> <rect width="100" height="100" rx="22" fill="url(#g)"/> <path d="M56 16 L74 16 L74 84 L56 84 L56 68 L20 68 L20 52 Z" fill="#fff"/> </svg>docs/index.html14 KB09596613e04bupstream ↗
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>x402-skill-md — the skill.md format, generator and validator</title> <meta name="description" content="The SKILL-MD format specification, plus a library, CLI and x402-paid service to generate agent-discoverable skill.md files from OpenAPI and validate them against 23 rules."> <style> :root{ --accent:#0052ff;--accent-2:#14f195;--bg:#ffffff;--fg:#0a0c10;--muted:#57606a; --card:#f6f8fa;--border:#d0d7de;--code-bg:#0d1117;--code-fg:#e6edf3; } @media (prefers-color-scheme:dark){ :root{--bg:#0d1117;--fg:#e6edf3;--muted:#9198a1;--card:#161b22;--border:#30363d} } *{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--fg);line-height:1.6;-webkit-text-size-adjust:100%} .wrap{max-width:900px;margin:0 auto;padding:0 20px} header{padding:72px 0 40px;text-align:center} .badge{display:inline-block;font-size:13px;font-weight:600;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:2px 12px;margin-bottom:16px} h1{font-size:44px;letter-spacing:-1px;margin-bottom:12px} @media (max-width:560px){h1{font-size:32px}} .tagline{font-size:19px;color:var(--muted);max-width:660px;margin:0 auto 24px} .install{display:inline-block;max-width:100%;overflow-x:auto;background:var(--code-bg);color:var(--code-fg);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:14px;padding:10px 18px;border-radius:8px;margin-bottom:24px;white-space:nowrap} .btns a{display:inline-block;margin:4px 6px;padding:10px 20px;border-radius:8px;font-weight:600;text-decoration:none;font-size:15px} .btn-primary{background:var(--accent);color:#fff} .btn-ghost{border:1px solid var(--border);color:var(--fg)} section{padding:32px 0} h2{font-size:26px;margin-bottom:14px;letter-spacing:-.5px} h3{font-size:18px;margin:20px 0 8px} p{margin-bottom:12px} ul{margin:0 0 12px 22px} li{margin-bottom:6px} .tablewrap{overflow-x:auto} table{width:100%;border-collapse:collapse;font-size:15px;min-width:520px} th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:top} th{color:var(--muted);font-size:13px;text-transform:uppercase;letter-spacing:.05em} td code,p code,li code,th code{background:var(--card);border:1px solid var(--border);border-radius:4px;padding:1px 6px;font-size:13px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace} .price{color:var(--accent);font-weight:700;white-space:nowrap} pre{background:var(--code-bg);color:var(--code-fg);border-radius:10px;padding:18px;overflow-x:auto;font-size:13.5px;line-height:1.55;font-family:ui-monospace,SFMono-Regular,Menlo,monospace} .rails{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px} .rail{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 18px} .rail b{display:block;font-size:16px;margin-bottom:6px} .rail .k{color:var(--muted);font-size:13px} .rail code{font-size:12px;word-break:break-all} figure{margin:0} figcaption{color:var(--muted);font-size:13px;text-align:center;margin-top:8px} svg{max-width:100%;height:auto;display:block;margin:0 auto} footer{padding:40px 0;color:var(--muted);font-size:14px;text-align:center;border-top:1px solid var(--border);margin-top:48px} footer a{color:var(--accent);text-decoration:none} a{color:var(--accent)} </style> </head> <body> <header class="wrap"> <div class="badge">x402 · USDC on Base + Solana</div> <h1>x402-skill-md</h1> <p class="tagline">The <code>skill.md</code> toolkit: the <b>format specification</b>, a generator, and a validator that returns a fix per finding. Ships as a library, a CLI, and an x402-paid service.</p> <div class="install">npx x402-skill-md generate openapi.json -o skill.md</div> <div class="btns"> <a class="btn-primary" href="https://github.com/nirholas/x402-skill-md">GitHub</a> <a class="btn-ghost" href="tutorial.md">Tutorial</a> <a class="btn-ghost" href="api.md">API Reference</a> <a class="btn-ghost" href="agents.md">For AI Agents</a> <a class="btn-ghost" href="spec.md"><b>Specification</b></a> </div> </header> <section class="wrap"> <h2>Pricing</h2> <div class="tablewrap"><table> <tr><th>Route</th><th>Price</th><th>What lands in the 200 body</th></tr> <tr><td><code>POST /generate</code></td><td class="price">$0.01</td><td>The generated <code>skill.md</code>, plus a validation report for it — one call gives you a file you can commit</td></tr> <tr><td><code>GET /rules</code></td><td class="price">free</td><td>The whole rule catalogue as JSON: id, severity, title and fix for each of the 23 rules</td></tr> <tr><td><code>GET /spec</code></td><td class="price">free</td><td>The SKILL-MD format specification</td></tr> <tr><td><code>POST /validate</code></td><td class="price">$0.002</td><td>The full report: every finding with its rule id, line, what is wrong <i>in this document</i>, and the exact fix</td></tr> <tr><td><code>GET /</code> · <code>/health</code> · <code>/skill.md</code> · <code>/.well-known/x402</code> · <code>/openapi.json</code></td><td class="price">free</td><td>Discovery</td></tr> </table></div> </section> <section class="wrap"> <h2>How a paid call works</h2> <figure> <svg viewBox="0 0 880 250" role="img" aria-label="Flow: the agent requests a resource, the server answers 402 with two payment rails, the agent signs on Base or Solana, the facilitator verifies and settles, and the server returns 200 with the artifact."> <defs> <marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/> </marker> </defs> <g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif" text-anchor="middle"> <!-- 1. agent --> <rect x="8" y="93" width="118" height="64" rx="10" fill="var(--card)" stroke="var(--border)"/> <text x="67" y="118" font-size="14" font-weight="600" fill="var(--fg)">Agent</text> <text x="67" y="137" font-size="11" fill="var(--muted)">GET /resource</text> <!-- 2. the 402 --> <rect x="166" y="93" width="170" height="64" rx="10" fill="var(--card)" stroke="var(--accent)"/> <text x="251" y="116" font-size="14" font-weight="600" fill="var(--accent)">402 Payment Required</text> <text x="251" y="136" font-size="11" fill="var(--muted)">accepts: [ two rails ]</text> <!-- 3a. EVM rail --> <rect x="376" y="25" width="164" height="58" rx="10" fill="var(--card)" stroke="var(--border)"/> <text x="458" y="47" font-size="13" font-weight="600" fill="var(--fg)">USDC on Base</text> <text x="458" y="65" font-size="11" fill="var(--muted)">EIP-3009 signature</text> <!-- 3b. Solana rail --> <rect x="376" y="167" width="164" height="58" rx="10" fill="var(--card)" stroke="var(--border)"/> <text x="458" y="189" font-size="13" font-weight="600" fill="var(--fg)">USDC on Solana</text> <text x="458" y="207" font-size="11" fill="var(--muted)">signed SPL transfer</text> <text x="458" y="130" font-size="12" font-style="italic" fill="var(--muted)">the client picks</text> <!-- 4. settle --> <rect x="576" y="93" width="170" height="64" rx="10" fill="var(--card)" stroke="var(--border)"/> <text x="661" y="116" font-size="13" font-weight="600" fill="var(--fg)">X-PAYMENT</text> <text x="661" y="136" font-size="11" fill="var(--muted)">facilitator: verify → settle</text> <!-- 5. the goods --> <rect x="786" y="93" width="86" height="64" rx="10" fill="var(--accent)"/> <text x="829" y="118" font-size="14" font-weight="700" fill="#fff">200</text> <text x="829" y="136" font-size="11" fill="#dbe6ff">artifact</text> <g fill="none" stroke="var(--accent)" stroke-width="2" marker-end="url(#ah)"> <path d="M126,125 L158,125"/> <path d="M336,116 C356,116 356,54 368,54"/> <path d="M336,134 C356,134 356,196 368,196"/> <path d="M540,54 C556,54 556,116 568,116"/> <path d="M540,196 C556,196 556,134 568,134"/> <path d="M746,125 L778,125"/> </g> </g> </svg> <figcaption>One round trip of overhead. Every 402 quotes both rails; the client signs whichever chain it holds USDC on.</figcaption> </figure> </section> <section class="wrap"> <h2>Two rails, one price</h2> <div class="rails"> <div class="rail"> <b>USDC on Base</b> <div class="k">network <code>base-sepolia</code> / <code>base</code></div> <div class="k">scheme <code>exact</code> · EIP-3009 <code>transferWithAuthorization</code></div> <div class="k">payTo <code>0x40252CFDF8B20Ed757D61ff157719F33Ec332402</code></div> </div> <div class="rail"> <b>USDC on Solana</b> <div class="k">network <code>solana</code> / <code>solana-devnet</code></div> <div class="k">scheme <code>exact</code> · SPL <code>transferChecked</code>, fee sponsored</div> <div class="k">payTo <code>WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW</code></div> </div> </div> <p style="margin-top:14px">Both rails appear in the <code>accepts</code> array of every 402. Settlement runs through the <a href="https://x402.org/facilitator">x402 facilitator</a>, and the 200 carries an <code>X-PAYMENT-RESPONSE</code> receipt naming the rail and the transaction.</p> </section> <section class="wrap"> <h2>Quickstart</h2> <pre># as a CLI — free, no wallet needed npx x402-skill-md generate openapi.json -o skill.md npx x402-skill-md validate skill.md # PASS score 100/100 (0 errors, 0 warnings, 0 infos) # service: x402-domains endpoints: 2 rails: evm + solana # No findings. This document is conformant. # as a library npm i x402-skill-md import { openapiToSkillMd, validateSkillMd } from "x402-skill-md"; const { skillMd, warnings } = openapiToSkillMd(spec, { baseUrl: "https://api.example.com" }); validateSkillMd(skillMd).valid; // true validateSkillMd(skillMd).service.dualRail; // true # in CI — exits 1 on errors - run: npx x402-skill-md validate skill.md --strict</pre> </section> <section class="wrap"> <h2>What a skill.md is</h2> <p>The file an AI agent reads to learn how to <b>use and pay for</b> an HTTP service — in one fetch, with no SDK and no human. It is Markdown, so a model can read it directly; it has a defined section grammar, so a parser can check it.</p> <p>OpenAPI already describes shapes. It does not describe <b>what a call costs, in what asset, on which chains, to which address</b> — and for an agent with a wallet and no account, that is the load-bearing information. An agent has to answer three questions before it spends anything: can this service do what I need, what will it cost, and <i>can I pay at all</i>.</p> <p><a href="spec.md"><b>Read the specification →</b></a></p> </section> <section class="wrap"> <h2>Dual rail is mandatory, not optional</h2> <p>A conformant <code>skill.md</code> documents an EVM rail <b>and</b> a Solana rail — rules SM005 and SM006, both <code>error</code> severity. Three reasons:</p> <ul> <li>An agent's wallet lives on exactly <b>one</b> chain. A single-rail service is invisible to every agent on the other one, and it cannot learn that cheaply.</li> <li>The 402 challenge already carries an <code>accepts</code> <b>array</b>. Two entries cost one extra object; the client picks. There is no protocol reason to publish one.</li> <li>Discovery indexers filter on rails. A single-rail listing halves your reachable market for no gain.</li> </ul> <p>The spec also requires the <b>facilitator to be declared per rail</b>, because facilitators are chain-specific: the reference <code>x402.org</code> facilitator settles Base Sepolia only and will not settle a Solana payment. A document naming one facilitator for both rails is telling clients something false.</p> </section> <section class="wrap"> <h2>Findings carry fixes</h2> <p>A validator that only says “invalid” is not worth paying for. Every finding names the rule, where it is, what is wrong <i>in this document</i>, and the remedy:</p> <pre>{ "rule": "SM006", "severity": "error", "title": "No Solana payment rail documented", "detail": "There is no Payment section, so no Solana rail is documented.", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay." }</pre> <p>23 rules: 10 errors, 10 warnings, 3 infos. <code>valid</code> is true when there are <b>zero errors</b>, independent of score; the score (100 − 12/error − 4/warning − 1/info) is a quality signal, <code>valid</code> is the gate.</p> </section> <section class="wrap"> <h2>Why a paid service at all</h2> <p>The library is free on npm and the CLI does the same work locally, so the honest answer is: <b>the paid routes exist for agents.</b> An agent that has just written an OpenAPI document cannot <code>npm install</code> inside its own reasoning loop; it can make an HTTP request and pay a cent. Humans should use the CLI. <code>GET /rules</code> and <code>GET /spec</code> are free on both paths, because you should be able to see what you are being judged against before paying to be judged.</p> </section> <footer class="wrap"> <p> <a href="https://github.com/nirholas/x402-skill-md">GitHub</a> · <a href="https://github.com/nirholas/x402-skill-md/blob/main/skill.md">skill.md</a> · <a href="https://github.com/nirholas/x402-skill-md/blob/main/public/.well-known/x402">.well-known/x402</a> · <a href="https://github.com/nirholas/x402-skill-md/blob/main/openapi.json">openapi.json</a> · <a href="https://x402.org">x402 protocol</a> · <a href="https://www.npmjs.com/package/x402-skill-md">npm</a> </p> <p>Part of the <a href="https://github.com/nirholas/x402-suite">x402 Suite</a> · Apache-2.0 · <a href="mailto:[email protected]">[email protected]</a></p> </footer> </body> </html>docs/og.png35 KB40e3e4381e69upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/spec.md24 KB722329e3e5b6upstream ↗
# The SKILL-MD format, version 1.0 **Status:** stable · **Version:** 1.0 · **Media type:** `text/markdown` · **Conventional location:** `/skill.md` at a service's root, and `skill.md` at a repository's root A `skill.md` is the file an AI agent reads to learn how to *use and pay for* an HTTP service, in one fetch, with no SDK and no human. It is Markdown, so a language model can read it directly; it has a defined section grammar, so a parser can check it. This document specifies that grammar. The reference implementation is [`x402-skill-md`](https://github.com/nirholas/x402-skill-md), which generates conformant documents from OpenAPI and validates existing ones against every rule below. --- ## 1. Why this exists OpenAPI already describes *shapes*. It does not describe **what a call costs, in what asset, on which chains, to which address** — and for an agent with a wallet and no account, that is the load-bearing information. An agent must be able to answer three questions before it spends anything: 1. Can this service do what I need? 2. What will it cost? 3. **Can I pay at all** — is there a rail my wallet lives on? Question 3 is why this format makes **dual-rail payment mandatory** (§5). A service that only takes USDC on Base is unusable to an agent holding USDC on Solana, and an agent that cannot discover that fact until it has already made a request has wasted a round trip and a planning step. The file states both rails, or it is not conformant. ### Relationship to other artifacts | artifact | audience | answers | |---|---|---| | `skill.md` | language models, humans skimming | what, how much, how to pay, what comes back | | `/.well-known/x402` | indexers, machine planners | the same, as JSON, per resource | | `openapi.json` | code generators | exact request/response shapes | They are three renderings of one contract and should never disagree. `x402-skill-md generate` derives the first from the third, which is the cheapest way to keep them honest. --- ## 2. Terminology **MUST**, **SHOULD**, **MAY** are used as in RFC 2119. - **Document** — one `skill.md` file. - **Endpoint section** — a level-2 heading matching §6 plus everything until the next level-2 heading. - **Rail** — one chain on which a service accepts payment: a network, an asset, a `payTo` address and a facilitator. - **Conformant** — the document triggers no rule of severity `error` (§9). --- ## 3. Document structure A conformant document is, in order: ``` [frontmatter] optional, §4 # <name> required, exactly one H1 <summary paragraph> required, ≥ 40 characters of prose <service card> required, must include a Base URL ## Payment required, §5 ## `M /path` — $price one or more, §6 ## Free routes optional but recommended, §7 ## Errors required, §8 ## Data source required *if* responses may be non-live, §7.3 <any other sections> allowed anywhere after the summary ``` Sections other than the ones named above are permitted and ignored by the validator. Order is not enforced beyond "H1 first" — but the order above is what readers expect, and the generator emits it. ### 3.1 Encoding UTF-8. `\n` or `\r\n` line endings (both are accepted; `\r\n` is normalised on parse). No byte-order mark. ### 3.2 Code fences Fenced blocks are opaque to the section grammar: a `#` inside a fence is never a heading. Use ` ```json ` for response and challenge examples — the validator looks for that fence specifically when checking rules SM009 and SM018. ` ```jsonc ` is accepted where you want comments in an example. --- ## 4. Frontmatter (optional) A document MAY begin with a `---`-delimited block of `key: value` lines. It exists so indexers can read the essentials without parsing prose. ```yaml --- skillMd: "1.0" name: x402-domains baseUrl: https://domains.example.com manifest: /.well-known/x402 openapi: /openapi.json contact: [email protected] --- ``` | key | required if frontmatter present | meaning | |---|---|---| | `skillMd` | yes | format version, currently `"1.0"` | | `name` | no | service identifier; SHOULD match the H1 | | `baseUrl` | no | absolute origin | | `manifest` | no | path to the x402 manifest | | `openapi` | no | path to the OpenAPI document | | `contact` | no | email or URL | This is intentionally *not* full YAML: values are plain strings, quotes are stripped, and nesting is not supported. A malformed block raises **SM022** (warning) — never an error, because frontmatter is optional and an indexer that cannot read it can still read the prose. --- ## 5. The Payment section A conformant document MUST have a level-2 section whose title begins with `Payment`. It is the part an agent needs before it can do anything else. ### 5.1 Required content | requirement | rule | |---|---| | An EVM network (`base` or `base-sepolia`) **and** a `0x…` payTo address | SM005 (error) | | A Solana network (`solana` or `solana-devnet`) **and** a base58 payTo address | SM006 (error) | | The x402 protocol version and scheme | SM016 (warning) | | A fenced JSON example of a real 402 body containing `accepts` | SM018 (warning) | ### 5.2 Dual rail is mandatory **A conformant `skill.md` documents at least one EVM rail and at least one Solana rail.** Not "may"; not "if convenient". The reasoning: - An agent's wallet lives on exactly **one** chain. A single-rail service is invisible to every agent on the other chain, and it cannot learn that cheaply. - The 402 challenge already carries an `accepts` **array**. Two entries cost one extra object; the client picks. There is no protocol reason to publish one. - Discovery indexers filter on rails. A single-rail listing halves your reachable market for no gain. If a service genuinely cannot settle on one of the two chains, it is not conformant to 1.0 and should say so plainly rather than imply coverage it lacks. ### 5.3 Declaring rails The recommended rendering is a table — dense, parseable, and readable by a model: ```markdown | rail | network | asset | payTo | facilitator | |---|---|---|---|---| | evm | `base-sepolia` | USDC `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | `0x40252CFDF8B20Ed757D61ff157719F33Ec332402` | `https://x402.org/facilitator` | | solana | `solana` | USDC `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | `WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW` | `https://facilitator.payai.network` | ``` Prose is also accepted — the validator looks for a network name and an address of the right shape within the section, not for a specific layout. #### The facilitator column is per rail, not per service This matters and is easy to get wrong. A facilitator is the component that verifies a signed payment and broadcasts settlement, and **facilitators are chain-specific**. The reference `https://x402.org/facilitator` settles Base Sepolia only; it will not settle a Solana payment. A service documenting both rails therefore has **two** facilitator endpoints, and the document MUST show which belongs to which network. A client that assumes one facilitator for the whole service will send a valid Solana payment to an endpoint that cannot settle it. The suite's convention, which implementations SHOULD follow: | rail | default facilitator | env override | |---|---|---| | evm | `https://x402.org/facilitator` | `FACILITATOR_URL` | | solana | `https://facilitator.payai.network` | `SOLANA_FACILITATOR_URL` | ### 5.4 The challenge example Include a real 402 body. This is the single highest-value block in the file, because a client can be written against it with no guessing: ```json { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ { "scheme": "exact", "network": "base-sepolia", "maxAmountRequired": "1000", "resource": "https://domains.example.com/check/example.com", "description": "Live RDAP lookup for one domain", "mimeType": "application/json", "payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "maxTimeoutSeconds": 60, "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "outputSchema": { "input": { "…": "§5.5" }, "output": { "…": "§5.5" } }, "extra": { "name": "USDC", "version": "2" } }, { "scheme": "exact", "network": "solana", "maxAmountRequired": "1000", "resource": "https://domains.example.com/check/example.com", "description": "Live RDAP lookup for one domain", "mimeType": "application/json", "payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW", "maxTimeoutSeconds": 60, "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "outputSchema": { "input": { "…": "§5.5" }, "output": { "…": "§5.5" } }, "extra": { "name": "USD Coin", "decimals": 6, "feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4" } } ] } ``` Notes a document SHOULD make explicit, because clients get them wrong: - **`maxAmountRequired` is in the asset's base units.** USDC has six decimals, so `"1000"` is $0.001 and `"10000"` is $0.01. It is not dollars and it is not a float. - **`extra` differs per rail.** On EVM it is the EIP-712 domain the wallet needs to build the `transferWithAuthorization` signature. On Solana it carries `decimals` and `feePayer` — the facilitator's sponsor account, which pays the SOL network fee so a buyer holding only USDC can still transact. - **`resource` is the absolute URL being purchased**, path only. If the price does not vary with the query string, say so. - **`outputSchema` is elided above only to keep the example readable.** In a real challenge it is spelled out in full on every entry — see §5.5. ### 5.5 The invocation contract (`outputSchema`) Every accept entry SHOULD carry an `outputSchema` — the machine-readable half of what the rest of this document says in prose. It is what lets an agent that has never seen the service decide whether the call is worth the price and then make it correctly, from the challenge alone, with no second fetch: ```json "outputSchema": { "input": { "type": "http", "method": "GET", "queryParams": { "max": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 } }, "pathParams": { "domain": { "type": "string", "description": "Registrable domain; IDNs punycode", "x-required": true } } }, "output": { "type": "object", "required": ["domain", "status", "checkedAt"], "properties": { "domain": { "type": "string" }, "status": { "type": "string", "enum": ["registered", "available", "unknown"] }, "checkedAt": { "type": "string", "format": "date-time" } } } } ``` - **`input`** describes how to build the request. `type` is `"http"` and `method` is the route's verb. Parameters are split by where they go: `queryParams` for the query string, `pathParams` for `:segments` in the path. A route with a body instead sets `bodyType: "json"` and lists its top-level fields in `bodyFields`. Every value is a JSON Schema for that one parameter or field; mark the mandatory ones `"x-required": true`, since a per-field schema has nowhere to put JSON Schema's document-level `required` array. - **`output`** is the JSON Schema of the 200 body — the artifact being sold (§6.3), not a wrapper around it. Two rules keep this honest: 1. **Derive both halves from the OpenAPI document, do not hand-write them.** The endpoint's `parameters` / `requestBody` become `input`; its `200` response schema becomes `output`, with `$ref`s resolved so the challenge stands alone. Discovery crawlers treat the runtime 402 as authoritative, so a hand-maintained copy that drifts from the spec is worse than no copy at all. 2. **Every accept entry carries the same contract.** Which rail a buyer pays on cannot change what the endpoint accepts or returns, so `outputSchema` MUST be identical across the EVM and Solana entries. Omitting `input` or `output` is what the x402scan discovery audit reports as `SCHEMA_INPUT_MISSING` / `SCHEMA_OUTPUT_MISSING`, both **errors** — it is the difference between a service an agent can find and one it can actually use. ### 5.6 The paywall answers first **A paid route MUST return its 402 challenge before it validates the request or checks that the thing being asked for exists.** An unpaid `GET /verify/no-such-id-123` or an unpaid `POST /register` with an empty body is a *quote request*, not a malformed call, and the only correct answer to it is the challenge. A document SHOULD say so explicitly, because it is what makes a route probe-able: ```markdown Every paid route answers an unpaid request with 402 regardless of what you send. A synthetic id or an empty body still returns the challenge, so you can price a call before making it. ``` This is not a nicety. Discovery crawlers — and any agent deciding whether a service is worth using — probe paid routes without paying to read the price and the contract. A route that answers `404 NOT_FOUND` or `400 BAD_REQUEST` first looks broken rather than paid: x402scan skips it, and the service can end up passing an audit while being unregisterable. It is also an information leak, since a 404-before-402 tells an unpaying caller which ids exist. In practice this is an ordering rule about middleware. The paywall must sit *above* every paid handler, and the handler must assume it only ever runs on a settled request: ```ts app.use(paywall(PAID_ROUTES, { … })); // 402 for anything unpaid app.get("/verify/:id", (req, res) => { // only reached after settlement const record = store.get(req.params.id); if (!record) return res.status(404).json({ error: "NOT_FOUND" }); … }); ``` A 404 for a genuinely unknown id is correct — *after* payment. Services that cannot charge for a miss should validate cheaply and refund, or price the lookup as the search it is. ### 5.7 The receipt A document SHOULD state that paid responses carry `X-PAYMENT-RESPONSE`: base64 JSON naming the rail, network, transaction and payer. Agents use it to reconcile spend. --- ## 6. Endpoint sections ### 6.1 Heading grammar ``` ## `METHOD /path` — price ``` Formally: ``` heading = "##" WS "`" method WS path "`" WS dash WS price method = 1*UPPER ; GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS path = "/" *VCHAR ; ":param" for path parameters dash = "—" / "–" / "-" price = "free" / "$" 1*DIGIT [ "." 1*DIGIT ] ``` Regex, as implemented: ``` /^##\s+`([A-Z]+)\s+(\/\S*)`\s*[—–-]\s*(free|\$\d+(?:\.\d+)?)\s*$/ ``` Conformant: ```markdown ## `GET /check/:domain` — $0.001 ## `POST /bounties` — $0.01 ## `GET /skills.json` — free ``` Not conformant (each raises **SM008**): ```markdown ### POST /bounties — $0.01 ← level 3, and no backticks ## POST /bounties ($0.01) ← price not after a dash ## `POST /bounties` — 1 cent ← price not a literal ## `POST /bounties` — from $0.01 ← ranges are not prices (SM017) ``` Path parameters use `:name`, not `{name}`. A generator converting from OpenAPI MUST rewrite `/check/{domain}` to `/check/:domain`. ### 6.2 Required content per endpoint | requirement | rule | |---|---| | ≥ 20 characters of prose saying what the 200 body contains | SM010 (warning) | | A fenced ` ```json ` block showing a real 200 body | SM009 (warning) | | A parameter table, if the path has `:params` or examples show a query string | SM011 (warning) | | A price matching the literal grammar | SM017 (warning) | Parameter tables SHOULD use these columns: ```markdown | name | in | type | required | notes | |---|---|---|---|---| | `domain` | path | string | yes | Registrable domain; IDNs must be punycode | | `max` | query | integer | no | 1–20, default 5. Clamped, not rejected | | `amount` | body | number | yes | Pledged bounty in USD | ``` `in` is one of `path`, `query`, `body`, `header`. ### 6.3 The 200 body is the product The x402 suite's rule, which this format assumes: **every paid route returns the purchased artifact in the 200 response body.** No job ids, no callbacks, nothing to poll. If a document describes a paid route that returns only an acknowledgement, the design is wrong, not the document — restructure it as pay-per-poll (return a snapshot now) or return a signed claim instrument immediately. The response example is what an agent plans against, so make it a *real* one. Paste actual server output, do not hand-write plausible-looking values, and regenerate examples when the shape changes. --- ## 7. Other sections ### 7.1 Free routes A table of unpaid routes. Recommended, not required (**SM021**, info). It is how an agent orients itself for nothing: ```markdown | route | returns | |---|---| | `GET /` | service card: rails, prices, doc links | | `GET /health` | `{ ok, uptime }` | | `GET /skill.md` | this file | | `GET /.well-known/x402` | machine-readable resource manifest | | `GET /openapi.json` | OpenAPI 3.1 | ``` Free routes MAY instead be written as endpoint sections with price `free`. ### 7.2 Discovery links A document SHOULD reference `/.well-known/x402` (**SM014**, warning) and `/openapi.json` (**SM019**, info), and give a contact address (**SM015**, warning). ### 7.3 Data source **If any response can be something other than live upstream data — a fixture, a cache, a stub — the document MUST say so**, and responses SHOULD carry a `source` field naming which. A document mentioning fixtures without a `## Data source` section raises **SM023**. ```markdown | `source` | meaning | |---|---| | `podcastindex-live` | fetched from the upstream API at request time | | `fixture` | this deployment has no credentials and served a deterministic sample | ``` This exists because a paid response that quietly returns canned data is fraud, and because env-gated demos are otherwise genuinely useful — the honest version is to ship fixtures *and* label them in every response. --- ## 8. Errors A conformant document MUST have a section whose title begins with `Error` (**SM012**), and it SHOULD document the 402 (**SM013**) — the response every paying client meets first. ```markdown | status | body `error` | meaning | |---|---|---| | 400 | `INVALID_DOMAIN` | the path segment is not a valid domain | | 402 | — | payment required or rejected; body is the dual-rail challenge | | 502 | `RDAP_UPSTREAM_ERROR` | upstream failed | ``` A document SHOULD also list the 402 sub-reasons an agent can act on differently — an unpaid first attempt, a malformed header, a rail the service does not take, and a facilitator rejection are four different situations with four different retries. Implementations SHOULD return a 5xx rather than a 402 when the *facilitator* is unreachable, so a retry loop cannot mistake an outage for a price. --- ## 9. Rule catalogue Severity: **error** — not conformant, an agent may fail to use the service. **warning** — conformant but lossy, an agent has to guess. **info** — a suggestion. Ids are stable forever. A rule may be retired; its id is never reused. | id | severity | title | |---|---|---| | SM001 | error | Missing H1 title | | SM002 | error | Missing summary paragraph | | SM003 | error | Missing base URL | | SM004 | error | Missing `## Payment` section | | SM005 | error | No EVM payment rail documented | | SM006 | error | No Solana payment rail documented | | SM007 | error | No endpoints documented | | SM008 | error | Endpoint heading does not match the grammar | | SM009 | warning | Endpoint has no response example | | SM010 | warning | Endpoint has no description | | SM011 | warning | Endpoint takes parameters but does not document them | | SM012 | error | Missing `## Errors` section | | SM013 | warning | Errors section does not mention 402 | | SM014 | warning | No link to the x402 manifest | | SM015 | warning | No contact address | | SM016 | warning | No `x402Version` / protocol version stated | | SM017 | warning | Price is not a well-formed literal | | SM018 | warning | No 402 challenge example | | SM019 | info | No OpenAPI reference | | SM020 | error | Placeholder text left in the document | | SM021 | info | No free routes documented | | SM022 | warning | Frontmatter is present but malformed | | SM023 | info | Data provenance not documented | Machine-readable, with the fix text for each: `GET /rules` on any deployment, or `npx x402-skill-md rules --json`. ### 9.1 Scoring A document scores out of 100, losing 12 per error, 4 per warning, 1 per info, floored at 0. `valid` is `true` when there are **zero errors**, independent of score — the score is a quality signal, `valid` is the gate. --- ## 10. Conformance A **conformant SKILL-MD 1.0 document** triggers no rule of severity `error`. A **conformant validator** implements every rule in §9 with the stated severities and reports each finding with its rule id, a document-specific detail, and the remedy. A **conformant generator** emits documents that its own validator passes, and warns — without failing — when the source material lacks something the format wants (no summary, no price, no response schema). Silently inventing a price would be worse than a warning. --- ## 11. Versioning The version is `major.minor`, declared in frontmatter as `skillMd`. - **Minor** bumps add rules or optional structure. A 1.0 document stays conformant under 1.1; new rules arrive at `warning` or `info` severity, never `error`. - **Major** bumps may change the section grammar or promote a rule to `error`. A document with no frontmatter is assumed to be 1.0. Validators SHOULD accept documents declaring a *newer* minor version and validate them against the rules they know. --- ## 12. A minimal conformant document Every rule satisfied, nothing extra: ````markdown --- skillMd: "1.0" name: example-service --- # example-service Returns the current UTC time as a signed timestamp, so an agent can prove it asked at a particular moment. One paid route, one free route, no account required. - **Base URL:** `https://example.com` - **Manifest:** `GET /.well-known/x402` - **OpenAPI:** `GET /openapi.json` - **Contact:** [email protected] ## Payment x402 v1, scheme `exact`. Unpaid requests get a 402 whose `accepts` array lists both rails — USDC on Base and USDC on Solana. Your client picks. | rail | network | payTo | facilitator | |---|---|---|---| | evm | `base-sepolia` | `0x40252CFDF8B20Ed757D61ff157719F33Ec332402` | `https://x402.org/facilitator` | | solana | `solana` | `WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW` | `https://facilitator.payai.network` | ```json { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ { "scheme": "exact", "network": "base-sepolia", "maxAmountRequired": "1000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "resource": "https://example.com/now", "mimeType": "application/json", "maxTimeoutSeconds": 60, "extra": { "name": "USDC", "version": "2" } }, { "scheme": "exact", "network": "solana", "maxAmountRequired": "1000", "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW", "resource": "https://example.com/now", "mimeType": "application/json", "maxTimeoutSeconds": 60, "extra": { "name": "USD Coin", "decimals": 6, "feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4" } } ] } ``` Amounts are USDC base units — `"1000"` is $0.001. Paid responses carry an `X-PAYMENT-RESPONSE` receipt. ## `GET /now` — $0.001 The current UTC time with an HMAC signature over it, in this response body. ```json { "now": "2026-08-07T12:00:00.000Z", "signature": "9f2c…" } ``` ## Free routes | route | returns | |---|---| | `GET /health` | `{ ok, uptime }` | | `GET /.well-known/x402` | machine-readable resource manifest | | `GET /openapi.json` | OpenAPI 3.1 | ## Errors | status | meaning | |---|---| | 402 | payment required or rejected; body is the dual-rail challenge | | 500 | signing failed | ```` Check any document against this specification: ```bash npx x402-skill-md validate skill.md npx x402-skill-md validate skill.md --json --strict ``` --- *SKILL-MD 1.0 · maintained alongside the [x402 Suite](https://github.com/nirholas/x402-suite) · corrections to [email protected]*docs/tutorial.md10 KB66c9b69023abupstream ↗
# Tutorial — write a skill.md that agents can actually use Three ways in, in increasing order of ceremony: the CLI (free, local), the library (free, embedded), and the paid service (for agents that cannot install anything). Start with the CLI. ## 1. Validate something you already have No install, no clone, no wallet: ```bash npx x402-skill-md validate skill.md ``` ``` PASS score 100/100 (0 errors, 0 warnings, 0 infos) service: x402-domains endpoints: 2 rails: evm + solana No findings. This document is conformant. ``` A file with problems tells you what they are and how to fix them: ```bash printf '# broken-service\n\nToo short.\n\n## `GET /thing` — 1 cent\n' > broken.md npx x402-skill-md validate broken.md ``` ``` FAIL score 0/100 (8 errors, 3 warnings, 2 infos) service: broken-service endpoints: 0 rails: none ✗ SM002 (line 1) Missing summary paragraph The text under the title is only 10 characters — too short to tell an agent what this service does. fix: Put one prose paragraph directly under the H1 saying what the service does and what an agent gets from it. ✗ SM003 Missing base URL No line documents a base URL with a resolvable URL on it. fix: Document the base URL, e.g. `- **Base URL:** https://your-host` (a localhost placeholder is fine for a self-hosted service). ✗ SM006 No Solana payment rail documented There is no Payment section, so no Solana rail is documented. fix: Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay. ✗ SM008 (line 5) Endpoint heading does not match the grammar `## `GET /thing` — 1 cent` names an HTTP method but does not match the endpoint grammar. fix: Use ``## `METHOD /path` — $0.001`` : backticked `METHOD /path`, a dash, then `$amount` or `free`. … ``` `--json` gives the same thing as a machine-readable report. `--strict` also fails on warnings. ## 2. See the rules before you write anything ```bash npx x402-skill-md rules npx x402-skill-md rules --json | jq '.rules | to_entries[] | select(.value.severity=="error") | .key' ``` 23 rules: 10 errors, 10 warnings, 3 infos. Reading them first turns validation from a diagnosis into a checklist. The full normative text is the [specification](spec.md). ## 3. Generate from OpenAPI If you already have an OpenAPI document, do not write the file by hand: ```bash npx x402-skill-md generate openapi.json -o skill.md \ --base-url https://api.example.com \ --contact [email protected] ``` ``` wrote skill.md — x402-podcasts, 2 paid of 6 endpoints ``` Then check it: ```bash npx x402-skill-md validate skill.md # PASS score 100/100 ``` ### Declaring prices The generator reads a price from, in order of preference: 1. **`x-x402` on the operation** — the exact, declarative way: ```json "/search": { "get": { "summary": "Search shows", "x-x402": { "price": "$0.002" }, "responses": { "200": {…}, "402": {…} } } } ``` 2. The `maxAmountRequired` in a **402 response example**, converted from USDC base units. 3. `--default-price` from the command line. 4. Otherwise: an operation with **no 402 response** is treated as free. Anything guessed comes back as a warning on stderr: ``` warning: GET /reports: declares a 402 response but no price could be determined. Add `x-x402: { price: "$0.001" }` to the operation, or pass defaultPrice. ``` The generator never invents a price silently. A wrong price in a published contract is worse than a missing one. ### What the generator cannot invent It will warn, not guess, when the OpenAPI document is thin: | missing in OpenAPI | effect | fix | |---|---|---| | `info.summary` / `info.description` | the summary paragraph is a stub | write one — it is the first thing a model reads | | a 200 example or schema | the endpoint section has no response example (SM009) | add `content.application/json.example` | | operation `description` | the section says "Returns the resource in the 200 body" | write one sentence saying what the body contains | A generated file is a good 80%. The last 20% — the sentence explaining what `status: unknown` means, the note that fixtures exist — is the part that makes an agent behave correctly, and only you can write it. ## 4. Use it as a library ```bash npm i x402-skill-md ``` ```ts import { readFile, writeFile } from "node:fs/promises"; import { openapiToSkillMd, validateSkillMd, RULES } from "x402-skill-md"; const spec = JSON.parse(await readFile("openapi.json", "utf8")); const { skillMd, service, warnings } = openapiToSkillMd(spec, { baseUrl: "https://api.example.com", contact: "[email protected]", defaultPrice: "$0.001", frontmatter: true, }); for (const w of warnings) console.warn(w); await writeFile("skill.md", skillMd); const report = validateSkillMd(skillMd); console.log(report.valid, report.score); // true 100 console.log(report.service.dualRail); // true console.log(report.service.endpoints); // [{ method, path, price }, …] // The rule catalogue is exported too — build your own reporting on it. console.log(RULES.SM006.fix); ``` The parser is also exported if you want to do something else with the structure: ```ts import { parseSkillMd } from "x402-skill-md"; const doc = parseSkillMd(await readFile("skill.md", "utf8")); doc.endpoints; // [{ method, path, price, line, hasJsonExample, … }] doc.frontmatter; // { skillMd: "1.0", name: "…", … } | null doc.sections; // every level-2 section that is not an endpoint ``` ## 5. Put it in CI `validate` exits 1 when there are errors, so it needs no wrapper: ```yaml - name: skill.md is conformant run: npx x402-skill-md validate skill.md --strict ``` Add `--strict` once you are at zero warnings, so the file cannot quietly degrade. If you generate the file, regenerate and diff instead — a `skill.md` that has drifted from its OpenAPI is worse than none, because an agent will trust it: ```yaml - run: npx x402-skill-md generate openapi.json > /tmp/skill.md - run: diff -u skill.md /tmp/skill.md ``` ## 6. Run the service Everything above is free and local. The HTTP service exists for agents that cannot install anything: ```bash git clone https://github.com/nirholas/x402-skill-md.git cd x402-skill-md npm install cp .env.example .env # already filled with working defaults npm run dev # http://localhost:4029 ``` ``` x402-skill-md listening on http://localhost:4029 SKILL-MD format version 1.0 · 23 rules Payment rails (USDC — the client picks): evm base-sepolia → 0x40252CFDF8B20Ed757D61ff157719F33Ec332402 via https://x402.org/facilitator solana solana → WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW via https://facilitator.payai.network Routes: POST /generate $0.01 POST /validate $0.002 GET /rules free GET /spec free ``` The free routes work immediately: ```bash curl -s localhost:4029/rules | jq '{skillMdVersion, count}' # { "skillMdVersion": "1.0", "count": 23 } curl -s localhost:4029/spec | head -3 ``` ## 7. Your first 402 ```bash curl -i -s -X POST localhost:4029/validate -H 'content-type: text/markdown' -d '# x' ``` ```jsonc { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ { "scheme": "exact", "network": "base-sepolia", "maxAmountRequired": "2000", "payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", … }, { "scheme": "exact", "network": "solana", "maxAmountRequired": "2000", "payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW", "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", … } ] } ``` `maxAmountRequired` is in USDC base units — six decimals, so `"2000"` is $0.002. ## 8. Pay for one Get a throwaway Base Sepolia wallet and a little testnet USDC from the [Circle faucet](https://faucet.circle.com), then: ```bash PRIVATE_KEY=0xyourTestnetKey npm run client ``` ``` SKILL-MD 1.0 — 23 rules, catalogue is free at GET /rules errors: 10, warnings: 10, infos: 3 ① Unpaid request → expect a dual-rail 402 accepts: base-sepolia 10000 base units USDC → 0x40252CF… accepts: solana 10000 base units USDC → WwwuGbqH… ② Generating from 0xYourWallet on Base Sepolia — $0.01 ③ 200 — example-service: 1 paid of 2 endpoints generated file scores 100/100, valid: true --- skill.md --- …the whole file… ``` ## 9. The one thing to understand about `/validate` A non-conformant document is a **200 with `valid: false`**, not a 4xx. You paid for the report and the report is the artifact — findings, fixes and all. Only an unparseable request (empty body, wrong content type) is an error. Agents get this wrong and retry. Say so in your tool description. ## 10. Going to mainnet ```bash # EVM rail → Base mainnet NETWORK=base FACILITATOR_URL=https://your-mainnet-facilitator.example # Solana rail → already mainnet by default SOLANA_NETWORK=mainnet-beta SOLANA_FACILITATOR_URL=https://facilitator.payai.network # your own addresses, or you are donating to the suite PAY_TO_ADDRESS=0xYourMainnetAddress SOLANA_PAY_TO_ADDRESS=YourSolanaAddress # so 402 challenges quote absolute public URLs PUBLIC_BASE_URL=https://skill-md.yourdomain.com ``` Facilitators are **per rail**. `https://x402.org/facilitator` settles Base Sepolia only; the Solana rail needs its own, which is why `SOLANA_FACILITATOR_URL` exists and defaults to PayAI's public one. Pointing both rails at one facilitator will silently fail to settle one of them. Then list the deployment: submit your origin to [x402scan.com](https://x402scan.com), the x402 Bazaar, and [agentic.market](https://agentic.market). They read `/.well-known/x402`, which this server already serves. ## Next - **[Specification](spec.md)** — the format itself, normative - [API reference](api.md) — every field of every response - [For AI agents](agents.md) — discovery, MCP, listing - [`examples/curl.md`](https://github.com/nirholas/x402-skill-md/blob/main/examples/curl.md) — the protocol by handexamples/agent-client.ts7.8 KB1387e225770eupstream ↗
/** * examples/agent-client.ts — generate a skill.md, then validate it, paid. * * PRIVATE_KEY=0x… npm run client * PRIVATE_KEY=0x… OPENAPI=./path/to/openapi.json npm run client * * `wrapFetchWithPayment` catches each 402, picks the EVM requirement out of the * dual-rail `accepts` array, signs an EIP-3009 authorisation for the exact * amount, and retries with `X-PAYMENT`. The Solana rail is shown at the bottom. */ import { readFileSync } from "node:fs"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { base, baseSepolia } from "viem/chains"; import { wrapFetchWithPayment, decodeXPaymentResponse } from "x402-fetch"; const BASE_URL = process.env.BASE_URL ?? "http://localhost:4029"; const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}` | undefined; /** A tiny but complete OpenAPI document, used when none is supplied. */ const SAMPLE_OPENAPI = { openapi: "3.1.0", info: { title: "example-service", summary: "Returns the current UTC time as a signed timestamp, so an agent can prove it asked at a particular moment.", contact: { email: "[email protected]" }, }, servers: [{ url: "https://example.com" }], paths: { "/now": { get: { operationId: "getNow", summary: "Current UTC time, signed", description: "The current UTC time with an HMAC signature over it, in this response body.", "x-x402": { price: "$0.001" }, responses: { "200": { description: "The signed timestamp.", content: { "application/json": { example: { now: "2026-08-07T12:00:00.000Z", signature: "9f2c…" }, }, }, }, "402": { description: "Payment required." }, }, }, }, "/health": { get: { operationId: "getHealth", summary: "Liveness probe (free)", responses: { "200": { description: "OK" } }, }, }, }, }; function loadOpenapi(): Record<string, unknown> { const path = process.env.OPENAPI; if (!path) return SAMPLE_OPENAPI; return JSON.parse(readFileSync(path, "utf8")); } async function showChallenge(): Promise<void> { console.log(`\n① Unpaid request → expect a dual-rail 402\n POST ${BASE_URL}/generate`); const res = await fetch(`${BASE_URL}/generate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(SAMPLE_OPENAPI), }); const body = await res.json(); console.log(` HTTP ${res.status}`); for (const a of body.accepts ?? []) { console.log( ` accepts: ${String(a.network).padEnd(14)} ${a.maxAmountRequired} base units USDC → ${a.payTo}`, ); } } async function main(): Promise<void> { // The rule catalogue is free — see what you'll be judged against first. const rules = await (await fetch(`${BASE_URL}/rules`)).json(); console.log(`SKILL-MD ${rules.skillMdVersion} — ${rules.count} rules, catalogue is free at GET /rules`); console.log( `errors: ${rules.rules.filter((r: { severity: string }) => r.severity === "error").length}, ` + `warnings: ${rules.rules.filter((r: { severity: string }) => r.severity === "warning").length}, ` + `infos: ${rules.rules.filter((r: { severity: string }) => r.severity === "info").length}`, ); await showChallenge(); if (!PRIVATE_KEY) { console.log( "\nSet PRIVATE_KEY to a funded Base Sepolia wallet to complete the purchase." + "\nTestnet USDC faucet: https://faucet.circle.com\n" + "\nOr skip payment entirely and use the library: npx x402-skill-md generate openapi.json\n", ); return; } const chain = process.env.NETWORK === "base" ? base : baseSepolia; const account = privateKeyToAccount(PRIVATE_KEY); const wallet = createWalletClient({ account, chain, transport: http() }).extend(publicActions); // eslint-disable-next-line @typescript-eslint/no-explicit-any const pay = wrapFetchWithPayment(fetch, wallet as any); console.log(`\n② Generating from ${account.address} on ${chain.name} — $0.01`); const generated = await pay(`${BASE_URL}/generate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ openapi: loadOpenapi() }), }); const result = await generated.json(); if (!generated.ok) throw new Error(`generate failed: ${generated.status} ${JSON.stringify(result)}`); console.log( `\n③ 200 — ${result.service.name}: ${result.service.paid} paid of ${result.service.endpoints} endpoints`, ); console.log( ` generated file scores ${result.validation.score}/100, valid: ${result.validation.valid}`, ); for (const w of result.warnings ?? []) console.log(` warning: ${w}`); console.log("\n--- skill.md ---"); console.log(result.skillMd); console.log("--- end ---"); const r1 = generated.headers.get("x-payment-response"); if (r1) console.log("\n X-PAYMENT-RESPONSE:", decodeXPaymentResponse(r1)); // --- validate something deliberately broken, to see the fixes ----------- const broken = "# broken-service\n\nToo short.\n\n## `GET /thing` — 1 cent\n"; console.log(`\n④ Validating a deliberately broken document — $0.002`); const checked = await pay(`${BASE_URL}/validate`, { method: "POST", headers: { "content-type": "text/markdown" }, body: broken, }); const report = await checked.json(); console.log( ` valid: ${report.valid} score: ${report.score}/100 ` + `(${report.summary.errors} errors, ${report.summary.warnings} warnings)`, ); for (const f of report.findings) { console.log(` ${f.severity === "error" ? "✗" : f.severity === "warning" ? "!" : "·"} ${f.rule} ${f.title}`); console.log(` ${f.detail}`); console.log(` fix: ${f.fix}`); } console.log( "\n Note this was a 200, not a 4xx. A non-conformant document is a successful report —" + "\n you bought the findings, and the findings are the artifact.", ); } main().catch((err) => { console.error("\nfailed:", err instanceof Error ? err.message : err); process.exit(1); }); /* --------------------------------------------------------------------------- * Paying on the SOLANA rail instead * --------------------------------------------------------------------------- * import { * prepareSolanaCheckout, * encodeX402Payment, * } from "@three-ws/x402-payment-modal/server"; * * const url = `${BASE_URL}/validate`; * const challenge = await (await fetch(url, { method: "POST", body: "# x" })).json(); * const accept = challenge.accepts.find((a: any) => a.network.startsWith("solana")); * * const { tx_base64 } = await prepareSolanaCheckout({ accept, buyer: myPubkey }); * const signed = await wallet.signTransaction(tx_base64); // Phantom, Solflare, a keypair * const { x_payment } = encodeX402Payment({ accept, signedTxBase64: signed, resourceUrl: url }); * * await fetch(url, { * method: "POST", * headers: { "content-type": "text/markdown", "X-PAYMENT": x_payment }, * body: mySkillMd, * }); * * Those helpers only build and encode the payment client-side; verification and * settlement happen server-side through the rail's facilitator. * `accept.extra.feePayer` sponsors the SOL network fee, so the buyer spends USDC only. * * --------------------------------------------------------------------------- * No wallet? The same logic ships as a library and a CLI. * --------------------------------------------------------------------------- * npm i x402-skill-md * * import { openapiToSkillMd, validateSkillMd } from "x402-skill-md"; * const { skillMd } = openapiToSkillMd(JSON.parse(readFileSync("openapi.json", "utf8"))); * console.log(validateSkillMd(skillMd).valid); // true * * npx x402-skill-md generate openapi.json -o skill.md * npx x402-skill-md validate skill.md --strict */examples/curl.md7.1 KBaf4ccfbe22a7upstream ↗
# The raw 402 → pay → 200 walkthrough No SDK — just HTTP, so you can see exactly what the protocol does. ```bash npm install && npm run dev # http://localhost:4029 ``` ## 0. Discover the service, and the rules, for free ```bash curl -s localhost:4029/ | jq '{name, skillMdVersion, endpoints}' curl -s localhost:4029/rules | jq '{skillMdVersion, count}' curl -s localhost:4029/spec | head -5 ``` ``` { "skillMdVersion": "1.0", "count": 23 } ``` The whole rule catalogue and the specification are free. You should be able to see what you are being judged against before paying to be judged — and in most cases self-check first and never need the paid route at all. ```bash curl -s localhost:4029/rules | jq '.rules[] | select(.severity=="error") | {id, title}' ``` ## 1. Validate without paying → 402 with **both** rails ```bash curl -i -s -X POST localhost:4029/validate \ -H 'content-type: text/markdown' \ --data-binary @skill.md ``` ```http HTTP/1.1 402 Payment Required Content-Type: application/json ``` ```jsonc { "x402Version": 1, "error": "X-PAYMENT header is required", "accepts": [ { "scheme": "exact", "network": "base-sepolia", "maxAmountRequired": "2000", // 2000 base units = $0.002 USDC (6 dp) "resource": "http://localhost:4029/validate", "description": "Validate a posted skill.md and return a report with per-finding fixes", "mimeType": "application/json", "payTo": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "maxTimeoutSeconds": 60, "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "extra": { "name": "USDC", "version": "2" } }, { "scheme": "exact", "network": "solana", "maxAmountRequired": "2000", "resource": "http://localhost:4029/validate", "description": "Validate a posted skill.md and return a report with per-finding fixes", "mimeType": "application/json", "payTo": "WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW", "maxTimeoutSeconds": 60, "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "extra": { "name": "USD Coin", "decimals": 6, "feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4" } } ] } ``` Note the two facilitators behind those rails: `x402.org` settles the Base Sepolia entry, PayAI settles the Solana one. They are not interchangeable. ## 2. Build the payment `X-PAYMENT` is base64 of a JSON payload proving you authorised exactly `maxAmountRequired` to `payTo` on the rail you chose. **EVM rail** — an EIP-3009 `transferWithAuthorization` signature: ```jsonc { "x402Version": 1, "scheme": "exact", "network": "base-sepolia", "payload": { "signature": "0x…", "authorization": { "from": "0xYourWallet", "to": "0x40252CFDF8B20Ed757D61ff157719F33Ec332402", "value": "2000", "validAfter": "0", "validBefore": "1791234567", "nonce": "0x…" } } } ``` **Solana rail** — a signed SPL `transferChecked` transaction, base64, in the same envelope. `@three-ws/x402-payment-modal/server` builds and encodes it client-side (`prepareSolanaCheckout` → sign → `encodeX402Payment`); verification and settlement still happen server-side through the facilitator. ```bash X_PAYMENT=$(printf '%s' "$PAYLOAD_JSON" | base64 -w0) ``` ## 3. Retry with the header → 200 + the report ```bash curl -i -s -X POST localhost:4029/validate \ -H 'content-type: text/markdown' \ -H "X-PAYMENT: $X_PAYMENT" \ --data-binary @broken.md ``` Where `broken.md` is: ```markdown # broken-service Too short. ## `GET /thing` — 1 cent ``` ```http HTTP/1.1 200 OK X-PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLCJyYWlsIjoiZXZtIiwi… ``` ```json { "skillMdVersion": "1.0", "valid": false, "score": 0, "summary": { "errors": 8, "warnings": 3, "infos": 2 }, "service": { "name": "broken-service", "baseUrl": null, "contact": null, "endpoints": [], "rails": { "evm": false, "solana": false }, "dualRail": false }, "findings": [ { "rule": "SM002", "severity": "error", "title": "Missing summary paragraph", "detail": "The text under the title is only 10 characters — too short to tell an agent what this service does.", "fix": "Put one prose paragraph directly under the H1 saying what the service does and what an agent gets from it.", "line": 1 }, { "rule": "SM006", "severity": "error", "title": "No Solana payment rail documented", "detail": "There is no Payment section, so no Solana rail is documented.", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay." }, { "rule": "SM008", "severity": "error", "title": "Endpoint heading does not match the grammar", "detail": "`## `GET /thing` — 1 cent` names an HTTP method but does not match the endpoint grammar.", "fix": "Use ``## `METHOD /path` — $0.001`` : backticked `METHOD /path`, a dash, then `$amount` or `free`.", "line": 5 } ], "checkedAt": "2026-08-07T12:00:00.000Z" } ``` **That 200 is the point.** A non-conformant document is a *successful report*, not an error — you bought the findings, and the findings are the artifact. Only an unparseable request (empty body, wrong content type) is a 4xx. That exact output, minus the payment, is what you get for free from the CLI: ```bash npx x402-skill-md validate broken.md --json ``` ## 4. Generate — $0.01 ```bash curl -s -X POST localhost:4029/generate \ -H 'content-type: application/json' \ -H "X-PAYMENT: $X_PAYMENT_10000" \ -d '{"openapi": '"$(cat openapi.json)"', "options": {"baseUrl": "https://api.example.com"}}' \ | jq -r .skillMd > skill.md ``` The response also carries a validation report for what it just generated: ```bash … | jq '{service, warnings, validation: .validation | {valid, score}}' ``` ```json { "service": { "name": "x402-podcasts", "baseUrl": "http://localhost:4028", "endpoints": 6, "paid": 2 }, "warnings": [], "validation": { "valid": true, "score": 100 } } ``` `warnings` is where the generator admits what it had to guess — a missing summary, a price it could not determine. It never invents a price silently. ## 5. Prices are separate payments `$0.01` for generate, `$0.002` for validate. `X-PAYMENT` is not a session: each paid call needs its own signed payment for its own amount. Get the challenge from an unpaid call to the route you want. ## Errors you may hit | what you sent | you get | |---|---| | no header on a paid route | 402, `error: "X-PAYMENT header is required"` | | garbage header | 402, `error: "invalid X-PAYMENT header: …"` | | a rail we don't take | 402, `error: "unsupported rail: …"` | | short-paid or expired authorisation | 402, `error:` the facilitator's `invalidReason` | | an empty body | 400, `BAD_REQUEST` | | JSON with no `paths` to `/generate` | 400, `NOT_AN_OPENAPI_DOCUMENT` | | a *non-conformant* skill.md | **200**, `valid: false` — that is a report, not an error |examples/mcp-tool.md5.4 KB315c1335553bupstream ↗
# Exposing x402-skill-md as an MCP tool [Model Context Protocol](https://modelcontextprotocol.io) servers give Claude tools it can call mid-conversation. This one is unusually well suited to it: an agent that has just written or discovered an OpenAPI document cannot `npm install` inside its own reasoning loop, but it can make an HTTP request and pay a cent. ## The server ```bash npm install @modelcontextprotocol/sdk x402-fetch viem zod ``` `mcp-skill-md.ts`: ```ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia } from "viem/chains"; import { wrapFetchWithPayment } from "x402-fetch"; import { z } from "zod"; const BASE_URL = process.env.X402_SKILL_MD_URL ?? "http://localhost:4029"; // One wallet, reused for every purchase. Its balance IS the spending cap. const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const wallet = createWalletClient({ account, chain: baseSepolia, transport: http() }) .extend(publicActions); const pay = wrapFetchWithPayment(fetch, wallet as never); const server = new McpServer({ name: "x402-skill-md", version: "0.1.0" }); // Free: the rule catalogue. Expose it so the model can self-check before spending. server.tool( "skill_md_rules", "Get the SKILL-MD rule catalogue — every rule id, severity, title and fix. FREE. " + "Call this before writing a skill.md by hand; most problems can be avoided rather than " + "diagnosed.", {}, async () => { const res = await fetch(`${BASE_URL}/rules`); return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] }; }, ); server.tool( "generate_skill_md", "Turn an OpenAPI 3.x document into a conformant skill.md — the agent-facing contract file " + "an x402 service ships at its root. Returns the file plus a validation report for it. " + "$0.01 USDC, paid automatically. Declare prices as `x-x402: { price: \"$0.001\" }` on each " + "paid operation; anything the generator has to guess comes back in `warnings`.", { openapi: z.record(z.unknown()).describe("The OpenAPI 3.x document, parsed"), baseUrl: z.string().optional().describe("Overrides servers[0].url"), contact: z.string().optional().describe("Overrides info.contact.email"), defaultPrice: z.string().optional().describe('Price for paid ops with no amount, e.g. "$0.001"'), }, async ({ openapi, ...options }) => { const res = await pay(`${BASE_URL}/generate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ openapi, options }), }); return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] }; }, ); server.tool( "validate_skill_md", "Check a skill.md against all 23 SKILL-MD rules. Returns a report naming every finding, " + "where it is, what is wrong, and the exact fix. $0.002 USDC. A non-conformant document " + "is a successful report with `valid: false` — not an error.", { skillMd: z.string().describe("The full contents of the skill.md file") }, async ({ skillMd }) => { const res = await pay(`${BASE_URL}/validate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ skillMd }), }); return { content: [{ type: "text", text: JSON.stringify(await res.json(), null, 2) }] }; }, ); await server.connect(new StdioServerTransport()); ``` ## Wiring it into Claude Desktop / Claude Code `claude_desktop_config.json` (or `.mcp.json` for Claude Code): ```json { "mcpServers": { "x402-skill-md": { "command": "npx", "args": ["tsx", "/absolute/path/to/mcp-skill-md.ts"], "env": { "PRIVATE_KEY": "0xYourFundedTestnetKey", "X402_SKILL_MD_URL": "https://your-deployment.example.com" } } } } ``` Then: > **You:** I just shipped an API. Here's the OpenAPI spec — write me a skill.md for it. > > **Claude:** *(calls `skill_md_rules` — free — to see what conformance requires, then > `generate_skill_md` — $0.01 — and reports: 4 paid endpoints, scores 100/100, one warning > that `/reports` had no 200 example so the section has no response block)* > > **You:** Fix that and check it again. > > **Claude:** *(adds the example, calls `validate_skill_md` — $0.002 — confirms 100/100)* ## Notes - **Call `skill_md_rules` first.** It is free, and it turns validation from a diagnosis into a checklist. The paid `validate` route is worth it *after* a draft exists, not before. - **Budget the wallet, not the tool.** The MCP process holds the key; its balance is the agent's spending cap. - **For humans, use the CLI instead.** `npx x402-skill-md validate skill.md` does the same work for free. The paid service exists because an agent cannot `npm install` mid-thought. - **Teach the model about the 200.** `valid: false` is a successful report; it should read the `findings` and fix the file, not retry the call. - **Solana rail.** Swap `wrapFetchWithPayment` for a Solana x402 client if the agent's wallet lives on Solana; the service accepts either and the tool code is unchanged. - **Discovery.** An agent that can read [`skill.md`](../skill.md) or `GET /.well-known/x402` can write this wrapper itself — which is, recursively, the entire point of the format.LICENSE11 KBd64569567334upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
llms-full.txt9.8 KBa2feb4777563upstream ↗
# x402-skill-md > The skill.md toolkit: generate and validate agent-discoverable skill files (the agentres.dev pattern) from OpenAPI specs. Defines the SKILL-MD format used across the x402 Suite. <h1 align="center">x402-skill-md</h1> <p align="center"> <b>The <code>skill.md</code> toolkit: the format, a generator, a validator.</b><br> A library, a CLI, and an x402-paid service. Defines the agent-facing contract file the whole suite ships. </p> <p align="center"> <a href="LICENSE"><img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a> <a href="https://x402.org"><img alt="x402" src="https://img.shields.io/badge/protocol-x402-0052ff.svg"></a> <img alt="rails" src="https://img.shields.io/badge/USDC-Base%20%2B%20Solana-2775ca.svg"> <img alt="format" src="https://img.shields.io/badge/SKILL--MD-1.0-6b46c1.svg"> <a href="https://nirholas.github.io/x402-skill-md/"><img alt="docs" src="https://img.shields.io/badge/docs-Pages-24292f.svg"></a> </p> --- ## What a `skill.md` is The file an AI agent reads to learn how to **use and pay for** an HTTP service: in one fetch, with no SDK and no human. It is Markdown, so a model can read it directly; it has a defined section grammar, so a parser can check it. OpenAPI already describes shapes. It does not describe **what a call costs, in what asset, on which chains, to which address**: and for an agent with a wallet and no account, that is the load-bearing information. **[Read the specification →](docs/spec.md)** ## Use it three ways ### As a CLI ```bash npx x402-skill-md generate openapi.json -o skill.md npx x402-skill-md validate skill.md npx x402-skill-md rules ``` ``` PASS score 100/100 (0 errors, 0 warnings, 0 infos) service: x402-domains endpoints: 2 rails: evm + solana No findings. This document is conformant. ``` `validate` exits 1 on errors, so it drops straight into CI: ```yaml - run: npx x402-skill-md validate skill.md --strict ``` ### As a library ```bash npm i x402-skill-md ``` ```ts import { openapiToSkillMd, validateSkillMd, RULES } from "x402-skill-md"; const { skillMd, warnings } = openapiToSkillMd( JSON.parse(await readFile("openapi.json", "utf8")), { baseUrl: "https://api.example.com", contact: "[email protected]" }, ); const report = validateSkillMd(skillMd); report.valid; // true report.service.dualRail; // true report.findings; // []: each finding carries rule, detail, fix, line ``` Zero runtime dependencies in the library path: the parser, validator and generator are plain TypeScript. ### As a paid service | Route | Price | What lands in the 200 body | |---|---|---| | `POST /generate` | **$0.01** | The generated `skill.md`, plus a validation report for it: one call gives you a file you can commit | | `POST /validate` | **$0.002** | The full report: every finding with its rule id, location, what is wrong *in this document*, and the exact fix | | `GET /rules` | free | The whole rule catalogue as JSON | | `GET /spec` | free | The format specification | | `GET /` · `/health` · `/skill.md` · `/.well-known/x402` · `/openapi.json` | free | Discovery | `GET /rules` is free on purpose: you should be able to see what you are being judged against before paying to be judged. ## Why x402 for this Validation is the definition of a spiky, per-artifact need: you check a `skill.md` when you write one, then not again for a month. A subscription is absurd for that, and so is an API key. But the library is also just free on npm, so the honest answer is: **the paid service exists for agents.** An agent that generated an OpenAPI document and wants a `skill.md` for it cannot `npm install` inside its own reasoning loop; it can make an HTTP request and pay a cent. Humans should use the CLI. ## The format in one paragraph Markdown with a defined section grammar: one H1, a summary paragraph, a service card with a base URL, a `## Payment` section documenting **both an EVM and a Solana rail**, one `` ## `METHOD /path`: $price `` section per endpoint with a parameter table and a real JSON response example, and a `## Errors` section that mentions the 402. ### Dual rail is mandatory, not optional A conformant `skill.md` documents an EVM rail **and** a Solana rail (rules SM005/SM006, both `error`). The reasoning: - An agent's wallet lives on exactly **one** chain. A single-rail service is invisible to every agent on the other one, and it cannot learn that cheaply. - The 402 challenge already carries an `accepts` **array**. Two entries cost one extra object; the client picks. There is no protocol reason to publish one. - Discovery indexers filter on rails. A single-rail listing halves your reachable market for no gain. The spec also requires the **facilitator to be declared per rail**, because facilitators are chain-specific: the reference `x402.org` facilitator settles Base Sepolia only and will not settle a Solana payment. A document that names one facilitator for both rails is telling clients something false. ## Rules 23 rules across three severities. `valid` is `true` when there are **zero errors**, independent of score; the score (100 − 12/error − 4/warning − 1/info) is a quality signal. | | count | meaning | |---|---|---| | `error` | 10 | not conformant: an agent may fail to use the service | | `warning` | 10 | conformant but lossy: an agent has to guess | | `info` | 3 | a suggestion | Every finding carries a fix, not just a verdict: ```json { "rule": "SM006", "severity": "error", "title": "No Solana payment rail documented", "detail": "There is no Payment section, so no Solana rail is documented.", "fix": "Name a Solana network (`solana` or `solana-devnet`) and its `payTo` address in the Payment section. Every conformant skill.md is dual-rail: an agent's wallet lives on one chain, and it must be able to tell from this file alone whether it can pay." } ``` Full catalogue: `npx x402-skill-md rules`, `GET /rules`, or [§9 of the spec](docs/spec.md). ## Generating from OpenAPI Pricing is read from, in order of preference: 1. `x-x402: { price: "$0.001" }` on the operation: the exact, declarative way 2. the `maxAmountRequired` in a 402 response example, converted from USDC base units 3. `options.defaultPrice` 4. otherwise, an operation with no 402 response is treated as **free** Anything guessed is reported in `warnings`. The generator never invents a price silently: a wrong price in a published contract is worse than a missing one. ```ts openapiToSkillMd(doc, { baseUrl: "https://api.example.com", // overrides servers[0].url contact: "[email protected]", // overrides info.contact.email defaultPrice: "$0.001", frontmatter: true, // emit the YAML block rails: [ /* defaults to the suite's dual-rail pair */ ], }); ``` ## Quickstart (the service) ```bash git clone https://github.com/nirholas/x402-skill-md.git cd x402-skill-md npm install cp .env.example .env # already filled with working defaults npm run dev # http://localhost:4029 ``` ```bash curl -i -s -X POST localhost:4029/validate -H 'content-type: text/markdown' -d '# x' # HTTP/1.1 402 Payment Required # { "x402Version": 1, "accepts": [ {…base-sepolia…}, {…solana…} ] } PRIVATE_KEY=0xyourTestnetKey npm run client ``` ## Dual-rail payment: Base **or** Solana Every 402 lists **two** payment requirements. The client picks whichever chain it can sign on; the server settles either through that rail's facilitator and returns the same artifact. | | EVM rail | Solana rail | |---|---|---| | network | `base-sepolia` (default) / `base` | `solana` (default) / `solana-devnet` | | asset | USDC `0x036CbD…dCF7e` (sepolia) | USDC mint `EPjFWdd5…TDt1v` | | payTo | `0x40252CFDF8B20Ed757D61ff157719F33Ec332402` | `WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW` | | facilitator | `https://x402.org/facilitator` | `https://facilitator.payai.network` | Those are the suite's public receive addresses and the server runs with them out of the box. Set `PAY_TO_ADDRESS` / `SOLANA_PAY_TO_ADDRESS` to be paid yourself. The Solana network fee is sponsored by the facilitator's fee payer, so a payer needs USDC only: no SOL. ## Real backend / API keys **None.** Generation and validation are pure functions of what you post: no upstream API, no key, no fixture mode, and nothing you send is stored. The same input always produces the same output, which is also why the library and the service cannot disagree. ## For AI agents - **[`skill.md`](skill.md)**: this service's own contract file, and a worked example of the format. It scores 100/100 against its own validator, which is the least it could do. - **`GET /.well-known/x402`**: machine-readable manifest of both paid resources with input and output schemas and both rails. - **`GET /rules`**: the catalogue, free, so an agent can self-check before paying. - **MCP**: [`examples/mcp-tool.md`](examples/mcp-tool.md) exposes `generate_skill_md` and `validate_skill_md` as Model Context Protocol tools. - **Client**: [`examples/agent-client.ts`](examples/agent-client.ts) runs the full flow: rules → 402 → generate → validate a deliberately broken file to show the fixes. ## Docs Full site: **<https://nirholas.github.io/x402-skill-md/>** - **[Specification](docs/spec.md)**: the format itself, normative - [Tutorial](docs/tutorial.md): CLI → library → paid service → CI - [API reference](docs/api.md): every field of every response - [For agents](docs/agents.md): discovery, payment, MCP, listing ## Support Questions, bugs, or a spec correction: **[email protected]** or open an issue. Part of the [x402 Suite](https://github.com/nirholas/x402-suite). ## License Apache-2.0: see [LICENSE](LICENSE). ## Star History [](https://www.star-history.com/#nirholas/x402-skill-md&Date)llms.txt1.8 KBb7224019771bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
openapi.json22 KBa62b2ca67337upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
package-lock.json287 KBcfe7223ae312upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
package.json1.5 KB89c4b5003f44upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
public/.well-known/x4026.9 KBce4e39bb5bb3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
public/favicon.svg423 B61d08fcdcc6fupstream ↗
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" role="img" aria-label="x402"> <defs> <linearGradient id="g" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#0052ff"/> <stop offset="1" stop-color="#14a37f"/> </linearGradient> </defs> <rect width="100" height="100" rx="22" fill="url(#g)"/> <path d="M56 16 L74 16 L74 84 L56 84 L56 68 L20 68 L20 52 Z" fill="#fff"/> </svg>public/index.html8.1 KBbef5a11ec6d2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
public/og.png35 KB40e3e4381e69upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
public/openapi.json22 KBa62b2ca67337upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
SECURITY.md676 B0d07c8d8a91bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/cli.ts4.7 KB64ad519559a4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/generate.ts16 KB1f0ea5223cd2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/index.ts1.2 KB90e19e2754d2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/parse.ts6.0 KB6ee88bc05f7eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/payments.ts16 KB16aa23085fefupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/schemas.ts8.4 KB87debac34d89upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/server.ts6.5 KB9126f323144aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/spec.ts8.5 KB6a0fe1d9cd27upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
src/validate.ts9.9 KBc00bda47539fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tsconfig.json382 B66c707edd738upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/nirholas-x402-skill-md.md