Metadata-Version: 2.4
Name: x402-conformance
Version: 1.4.0
Summary: Experimental conformance and safety checks for x402 facilitators and resource servers: GET /supported checks, 402 answer checks and fault injection
Author: Ryan Gillespie / Optitransfer
License: Apache-2.0
Project-URL: Homepage, https://atsp-protocol.github.io/x402-conformance/
Project-URL: Repository, https://github.com/atsp-protocol/x402-conformance
Project-URL: Documentation, https://github.com/atsp-protocol/x402-conformance/blob/main/docs/SPEC.md
Project-URL: Changelog, https://github.com/atsp-protocol/x402-conformance/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/atsp-protocol/x402-conformance/issues
Keywords: x402,http-402,pytest,pytest-plugin,fastapi,flask,agentic-payments,ai-agents,micropayments,stablecoin,usdc,solana,evm,conformance,fault-injection,ci,sarif
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Framework :: Pytest
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Provides-Extra: pytest
Requires-Dist: pytest>=7; extra == "pytest"
Dynamic: license-file

# x402 Conformance

**Prove your x402 payments can't double-credit, credit too early or credit on a broken answer, in one CI step.**

[![ci](https://github.com/atsp-protocol/x402-conformance/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/atsp-protocol/x402-conformance/actions/workflows/ci.yml)
[![release](https://img.shields.io/github/v/release/atsp-protocol/x402-conformance)](https://github.com/atsp-protocol/x402-conformance/releases/latest)
[![PyPI](https://img.shields.io/pypi/v/x402-conformance?label=PyPI)](https://pypi.org/project/x402-conformance/)
[![npm](https://img.shields.io/npm/v/x402-conformance-svm?label=npm)](https://www.npmjs.com/package/x402-conformance-svm)
[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-x402%20Conformance-blue?logo=github)](https://github.com/marketplace/actions/x402-conformance)
[![licence](https://img.shields.io/github/license/atsp-protocol/x402-conformance)](LICENSE)
[![Tested with x402-conformance](https://img.shields.io/badge/tested%20with-x402--conformance-2ea44f)](https://github.com/atsp-protocol/x402-conformance)

An open set of conformance and safety checks for [x402](https://github.com/x402-foundation/x402) facilitators and
resource servers. It checks the properties a seller's money depends on. It never signs, pays or touches mainnet. It
complements x402 and its facilitators and doesn't replace them.

**Experimental.** Passing a check is evidence about one property. It doesn't certify conformance to x402.
— Ryan Gillespie, Optitransfer

Project page with the checks and the latest results: https://atsp-protocol.github.io/x402-conformance/

## 60-second quickstart

> **Before 1.4.0 is published:** this page describes `main`. The released 1.3.1 packages and the `@v1` Action already
> have `live`, `resource`, `offline`, `report`, `devnet`, `probe --resource` and the Action inputs `facilitators`,
> `facilitator-auth-host`, `resources`, `resource-url`, `start`, `network`, `port`, `offline`, `results-dir` and
> `fail-on-error`. Everything else here ships in 1.4.0: both `demo` commands, R11 and the `binding-*` inputs, F11, P6,
> P7, `pr-comment`, `report --badge`, `--share`, `x402conf-svm resource`, the test helpers, the MCP server, the Docker
> image and the GitLab template. Until then, run those from a clone (`cd ts && npm ci && npm run build && node
> dist/src/cli.js demo`, or `PYTHONPATH=src python3 -m x402conf demo`). [CHANGELOG.md](CHANGELOG.md) lists what is
> unreleased.

See it work first, with nothing to set up: no project, no wallet, no chain, a few seconds each.

```
npx -y x402-conformance-svm demo                       # R0-R10 against a bundled mock server behind a facilitator double
pipx run --spec x402-conformance x402conf demo         # S1-S4, P1-P7 against a bundled mock, then the F1-F11 self-test
npx -y x402-conformance-svm demo --fault serve_on_any_settle   # what a failure looks like, with a fix link per check
```

Then point it at your own code:

```
pipx run --spec x402-conformance x402conf live https://x402.org/facilitator        # a facilitator's GET /supported
pipx run --spec x402-conformance x402conf resource https://your.api/paid-route      # your 402 answer, one unpaid GET
npx -y x402-conformance-svm probe --resource http://127.0.0.1:4020/paid --start "node server.js"   # fault injection
```

In CI, save this as `.github/workflows/x402-conformance.yml`:

```yaml
name: x402 conformance
on: [push, pull_request]
jobs:
  x402:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: atsp-protocol/x402-conformance@v1
        with:
          resource-url: http://127.0.0.1:4020/paid   # your paid route
          start: node server.js                      # must use X402_FACILITATOR_URL as its facilitator
```

**From your test suite, agent or CI system:** [docs/USING.md](docs/USING.md) covers vitest, jest, node:test, Hono,
Express, MCP clients (Claude, Cursor), the Vercel AI SDK, LangChain, Docker and GitLab CI, with the same checks and
report everywhere. Failures you choose to share build the public
[corpus](corpus) of real x402 failures.

## Who should use this

- **Resource-server builders** selling an API over x402. The probe puts a misbehaving facilitator in front of your paid
  route and fails your build if you would serve content on a `/settle` timeout, a broken answer, `settlement_pending`,
  an on-chain failure or a duplicate payment. `resource` checks that your 402 answer is well formed on any network.
- **Facilitator operators.** `live` checks your public `GET /supported` (shape, CAIP-2 IDs, Solana fee payers) with one
  GET, and `devnet` runs the safety suite over real `/verify` and `/settle` against a facilitator you run.
- **SDK and middleware maintainers.** Run the probe against your framework's example server in CI. The repo's own CI
  runs it against the stock `@x402/express` middleware on Solana devnet and Base Sepolia.

## What it catches

**Facilitators, live, read-only (`x402conf live`, one `GET /supported` each):**
- **S0** the endpoint doesn't answer HTTP 200 with a JSON object (unreachable, an error page, or credentials needed)
- **S1** the response is missing `kinds`, `extensions` or `signers` (a V1-only facilitator is graded against V1, which defines `kinds` only)
- **S2** a V2 network isn't written as a CAIP-2 ID
- **S3** a Solana kind uses an old V1 name such as `solana-devnet` instead of the published ID
- **S4** a Solana `exact` kind has no valid fee payer (error), or its fee payer isn't listed under `signers` (warning:
  the spec doesn't say it must be, I infer it from what `signers` is for)

**Any paid URL, read-only (`x402conf resource`, one unpaid `GET` each), any network, V1 or V2:**
- **P1** an unpaid request isn't answered with HTTP 402
- **P2** there's no `PAYMENT-REQUIRED` header (V2) or V1 JSON body, or it doesn't decode
- **P3** `x402Version`, `resource` or a non-empty `accepts` is missing
- **P4** an `accepts` entry lacks a required field or has the wrong type (amount in atomic units, CAIP-2 network, timeout)
- **P5** a scheme-specific field is missing: `extra.name` and `extra.version` for EVM `exact` (EIP-3009),
  `extra.feePayer` for Solana `exact`
- **P6** the `PAYMENT-REQUIRED` header is over 8 KB (warning) or 16 KB (error): common proxies and servers reject
  headers that large
- **P7** an offer whose scheme defaults to a non-authorization flow (`upto` on Solana) doesn't declare
  `extra.paymentFlow` (warning; V2 §6.1 says it must, the upto SVM scheme text says it may be omitted)

**Your resource server, under fault injection (`x402conf-svm probe`, R0-R11, R14 and R20):** the probe starts a facilitator double
on loopback, points your server at it through `X402_FACILITATOR_URL` and pays your route with test payloads on
**Solana devnet or Base Sepolia (`exact`; EVM via EIP-3009)**. Your server must:
- **R0** answer an unpaid request with a 402 offering one of those networks. If it offers neither, the result is
  **unsupported**, not failed: nothing was graded
- **R1** serve a settled payment, and use the double (if it serves without calling the double, the probe stops: the
  remaining checks would mean nothing)
- **R2** serve the same payment at most once
- **R3-R9** not serve when `/settle` never answers, answers an HTML 502, a 503 with a success body, a non-JSON page, a
  truncated body, `settlement_pending` or `invalid_transaction_state`
- **R10** send a `PAYMENT-RESPONSE` header that decodes to `success: true` with the content
- **R11** bind each payment to the request it pays for (give the routes with the binding options): a payment for a
  cheap route is refused on a dearer one (R11a), an altered `payTo`, asset or network is refused before `/settle`
  (R11b), a payment for the cheaper method (R11c) or query (R11d) is refused on the dearer one, an encoded separator
  or dot segment in a parameter never reaches a paid handler unpaid (R11e), and sign-in-with-x access is recorded for
  the route that was priced and settled, not the client's `resource.url` (R11f)
- **R14** survive a crash between a successful `/settle` and the response (add `--crash`): the probe kills your server
  while `/settle` is answering and again as the response starts, restarts it and presents the same payment. It must
  not settle again or ask for payment again, and must either deliver once (from an idempotency record) or refuse and
  name the transaction or payment id so it can be reconciled. With `--start` the probe restarts that command; for a
  server it didn't start, give `--crash-kill` and `--crash-restart`
- **R20** with `--retry`, never charge one request twice when the client times out before `/settle` answers and
  retries: a retry with a fresh payment under the same payment-identifier id is not settled again (R20a), and a retry
  resending the same signed payment is refused as a duplicate by `/settle`; both get the earlier result (R20b). The
  same check grades a client's own retry (`checkClientRetry`): it must resend the payment or keep the payment id

Every result carries its **profile** (`normative`: the spec says MUST; `security-profile`: a hardening rule the spec
leaves open; `resilience`: behaviour under failure), its spec **basis** and a **fix** link (results format
`x402-conformance-evidence/2`).

**Safety suite (`x402conf offline` and `devnet`, F1-F11):** a malformed payment accepted (F1), an underpayment accepted
(F2), a payment settled twice (F3), a memo or nonce replayed (F4), success before confirmation, or `settlement_pending` without a transaction and network (F5), a timeout or
garbage answer credited (F6), a pending payment never finalised or credited twice (F7), a failure stuck pending (F8),
a broken `/settle` transport credited (F9), a payment accepted under requirements it wasn't made for (F10),
`/verify` committing payment state (F11). `offline` runs them against built-in doubles: it's a self-test of the suite
and doesn't test your code. `devnet` runs F1-F6 over real `/verify` and `/settle` on Solana devnet.

[docs/SPEC.md](docs/SPEC.md) quotes the spec text behind every check, and [docs/FIXES.md](docs/FIXES.md) explains, for
every check, what failed, why it matters and how to fix it.

## Install

| how | command | needs |
|---|---|---|
| GitHub Action | `uses: atsp-protocol/x402-conformance@v1` (see [below](#github-action)) | python3 3.10+ on the runner (GitHub-hosted runners have it) |
| Python CLI, one-off | `pipx run --spec x402-conformance x402conf --help` or `uvx --from x402-conformance x402conf --help` | [pipx](https://pipx.pypa.io) or [uv](https://docs.astral.sh/uv/) |
| Python CLI, installed | `pip install x402-conformance` | Python 3.10+, no dependencies |
| TypeScript probe | `npx -y x402-conformance-svm --help` | Node 20+ |

Packages: [x402-conformance on PyPI](https://pypi.org/project/x402-conformance/) (command `x402conf`) and
[x402-conformance-svm on npm](https://www.npmjs.com/package/x402-conformance-svm) (command `x402conf-svm`; the name is
historical, it grades Base Sepolia too). `x402conf --version` and `x402conf-svm --version` print the version. Pin
`x402-conformance==1.4.0`, `x402-conformance-svm@1.4.0` or the Action at `@v1.4.0` if you want a fixed version; `@v1`
follows the latest 1.x release.

## CLI

```
x402conf demo [--fault no_fee_payer|no_402]           # a bundled mock on loopback, then the self-test
x402conf live https://x402.org/facilitator             # S0-S4, GET /supported only
x402conf resource https://api.example.com/paid         # P1-P7, one unpaid GET
x402conf offline                                       # F1-F11 self-test against the built-in doubles
x402conf report --dir DIR --sarif out.sarif --junit junit.xml --badge badge.json
x402conf pr-comment DIR/pr-comment.md                  # sticky PR comment (GITHUB_TOKEN, in a pull_request run)
x402conf devnet --faults --keys DIR                    # F1-F6 over real /verify and /settle on Solana devnet
x402conf-svm demo [--fault serve_on_any_settle]       # R0-R10 against a bundled mock server
x402conf-svm probe --resource http://127.0.0.1:4020/paid --start "node server.js" --out resource-server.json
x402conf-svm probe --resource http://127.0.0.1:4020/paid --start "node server.js" --crash   # R0-R10, then R14
```

`live`, `resource` and `offline` print JSON with one entry per check (`check`, `pass`, `severity`, `detail`). Save them
as `live.json`, `resource-402.json` and `offline.json` (and the probe's `--out` as `resource-server.json`) in one
directory, and `report` converts them to SARIF and JUnit, printing a one-line summary such as
`{"checks": 14, "failed": 0, "failed_ids": []}`. Add `--fail-on-error` to `live`, `resource` or `report` to exit 1 when
an error-severity check fails; without it they exit 0 so existing scripts keep working. Warnings never fail a run.

`devnet` runs against a self-hosted reference facilitator on loopback with throwaway keys from `--keys`, and refuses any
other facilitator (`--facilitator URL`) unless you pass `--operator-permission` with the operator's written permission.
`--rpc` picks the devnet RPC (default `https://api.devnet.solana.com`). It needs Node 20+ and a funded devnet fee payer;
[evidence/devnet](evidence/devnet) shows a full run. `live --evidence DIR` and `devnet --evidence DIR` also write the raw
bodies, `run.json` and `SHA256SUMS` to `DIR`.

**Facilitators that need credentials** (Coinbase CDP, for example): put the full `Authorization` header value in
`X402C_FACILITATOR_AUTHORIZATION` and the facilitator's host name in `X402C_FACILITATOR_AUTH_HOST`. The header goes to
that host only, never to the others in the same run, and is never printed or written to evidence. A credential without
a host is a usage error. CDP uses short-lived JWTs, so mint one just before the run.

**Exit codes.**

| command | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| `x402conf live`, `x402conf resource` | done (with `--fail-on-error`: no error-severity check failed) | with `--fail-on-error`, an error-severity check failed, S0 included | usage or configuration error | – |
| `x402conf offline` | the reference passes every check and every fault-injected double is caught | otherwise | usage error | – |
| `x402conf report` | reports written (with `--fail-on-error`: and nothing failed) | with `--fail-on-error`, an error-severity check failed | usage error, or no results JSON in the directory | – |
| `x402conf devnet` | the facilitator under test passes, and with `--faults` every fault-injected build is caught | otherwise (the current reference exits `1` on F4, see [Results](#results)) | usage error | – |
| `x402conf-svm probe` | every requested check (R0-R10, R11, R14) passes | a check failed | usage error, or the run couldn't complete: a port in use, your `--start` server exited or never answered | unsupported: your server offers no network the probe grades |
| `x402conf --help`, `--version`, `x402conf-svm --help`, `--version` | always | – | – | – |

With `--out`, the probe writes its results file on every outcome, errors included.

## GitHub Action

The [quickstart](#60-second-quickstart) workflow runs as is. Inputs:

| input | default | what it does |
|---|---|---|
| `facilitators` | empty | Space-separated facilitator base URLs, checked with `GET /supported` only (S0-S4) |
| `facilitator-auth-host` | empty | The one host that gets `X402C_FACILITATOR_AUTHORIZATION` (set it in the step's `env` from a secret) |
| `resources` | empty | Space-separated paid URLs, one unpaid `GET` each (P1-P7) |
| `resource-url` | empty | Your paid route; the Action puts a facilitator double in front of it and runs R0-R10 |
| `binding-cheap-url`, `binding-expensive-url` | empty | Two paid routes at different prices (R11, R11a, R11b) |
| `binding-method-url` | empty | A paid URL priced differently for `GET` and `POST` (R11c) |
| `binding-query-urls` | empty | Two paid URLs that differ only in the query, comma-separated (R11d) |
| `binding-param-url` | empty | A paid parameterised route with `{id}` for the parameter (R11e) |
| `binding-siwx-urls` | empty | Two paid sign-in-with-x routes, cheap first, comma-separated, Base Sepolia (R11f) |
| `crash` | `false` | `true` also runs R14: the probe kills your server after `/settle` succeeds and before the response, restarts it and presents the same payment (needs `resource-url`, and `start` or the two commands below) |
| `crash-kill`, `crash-restart` | empty | For a server `start` didn't launch: a command that kills it hard and returns once it's gone, and one that starts it again in the background (R14) |
| `retry` | `false` | `true` also checks R20 retry double-pay on `resource-url` (`--retry`) |
| `start` | empty | Command that starts your server; it receives `X402_FACILITATOR_URL` |
| `network` | `auto` | Which offered network the probe pays on: `auto`, `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` or `eip155:84532` |
| `port` | `4402` | Loopback port for the facilitator double |
| `offline` | `true` | Also run the F1-F11 self-test of the suite (it tests the suite, not your code; set `false` to skip) |
| `results-dir` | `x402-conformance` | Where `live.json`, `resource-402.json`, `resource-server.json` and `offline.json` are written |
| `fail-on-error` | `true` | Fail the job when a check fails, a requested run left no results, or the probe found nothing to grade |
| `pr-comment` | `false` | `true` posts one sticky pull request comment with the result and a fix link per failed check (needs `permissions: { pull-requests: write }`) |
| `github-token` | the job token | Token used for `pr-comment`; the default job token is enough |

| output | value |
|---|---|
| `result` | `pass` when every requested check passed, `unsupported` when the probe found nothing it could grade, otherwise `fail` |
| `results-dir` | the directory holding the results JSON |
| `badge` | `<results-dir>/badge.json`, a shields.io endpoint badge |
| `pr-comment` | `<results-dir>/pr-comment.md`, the sticky comment body |
| `sarif` | `<results-dir>/x402-conformance.sarif`, SARIF 2.1.0 of the failed checks |
| `junit` | `<results-dir>/junit.xml`, one test case per check |

**It fails closed.** If the probe can't run (its port is taken, your server exits or never answers, the build fails),
the job fails with the reason; a run that left no results is never reported as a pass. **It leaves your toolchain
alone.** It doesn't run `setup-python` or `setup-node`: the Python checks use the runner's `python3`, and the probe runs
on a pinned, checksum-verified Node in `RUNNER_TEMP` that is never put on your `PATH`, so your server and later steps
keep your Node.

**Example job summary** (abridged):

```
## x402 Conformance: pass
| target                          | check                                                          | result |
| http://127.0.0.1:4020/paid      | R1 a settled payment is served, and the server used the facilitator double | pass |
| http://127.0.0.1:4020/paid      | R3 /settle never answers: content not served (F6)              | pass   |
| https://x402.org/facilitator    | S1 SupportedResponse shape: kinds[], extensions[], signers{} ... | pass |
```

**Failures in code scanning.** Every failed check can show up in your repo's Security tab, with a link to what failed
and how to fix it. Add `permissions: { security-events: write, contents: read }` to the job, `id: x402` and
`fail-on-error: false` to the step above, then:

```yaml
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: ${{ steps.x402.outputs.sarif }}
          category: x402-conformance
      - run: test "${{ steps.x402.outputs.result }}" = pass
```

The `junit` output works with any JUnit test-report viewer.

**A comment on the pull request.** Set `pr-comment: "true"` and give the job `permissions: { pull-requests: write }`.
The action posts one comment with the result and a fix link for each failed check, and updates that same comment on
every push instead of adding new ones. It's off by default; on a fork's pull request the token can't comment, so it
only warns.

## Results

The full results, with raw bodies and hashes, are on the [project page](https://atsp-protocol.github.io/x402-conformance/#results).

### Live, 2026-10-20, `GET /supported` only, one request per facilitator

| facilitator | HTTP | S0 | S1 | S2 | S3 | S4 | note |
|---|---|---|---|---|---|---|---|
| x402.org/facilitator | 200 | pass | pass | pass | pass | pass | |
| facilitator.payai.network | 200 | pass | pass | pass | pass | pass | |
| x402.dexter.cash | 200 | pass | pass | pass | pass | pass | |
| facilitator.ultravioletadao.xyz | 200 | pass | pass | pass | pass | warning | devnet fee payer not listed under `signers` (inferred rule) |
| facilitator-A (name withheld) | 200 | pass | pass | pass | pass | pass | |
| facilitator.xpay.sh | 200 | pass | pass | pass | pass | pass | |
| x402.polygon.technology | 200 | pass | pass | pass | pass | pass | |
| facilitator.goplausible.xyz | 200 | pass | pass | **fail** | pass | pass | Algorand IDs use the full 44-character genesis hash; the x402 Algorand spec uses `algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k` |
| facilitator.pieverse.io | 200 | pass | pass | pass | pass | pass | |
| facilitator.x402.rs | 200 | pass | pass | pass | pass | pass | |
| facilitator.daydreams.systems | 200 | pass | pass | pass | pass | pass | |
| facilitator.mogami.tech | 200 | pass | pass | pass | pass | pass | V1-only, graded against V1 |
| api.cdp.coinbase.com/platform/v2/x402 | 401 | **fail** | – | – | – | – | needs credentials; not run with any |
| facilitator.cascade.fyi | – | **fail** | – | – | – | – | TLS handshake failed |
| facilitator.corbits.dev | – | **fail** | – | – | – | – | TLS handshake failed |

Raw bodies, `run.json` and `SHA256SUMS` for this run are in [evidence/2026-10-20-v1.3.0](evidence/2026-10-20-v1.3.0). A
failed check is evidence about one property on one day, not a verdict on a facilitator; operators can
[report a false positive](https://github.com/atsp-protocol/x402-conformance/issues/new?template=false-positive.yml).

The first run, with 1.2.0, is in [evidence/2026-10-20](evidence/2026-10-20).
Want your facilitator listed? [Open a request](https://github.com/atsp-protocol/x402-conformance/issues/new?template=add-facilitator.yml).

### Devnet, 2026-10-20, F1-F6 over real `/verify` and `/settle`

I ran F1 to F6 on Solana devnet against my self-hosted reference facilitator (`@x402/core` and `@x402/svm` 2.28.0),
then against fault-injected builds of it.

| facilitator | F1 | F2 | F3 | F4 | F5 | F6 |
|---|---|---|---|---|---|---|
| self-hosted reference, `@x402/svm` 2.28.0 | pass | pass | pass | **fail** | pass | pass |

F4 fails because the reference facilitator deduplicates by transaction but not by memo, so two different
transactions with the same memo both settled. The payer paid twice, so the seller lost nothing, but a resource server
that ties one purchase to one memo has to enforce that itself. Every fault-injected build was caught by the check
it targets. Transaction links, the full `run.json` and its hash are in [evidence/devnet](evidence/devnet). Results for
other facilitators go in the [facilitator matrix](docs/MATRIX.md), and I run them against a third party only with the
operator's permission.

## Roadmap

Next is depth, not breadth. v1.4 tests that a payment for one route, price, recipient or method never unlocks another,
on a real resource server rather than only on the facilitator double, and makes the suite's own oracle fail closed on
malformed amounts. v1.5 adds parallel and multi-replica replay, a crash between settling and serving, and payment
versus delivery (failed handlers, `HEAD`, caches, late settlement). Every check is labelled normative, security profile
or resilience, and every check ships with a passing control and a faulty build it catches. Alongside the checks, an
adoption track makes the suite quick to run and keep: a one-line demo against a local mock target in under 60 seconds
with no wallet, a 3-line Action with a pull-request summary, fix hints on every failure, framework quickstarts, a
public regression corpus anyone can add a case to, and a compatibility matrix rebuilt in CI. The plan and the release
gate for dropping the experimental label are in [docs/ROADMAP.md](docs/ROADMAP.md).

## How this repo is verified

Scope and acceptance criteria live in [`proofline.json`](proofline.json), a [Proofline](https://github.com/Optitransfer/proofline)
contract. It's signed with my owner key: the signature is in `proofline.json.sig` and the public key is pinned in
[`guard/owner.pub`](guard/owner.pub). On every push to `main` and on every pull request, CI checks that signature and then runs `tools/vv.py`, which checks
every acceptance criterion and uploads a receipt ([docs/VV.md](docs/VV.md)). Any change to the contract has to be
re-signed before CI passes again. You can check the signature yourself from a clone:

```
node tools/verify-sig.mjs                                       # no dependencies; exits 0 when the signature matches
npx -y @optitransfer/proofline@next contract verify-sig         # the Proofline CLI (Node 22+), same check
```

The Lean 4 models in [`lean/`](lean) cover the credit rule, nonces, timeouts, `settlement_pending` reconciliation and
concurrent settles. They use only standard axioms and contain no `sorry`. They model the rule the suite checks, not any
facilitator's code.

## Add the badge

If your project runs the suite in CI, you can say so. It means "tested with", not "certified":

```markdown
[![x402 conformance: tested](https://img.shields.io/badge/x402%20conformance-tested-2ea44f)](https://github.com/atsp-protocol/x402-conformance)
```

[![x402 conformance: tested](https://img.shields.io/badge/x402%20conformance-tested-2ea44f)](https://github.com/atsp-protocol/x402-conformance)

For a badge that shows the latest result (`pass 32/32` or `2 failing`), publish the action's `badge` output somewhere
public, for example with GitHub Pages or a gist, and point shields.io at it:

```markdown
[![x402 conformance](https://img.shields.io/endpoint?url=https://YOU.github.io/YOUR-REPO/x402-badge.json)](https://github.com/atsp-protocol/x402-conformance)
```

`x402conf report --dir x402-conformance --badge x402-badge.json` writes the same file outside the action.

## Who uses it

No public users yet. I only list a user once they've said so publicly. If you run it, open an issue or say so in
[Discussions](https://github.com/atsp-protocol/x402-conformance/discussions) and I'll add you here and to
[docs/ADOPTION.md](docs/ADOPTION.md).

**Free run for your project.** Open an issue with your repo or facilitator URL and I'll run the suite against it,
post the results, and send a tests-only pull request with the Action and badge. It's free, with no conditions.

## Safety

The suite holds no keys of yours and never signs or pays. `live` and `resource` are GET-only, and that's tested. The
probe's facilitator double listens on loopback only and never touches a chain. A facilitator credential, if you give
one, comes from the environment, goes only to the host you name and is never printed. The devnet runner refuses
mainnet and any third-party facilitator without written permission.

## Security

Please report vulnerabilities through GitHub private vulnerability reporting
([Security → Report a vulnerability](https://github.com/atsp-protocol/x402-conformance/security/advisories/new)), not in
a public issue. When the suite finds a failing safety check in someone else's facilitator, I tell the operator
privately first. The full policy is in [SECURITY.md](SECURITY.md).

## Contributing

Questions and ideas are welcome in [Discussions](https://github.com/atsp-protocol/x402-conformance/discussions).
Issues and pull requests are welcome, and new checks especially: propose one with the check-proposal issue template,
quoting the x402 spec text it rests on. If a check flags something you believe is correct, open a
[false positive report](https://github.com/atsp-protocol/x402-conformance/issues/new?template=false-positive.yml). Every check needs a test that fails against a faulty double and passes against
the reference. [CONTRIBUTING.md](CONTRIBUTING.md) lists the local checks to run before you push, and everyone taking
part follows the [Code of Conduct](CODE_OF_CONDUCT.md). Changes are listed in [CHANGELOG.md](CHANGELOG.md).

## Licence

Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE) (Ryan Gillespie / Optitransfer).
