How to test an x402 payment endpoint: a practical checklist

Most of an x402 implementation can be verified without a wallet or payment key and without moving money. This is the order we recommend, from safe passive checks to one carefully scoped active test.

In short

Test an x402 endpoint from safest to riskiest: write down the terms you expect, run a passive scan of the 402 challenge, keep the report, check the facilitator, then, with the owner's written approval, send one invalid payment that must be refused. Finish with offline signature checks, a CI gate and monitoring.

On this page

You can verify most of an x402 implementation without ever holding a wallet or payment key, or moving money. The trick is to go in the right order: start with checks that only read what the endpoint already publishes, add the facilitator, and keep the single active test for the end, scoped and approved. This guide walks through that order with the Settlens CLI and shows the real output of each command against the bundled demo target.

Each step maps to one or more of the x402 failure modes worth testing. If you only have ten minutes, do steps 1 and 2: they cover everything the 402 challenge itself reveals and need no one's permission beyond your own.

Before you start

  • Settlens CLI and Node.js 20.19 or newer. The code lives in a private repository that pilot participants receive. The commands below use settlens; in a checkout, node src/cli.js or npm run settlens -- run the same thing.
  • The exact URL an agent will request, path and query included. x402 binds a payment to a resource URL, so “roughly the same endpoint” is not good enough.
  • The terms you reviewed: allowed schemes, CAIP-2 networks, recipient addresses, token assets and the maximum price in atomic units.
  • For step 5 only: written approval from the endpoint owner and a reference you can cite, such as a ticket number.
  • An x402 V2 endpoint. Settlens assesses the V2 headers. If yours still uses the V1 X-PAYMENT flow, start with the V1 to V2 migration guide.

To practice first, run npm run demo:server in the Settlens checkout. It serves a compliant route (/secure) and deliberately broken ones (/missing-header, /drift, /bypass) plus demo facilitators on http://127.0.0.1:4402. Every transcript on this page comes from that server.

1. Write down the terms you expect

A test needs an expected result. For x402 that means the payment terms you have actually reviewed: which networks you accept payment on, which addresses may receive the money, which token contracts are valid and the highest price you intend to charge. Without them, a scan can only tell you that the challenge is well formed, not that it is yours.

Amounts are compared in the asset's atomic units. USDC uses 6 decimals, so "10000" means 0.01 USDC. The same number means something entirely different in a token with other decimals, which is why a price ceiling only makes sense next to an asset allowlist. Put the terms in a configuration file so they are reviewed like code. This is the example shipped with Settlens, pointed at the demo:

JSONsettlens.config.example.json
{
  "version": 1,
  "failOn": "high",
  "output": "reports",
  "defaults": {
    "method": "GET",
    "timeoutMs": 8000,
    "repeat": 2
  },
  "targets": [
    {
      "name": "local-secure-demo",
      "url": "http://127.0.0.1:4402/secure",
      "facilitator": "http://127.0.0.1:4402/facilitator",
      "expected": {
        "resourceUrl": "http://127.0.0.1:4402/secure",
        "schemes": [
          "exact"
        ],
        "networks": [
          "eip155:84532"
        ],
        "payTo": [
          "0x209693Bc6afc0C5328bA36FaF03C514EF312287C"
        ],
        "assets": [
          "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
        ],
        "maxAmount": "10000"
      }
    }
  ]
}

For one-off scans, the same constraints exist as CLI flags: --expect-scheme, --expect-network, --expect-pay-to, --expect-asset and --max-amount. The configuration reference lists every field, including environment-variable expansion for secrets in headers.

2. Run a passive scan

A passive scan sends ordinary unauthenticated requests and inspects the response. It never attaches a payment authorization: Settlens refuses a user-supplied PAYMENT-SIGNATURE header outright, so a passive scan cannot send a real payment by accident. It does not follow redirects, stops reading the body at 1 MiB, times out after 8 seconds by default and samples the challenge twice to compare the terms. It is safe to run against any endpoint you own.

Passive scan of the compliant demo route
settlens scan http://127.0.0.1:4402/secure
Settlens x402 assessment d61e12e0
Targets 1 · Active 0 · Settlement calls 0
 
127.0.0.1  GET http://127.0.0.1:4402/secure
13 pass · 0 fail · 0 warn · 0 skip
 
Total: 13 pass · 0 fail · 0 warn · 0 skip

Thirteen checks passed: HTTP 402, the PAYMENT-REQUIRED header and its Base64 JSON encoding, validation by the official @x402/core 2.28.0 SDK, the V2 core shape, the binding of resource.url to the requested URL, well-formed and unique payment options, a bounded authorization window, transport and stability. Now give the same scan a policy it should not satisfy, a mainnet network and a lower ceiling:

The same route against a stricter policy
settlens scan http://127.0.0.1:4402/secure \
    --expect-scheme exact \
    --expect-network eip155:8453 \
    --expect-pay-to 0x209693Bc6afc0C5328bA36FaF03C514EF312287C \
    --max-amount 5000 --no-write
Settlens x402 assessment 989c24d5
Targets 1 · Active 0 · Settlement calls 0
 
127.0.0.1  GET http://127.0.0.1:4402/secure
15 pass · 2 fail · 0 warn · 0 skip
  FAIL high     Networks match the local policy
       Unexpected networks: eip155:84532.
  FAIL high     Amounts stay within the approved ceiling
       1 payment amount(s) exceed 5000.

Policy failures are high severity on purpose: a challenge that asks for payment on the wrong chain or above your ceiling is not a cosmetic issue. The exit code tells automation what happened:

settlens scan exit codes
Exit codeMeaning
0No failing check at or above the --fail-on severity (high by default).
1At least one failing check at or above the threshold. Warnings never change the exit code.
2CLI or configuration error, for example an active probe without authorization.

No CLI at hand? The free x402 payment request checker runs the offline part of these checks (decoding, the official SDK schema, binding and payment options) and your spending policy on a header you paste, from your browser through the public Settlens MCP endpoint. It does not send requests to your endpoint, so status, transport and stability checks stay with the scan.

3. Read and keep the evidence

Each scan writes JSON, HTML and SARIF reports to reports/ (change it with --out, pick formats with --format, or skip files with --no-write). Reports keep what an assessor needs and drop what a leak would expose: URLs are sanitized, request headers appear by name only, and the resource's response body is never stored, only its size and SHA-256.

The JSON report ends with a deterministic SHA-256 integrity block. Anyone can recompute it:

Recompute the report digest
settlens verify-report reports/settlens-e474a127.json
VALID Report content matches its SHA-256 evidence digest.
Expected: b363efd5ffd5b2bd5228c503cf8f36de9f1e533f3c7caba95e61710c83426ed0
Actual:   b363efd5ffd5b2bd5228c503cf8f36de9f1e533f3c7caba95e61710c83426ed0

A matching digest shows the file was not modified after it was generated. It does not show who generated it. For that, sign it: our article on signed evidence for payment assessments covers ed25519 attestations and offline verification, and the report reference documents every field.

4. Check the facilitator

If your resource relies on a facilitator, test the facilitator too. settlens facilitator reads GET /supported and checks that it is reachable, returns HTTP 200 with a JSON media type over HTTPS, stays under the 1 MiB evidence limit, matches the SupportedResponse shape (kinds, extensions and a signers map), advertises at least one x402 V2 kind and lists no duplicate kinds.

Facilitator capability scan
settlens facilitator http://127.0.0.1:4402/facilitator --no-write
Settlens x402 assessment 7b34c930
Targets 1 · Active 0 · Settlement calls 0
 
127.0.0.1  GET http://127.0.0.1:4402/facilitator/supported
8 pass · 0 fail · 0 warn · 0 skip

Then pass --facilitator <base-url> to the resource scan. Settlens cross-checks every scheme and network the resource advertises against the facilitator's supported kinds and signers. A gap there means an agent can sign a valid payment that will never be verified or settled; the failure modes article shows what that finding looks like.

5. Run the authorized fail-closed probe

Only with the owner's written approval

The active probe sends a payment attempt the endpoint owner must authorize. Keep the approval and cite it with --authorization-ref; Settlens records the reference in the report.

This is the test that catches a payment bypass, the most expensive x402 bug. Settlens builds one intentionally invalid authorization from the first advertised payment option (for an EVM option, an all-zero signature from the zero address), retries the request with it and asserts the resource does not answer with a 2xx. With --facilitator, it also sends the same payload to the facilitator's /verify and expects isValid: false. It uses no private key, never creates a valid payment and never calls /settle, so it cannot move funds.

The CLI will not run it half-authorized:

Active mode without the authorization flags
settlens scan http://127.0.0.1:4402/secure --active
Settlens error: Active testing requires --authorized to confirm explicit owner approval.
Authorized active probe with facilitator /verify
settlens scan http://127.0.0.1:4402/secure \
    --facilitator http://127.0.0.1:4402/facilitator \
    --active --authorized --authorization-ref DEMO-APPROVAL-001 --no-write
Settlens x402 assessment e22f6df8
Targets 1 · Active 1 · Settlement calls 0
 
127.0.0.1  GET http://127.0.0.1:4402/secure
27 pass · 0 fail · 0 warn · 0 skip

Twenty-seven checks passed, including the two critical ones: the resource refused the invalid payment, and the facilitator reported it invalid. Against the demo's /bypass route, the same probe fails at critical severity and the scan exits with code 1.

6. Verify a captured payment offline

When you have a captured payment, for example from a testnet integration test, you can check it cryptographically without a wallet, an RPC node or the facilitator. The input is a JSON file with the payment option that was advertised and the payload that was signed. Here is one produced with a throwaway test key on Base Sepolia:

JSONpayment.json
{
  "requirement": {
    "scheme": "exact",
    "network": "eip155:84532",
    "amount": "10000",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
    "maxTimeoutSeconds": 60,
    "extra": { "name": "USDC", "version": "2" }
  },
  "payload": {
    "signature": "0x583173e1108ff674438d589b58be3835f620f9f4cb70f840cc6567e6d86c80f27e025a2829721b15310cc7321d0b98e3f125228fbf03e20488a031941355c03d1b",
    "authorization": {
      "from": "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
      "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
      "value": "10000",
      "validAfter": "1791884632",
      "validBefore": "1791884752",
      "nonce": "0x2222222222222222222222222222222222222222222222222222222222222222"
    }
  }
}

For the EVM exact scheme with EIP-3009, the default transfer method, verify-payment rebuilds the EIP-712 domain from the requirement itself (extra.name, extra.version, the asset as verifying contract and the chain id from the CAIP-2 network), recovers the EIP-3009 signer and checks it matches authorization.from. It also checks that to and value match the advertised payTo and amount, and that the current time is strictly between validAfter and validBefore (pass --now to check another moment). Changing a single field breaks the signature:

A valid authorization, then the same one with a modified value
settlens verify-payment payment.json --now 2026-10-13T09:44:30Z
VERIFIED exact EVM payment authorization
Recovered signer: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a
Signature valid:  true
Recipient bound:  true
Amount bound:     true
Within window:    true (at 1791884670)
 
settlens verify-payment payment-tampered-value.json --now 2026-10-13T09:44:30Z
REJECTED exact EVM payment authorization
Recovered signer: 0xdbb565778faeb0c723c824bf08c1fc09c8a695a1
Signature valid:  false
Recipient bound:  true
Amount bound:     false
Within window:    true (at 1791884670)
Reasons:
  - Recovered signer 0xdbb565778faeb0c723c824bf08c1fc09c8a695a1 does not match authorization.from 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a.
  - authorization.value does not match requirement.amount.

Check the original file at a later time, once validBefore has passed, and the authorization has expired:

The same authorization after its window
settlens verify-payment payment.json --now 2026-10-13T10:00:00Z
REJECTED exact EVM payment authorization
Recovered signer: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a
Signature valid:  true
Recipient bound:  true
Amount bound:     true
Within window:    false (at 1791885600)
Reasons:
  - authorization is outside its validAfter/validBefore window (EIP-3009 requires validAfter < now < validBefore).

For Solana, verify-payment checks the ed25519 signature in every signer slot of legacy and versioned transactions; the fee payer's slot of a client payload is reported as pending, because the facilitator signs it at settlement. Permit2 and ERC-7710 payloads of the EVM exact scheme are not verified. In every case the check is offline: it does not look at balances, does not check whether a nonce was already used on-chain and does not confirm settlement. The Verify page and the EIP-3009 explainer go deeper, and the EIP-3009 signature verifier runs the same EVM check from your browser, through the public Settlens MCP endpoint.

7. Gate every release in CI

Checks that run once go stale. Wire the scan into the pipeline so every change to the paid endpoint is assessed before it ships. Settlens includes a composite GitHub Action that runs settlens check with your configuration, writes the three report formats and fails the job at your threshold. This is the example workflow from the repository; it runs the action from a checkout of the Settlens repository:

YAMLexamples/github-workflow.yml
name: Settlens

on:
  pull_request:
  workflow_dispatch:

permissions:
  contents: read
  security-events: write

jobs:
  x402-assessment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

      # Until Settlens is published as a remote action, this repository can run
      # its local composite action directly.
      - name: Assess x402 endpoints
        uses: ./
        with:
          config: settlens.config.json
          output: reports
          formats: json,html,sarif
          fail-on: high
          # Set this from a repository variable when the config uses secrets.
          allowed-hosts: ${{ vars.SETTLENS_ALLOWED_HOSTS }}

      - name: Upload SARIF to code scanning
        if: >-
          always() &&
          hashFiles('reports/**/*.sarif') != '' &&
          (github.event_name != 'pull_request' ||
          github.event.pull_request.head.repo.full_name == github.repository)
        uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a
        with:
          sarif_file: reports

      - name: Upload evidence
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
        with:
          name: settlens-reports
          path: reports/
          if-no-files-found: error
          retention-days: 30

Two details matter. fail-on counts failing checks only, so a warning such as unstable terms never blocks a merge by itself. And when your configuration sends custom headers (an assessment token, for instance), their destination must be listed in allowed-hosts; a modified configuration therefore cannot forward a CI secret to a host you did not approve. The GitHub Action reference covers inputs and outputs.

8. Keep watching in production

Endpoints drift after they ship: a configuration change moves the recipient, a pricing experiment raises the amount, a migration drops the header. settlens monitor runs the passive scan once per invocation, so you schedule it with cron or a CI job. It compares each run with a stored baseline and exits with code 1 when an alert (a regression, a new failure or a removed check) reaches the --fail-on threshold, optionally posting an alert to a webhook:

ShellScheduled monitoring
settlens monitor --config settlens.config.json --state settlens.state.json \
  --webhook https://hooks.example.com/settlens

Monitoring is fully passive and never sends a payment. Our article on detecting x402 payment drift shows real Monitor output and how to pin your policy so a changed price becomes an alert.

The checklist

x402 endpoint testing checklist
StepCommandWhat it showsOwner approval
1. Expected termssettlens.config.jsonWhat a correct challenge looks like for youNo
2. Passive scansettlens scan <url>Conformance, binding, stability and policy of the 402 challengeNo
3. Evidencesettlens verify-reportThe report was not modified after it was generatedNo
4. Facilitatorsettlens facilitator <url>Capabilities, signers and coverage of advertised optionsNo
5. Fail-closed probe--active --authorizedAn invalid payment is refused by the resource and /verifyYes
6. Offline verificationsettlens verify-paymentA captured authorization is signed by its payer and bound to the termsNo
7. CI gateGitHub ActionEvery change is assessed before it shipsNo
8. Monitoringsettlens monitorRegressions are caught after releaseNo

Together these steps turn “we think payments work” into evidence your team, your customers and your auditors can check. If you want them run against your own endpoints with a report you can share, request a pilot; the quickstart covers installation for pilot participants.

Sources

  1. x402 V2 HTTP transport: PAYMENT-REQUIRED, PAYMENT-SIGNATURE and error status codesx402 Foundation · github.com
  2. x402 protocol specification, version 2 (§7.1 POST /verify, §7.3 GET /supported)x402 Foundation · github.com
  3. EIP-3009: Transfer With AuthorizationEthereum Improvement Proposals · eips.ethereum.org
  4. EIP-712: Typed structured data hashing and signingEthereum Improvement Proposals · eips.ethereum.org
  5. Static Analysis Results Interchange Format (SARIF) Version 2.1.0OASIS · docs.oasis-open.org