# x402-skill-md

Adopt this skill: https://proskills.md/skills/other/nirholas-x402-skill-md

Page: https://proskills.md/skills/other/nirholas-x402-skill-md
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skill.md
size: 9314
sha: 88382244f732b6e71b9f85e85f2decec7c3bcb60
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/skill.md

````text
---
skillMd: "1.0"
name: x402-skill-md
baseUrl: http://localhost:4029
manifest: /.well-known/x402
openapi: /openapi.json
contact: nichxbt@gmail.com
---

# 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:** nichxbt@gmail.com

## 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": "nichxbt@gmail.com",
    "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.
````

## File: README.md
size: 12159
sha: 22c46a9435ef5a3548c77e5fd9362c9296b51391
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/README.md

````text
<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: "ops@example.com" },
);

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: "ops@example.com",           // 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: **nichxbt@gmail.com** or open an issue.

Part of the [x402 Suite](https://github.com/nirholas/x402-suite).

## License

Apache-2.0 — see [LICENSE](LICENSE).

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=nirholas/x402-skill-md&type=Date)](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

[![Contributors](https://contrib.rocks/image?repo=nirholas/x402-skill-md)](https://github.com/nirholas/x402-skill-md/graphs/contributors)

<!-- /three.ws:growth -->
````

## File: .env.example
size: 2944
sha: b561b36ca3b4533155cf086264f72a80585f48e4
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.env.example

```text
# ── 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
```

## File: .github/FUNDING.yml
size: 106
sha: fda9433edd19ca513620f277c658bcb6cbea0f41
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.github/FUNDING.yml

```text
# Adds a Sponsor button to the repo. Links to the product, not to any token.
custom:
  - https://three.ws
```

## File: .github/ISSUE_TEMPLATE/bug.yml
size: 613
sha: 3d2ff4483e49d91c11d20c6640a625fde178ab54
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.github/ISSUE_TEMPLATE/bug.yml

```text
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
```

## File: .github/ISSUE_TEMPLATE/config.yml
size: 494
sha: 284a4bbba4b7a5b76986803c2ab5539da4d95331
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.github/ISSUE_TEMPLATE/config.yml

```text
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.
```

## File: .github/ISSUE_TEMPLATE/feature.yml
size: 325
sha: d7d4f2974d3beee55bfdc1d7b8cefe896584a240
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.github/ISSUE_TEMPLATE/feature.yml

```text
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?
```

## File: .github/PULL_REQUEST_TEMPLATE.md
size: 153
sha: fcbde8255921b207ed420e7a1098c18ebd7a64d5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.github/PULL_REQUEST_TEMPLATE.md

```text
## What changed and why

## How it was checked

- [ ] Tests pass locally (or the change is documentation only)
- [ ] Docs updated where behavior changed
```

## File: .gitignore
size: 43
sha: 8e2ff106943230c68547c4ad44b7e57aff8a467c
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/.gitignore

```text
node_modules/
dist/
.env
*.log
data/*.json
```

## File: AGENTS.md
size: 2194
sha: b1e17184ea7c3edddeabbc9ae18fe07b9e72a257
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/AGENTS.md

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

## File: CITATION.cff
size: 457
sha: 3ae455d5209fce928bc3e9181bf3b940e657a3fb
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/CITATION.cff

```text
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"
```

## File: CLAUDE.md
size: 11
sha: 43c994c2d3617f947bcb5adf1933e21dabe46bb5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/CLAUDE.md

```text
@AGENTS.md
```

## File: CODE_OF_CONDUCT.md
size: 453
sha: 4954bc6c109490256c9b136823f1f219b4086a17
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/CODE_OF_CONDUCT.md

```text
# 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 support@three.ws. Reports are handled in confidence.
```

## File: CONTRIBUTING.md
size: 1146
sha: bddcf037cef6792ef5a5a74515f0a2190ed80f87
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/CONTRIBUTING.md

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

## File: docs/.nojekyll
size: 0
sha: e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/.nojekyll

```text

```

## File: docs/agents.md
size: 8730
sha: a48d8142c3c013cd85ae813d8261d12a68d8fc2f
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/agents.md

````text
# 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: **nichxbt@gmail.com**.
````

## File: docs/api.md
size: 10968
sha: c58b2c988c6537f0e56fa418f0c4409a56fbb78b
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/api.md

````text
# 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": "ops@example.com" }
}
```

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

## File: docs/favicon.svg
size: 423
sha: 61d08fcdcc6f94adc58bb48f5f2ec7c516c148a5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/favicon.svg

```text
<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>
```

## File: docs/index.html
size: 14379
sha: 09596613e04b234f90b6314e117eeb133ef28210
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/index.html

```text
<!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:nichxbt@gmail.com">nichxbt@gmail.com</a></p>
</footer>
</body>
</html>
```

## File: docs/og.png
size: 35411
sha: 40e3e4381e692ca0bf0b5b30f05a5fbdc7afeb32
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/og.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/og.png

## File: docs/spec.md
size: 24525
sha: 722329e3e5b67e16aac27c729388803256ab7b49
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/spec.md

`````text
# 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: nichxbt@gmail.com
---
```

| 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:** ops@example.com

## 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 nichxbt@gmail.com*
`````

## File: docs/tutorial.md
size: 10353
sha: 66c9b69023abd19d6adc056e1d28256432da73f9
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/docs/tutorial.md

````text
# 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 ops@example.com
```

```
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: "ops@example.com",
  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 hand
````

## File: examples/agent-client.ts
size: 8022
sha: 1387e225770eb95cf5cd2518dfea07719bd87595
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/examples/agent-client.ts

```text
/**
 * 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: "ops@example.com" },
  },
  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
 */
```

## File: examples/curl.md
size: 7248
sha: af4ccfbe22a71c1bb01f0055b0813c904cb0c5ce
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/examples/curl.md

````text
# 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 |
````

## File: examples/mcp-tool.md
size: 5540
sha: 315c1335553beb6a00a8b7faf36f7309e503ac2f
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/examples/mcp-tool.md

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

## File: LICENSE
size: 11358
sha: d645695673349e3947e8e5ae42332d0ac3164cd7
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/LICENSE

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/LICENSE

## File: llms-full.txt
size: 9990
sha: a2feb477756343bb82b7082c2305e54cac15ab09
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/llms-full.txt

````text
# 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: "ops@example.com" },
);

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: "ops@example.com",           // 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: **nichxbt@gmail.com** or open an issue.

Part of the [x402 Suite](https://github.com/nirholas/x402-suite).

## License

Apache-2.0: see [LICENSE](LICENSE).

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=nirholas/x402-skill-md&type=Date)](https://www.star-history.com/#nirholas/x402-skill-md&Date)
````

## File: llms.txt
size: 1885
sha: b7224019771b3bd17ebd0f7855c77dfa7caef463
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/llms.txt

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/llms.txt

## File: openapi.json
size: 22965
sha: a62b2ca673377ad2e8720666f5c87f0403f599a1
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/openapi.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/openapi.json

## File: package-lock.json
size: 293409
sha: cfe7223ae312179e0df7a88d6cd56e2d9c97fd20
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/package-lock.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/package-lock.json

## File: package.json
size: 1569
sha: 89c4b5003f449c3f47fbc30d07345db6ab0df9e5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/package.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/package.json

## File: public/.well-known/x402
size: 7096
sha: ce4e39bb5bb360f68677818ae644e89d2892a9fe
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/.well-known/x402

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/.well-known/x402

## File: public/favicon.svg
size: 423
sha: 61d08fcdcc6f94adc58bb48f5f2ec7c516c148a5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/favicon.svg

```text
<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>
```

## File: public/index.html
size: 8302
sha: bef5a11ec6d274c5e0babd8c08ca408fce8d2bcf
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/index.html

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/index.html

## File: public/og.png
size: 35411
sha: 40e3e4381e692ca0bf0b5b30f05a5fbdc7afeb32
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/og.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/og.png

## File: public/openapi.json
size: 22965
sha: a62b2ca673377ad2e8720666f5c87f0403f599a1
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/openapi.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/public/openapi.json

## File: SECURITY.md
size: 676
sha: 0d07c8d8a91b4a2e0eec6eddb3a9f24ed615dc3f
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/SECURITY.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/SECURITY.md

## File: src/cli.ts
size: 4843
sha: 64ad519559a42b3514d9292a70db81b359101748
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/cli.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/cli.ts

## File: src/generate.ts
size: 16238
sha: 1f0ea5223cd270aac4f50243ffa9538d17da5172
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/generate.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/generate.ts

## File: src/index.ts
size: 1279
sha: 90e19e2754d294cf20c2c8503196e6674c95104d
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/index.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/index.ts

## File: src/parse.ts
size: 6174
sha: 6ee88bc05f7e6799bde4c06c49fd1bb2f4348b0d
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/parse.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/parse.ts

## File: src/payments.ts
size: 16239
sha: 16aa23085fef185d731924398c414d2d5bbd22bb
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/payments.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/payments.ts

## File: src/schemas.ts
size: 8642
sha: 87debac34d89c7488daded3811a64a4c4d07b40a
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/schemas.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/schemas.ts

## File: src/server.ts
size: 6656
sha: 9126f323144a18bd0d0a84349d18bb146ff7e5d5
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/server.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/server.ts

## File: src/spec.ts
size: 8747
sha: 6a0fe1d9cd2798be68b6b9e20e3af2fd3d1db5dc
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/spec.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/spec.ts

## File: src/validate.ts
size: 10155
sha: c00bda47539f47b7a12845ac2ce47483d7aad7c7
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/validate.ts

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/src/validate.ts

## File: tsconfig.json
size: 382
sha: 66c707edd738d158aeecc938abe68d776120fb78
upstream: https://github.com/nirholas/x402-skill-md/blob/HEAD/tsconfig.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/nirholas/x402-skill-md/blob/HEAD/tsconfig.json
