Skip to main content

test-x402-payment-client

Test an x402 payment client end to end against a live endpoint: drive the full 402 -> PAYMENT-REQUIRED -> sign -> PAYMENT-SIGNATURE -> verify/settle loop, confirm settlement on chain, and check any signed offers or receipts for conformance. Use when validating a new x402 client or SDK before shipping, debugging a payment that never completes despite a well-formed 402, running x402 conformance in CI against a testnet facilitator, or verifying an endpoint actually takes money before you route real value to it. Start on a testnet; opt into mainnet settlement explicitly.

Informations de source

Dépôt
pjt222/agent-almanac
Dernière activité de la source
4 septembre 2026 à 10:02
Langue détectée de SKILL.md
anglais
Étoiles
34
Forks
4

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
test-x402-payment-client
description
Test an x402 payment client end to end against a live endpoint: drive the full 402 -> PAYMENT-REQUIRED -> sign -> PAYMENT-SIGNATURE -> verify/settle loop, confirm settlement on chain, and check any signed offers or receipts for conformance. Use when validating a new x402 client or SDK before shipping, debugging a payment that never completes despite a well-formed 402, running x402 conformance in CI against a testnet facilitator, or verifying an endpoint actually takes money before you route real value to it. Start on a testnet; opt into mainnet settlement explicitly.
license
MIT
allowed-tools
Read Write Edit Bash Grep Glob WebFetch
metadata
{"author":"cv-scvd","version":"1.0","domain":"agent-commerce","complexity":"intermediate","language":"multi","tags":"agent-commerce, x402, payments, testing, conformance","locale":"de","source_locale":"en","source_commit":"460ba8e8f","fence_basis_commit":"460ba8e8f","translator":"(untranslated stub)","translation_date":"2026-09-03"}
# Test an x402 Payment Client Validate that an x402 payment client completes the full protocol loop against a live endpoint — not just that the endpoint answers a `402`, but that a signed payment can actually be built, submitted, verified, settled on chain, and (where offered) checked for offer/receipt conformance. A well-formed challenge is a claim; a completed settlement is the proof. The header names, scheme semantics, and network identifiers below are from the x402 v2 specification and its transport and scheme specs (`specs/x402-specification-v2.md`, `specs/transports-v2/http.md`, `specs/transports-v2/mcp.md`, `specs/schemes/exact/scheme_exact_evm.md`, `specs/schemes/exact/scheme_exact_svm.md`, and `specs/extensions/extension-offer-and-receipt.md` in [coinbase/x402](https://github.com/coinbase/x402)). Treat those specs as the source of truth over any single vendor's docs. ## When to Use - Validating a new x402 client or SDK before shipping it - Debugging a payment that never completes even though the endpoint returns a well-formed `402` (the failure is usually in the header the client did not read, or the scheme it did not recognize) - Running x402 conformance in CI against a testnet facilitator on every build - Verifying an endpoint actually accepts payment — not just that it answers — before routing real value to it - Checking that signed offers or receipts an endpoint serves conform to the spec ## Inputs - **Required**: The endpoint URL under test (an HTTP resource that answers `402`). - **Required**: A funded test wallet and its signer. For EVM start on Base Sepolia (`eip155:84532`); for SVM start on Solana Devnet. Testnet USDC is free from a faucet. - **Optional**: A facilitator base URL if the client uses one directly (default: whatever the endpoint's challenge names). - **Optional**: `mainnet_optin` (boolean, default `false`) — only when `true` does the procedure move real funds. The final mainnet step states its own cost. - **Optional**: Output format for the conformance report (`json`, `markdown`). ## Procedure ### Step 1: Request the resource unpaid and capture the challenge Send a plain request and read the `402`. The machine-readable terms ride the `PAYMENT-REQUIRED` response header as base64-encoded JSON; the body is human-oriented and must not be the client's source of truth. ```bash set -o pipefail # a 402 carrying NO payment-required header makes grep fail while every later # stage succeeds on empty input; without this the pipeline reports only the # last stage. That is the silent break named below. The assertion catches it # too — keep both, since either alone can be edited away. curl -sD challenge.txt -o /dev/null https://x402.example.testnet/resource # the status line is the first half of what Expected demands, and nothing else reads it head -1 challenge.txt | grep -q ' 402' || { echo "not a 402: $(head -1 challenge.txt)"; exit 1; } # base64-decode the PAYMENT-REQUIRED header value into terms.json grep -i '^payment-required:' challenge.txt | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r' | base64 -d | jq . > terms.json # assert the rest of it: a non-empty file is not enough, `null` is a non-empty file, and an # entry missing one of the six fields is not one the signing step can use jq -e '.x402Version == 2 and ([.accepts[] | select(.scheme and .network and .asset and .amount and .payTo and .maxTimeoutSeconds)] | length) > 0' terms.json > /dev/null \ || { echo 'no usable PAYMENT-REQUIRED terms — stop here'; exit 1; } ``` The transport spec does not pin the base64 alphabet. The one endpoint measured while writing this skill uses standard base64 with `=` padding, which the decode above reads; a server using base64url would fail that decode rather than mis-parse it — `base64 -d` rejects `-` and `_` — and the assertion turns the failure into a stop rather than an empty file. The decoded `PaymentRequired` object carries `x402Version` (must be `2`), one or more `accepts` entries (each a `PaymentRequirements`), and any advertised `extensions`. Each `accepts` entry carries `scheme`, `network` (CAIP-2, e.g. `eip155:84532`), the token `asset`, the `amount` in atomic units, `payTo`, and `maxTimeoutSeconds`. **Expected:** HTTP `402`; a `PAYMENT-REQUIRED` header that base64-decodes to JSON in `terms.json` with `x402Version: 2` and at least one `accepts` entry carrying `scheme`, `network`, `asset`, `amount`, `payTo`, and `maxTimeoutSeconds`. Every clause of that sentence is asserted by the block, which is the point: the status line, the decode, the version, and an entry carrying all six fields. It exits non-zero on a non-`402`, on a header that decodes to `null`, on one carrying no `accepts` entries, on one declaring a version other than `2`, and on one whose only entry is missing a field the signing step needs. **On failure:** If there is no `PAYMENT-REQUIRED` header, the endpoint is not serving v2 terms a client can act on — stop and report that (a `402` body with no header is the single most common silent break). If the header is present but does not base64-decode to JSON, record it as malformed and stop. ### Step 2: Select an `accepts` entry the client actually supports Pick the entry whose `scheme` and `network` the client implements. The `exact` scheme is the baseline (EIP-3009 `TransferWithAuthorization` on EVM; `TransferChecked` for SPL tokens on SVM). A challenge may also offer other schemes (for example `upto` for variable amounts, or a batch-settlement scheme); a client built only for `exact` must skip those entries rather than misread them. Then gate the network: unless `mainnet_optin` is `true`, the selected entry must be on a testnet the spec's network list names (Base Sepolia `eip155:84532`, Avalanche Fuji `eip155:43113`, Solana Devnet `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`). This is the step that makes testnet-first a procedure rather than a preference. ```bash # pick the first `exact` entry on a network the client implements ($supported: edit to # match the client), then gate it: a mainnet network passes only with MAINNET_OPTIN=true jq -e --arg optin "${MAINNET_OPTIN:-false}" ' ["eip155:84532","eip155:43113","solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"] as $testnets | ["eip155:","solana:"] as $supported | [.accepts[] | select(.scheme=="exact" and (.network as $n | any($supported[]; . as $p | $n|startswith($p))))] as $ok | if ($ok|length)==0 then error("unsupported-scheme: " + (if (.accepts|length)==0 then "(challenge offered no accepts entries)" else ([.accepts[] | .scheme+"@"+.network]|join(",")) end)) else ([$ok[] | select((.network|IN($testnets[])) or $optin=="true")][0] // error("mainnet-not-opted-in: " + $ok[0].network)) end' terms.json > selected.json ``` **Expected:** Exactly one supported `accepts` entry in `selected.json`; its `scheme`, `network`, `asset`, `amount`, `payTo`, and `maxTimeoutSeconds` read into the signing step. A mainnet `network` appears there only when `mainnet_optin` is `true`. **On failure:** If no offered entry matches a scheme+network the client supports, that is a real interop result — record `unsupported-scheme` with the schemes offered, and stop. Do not coerce an `upto` or batch entry into an `exact` signature. If the only matching entry is on a mainnet network and `mainnet_optin` is `false`, record `mainnet-not-opted-in` and stop — that is the gate working, not a defect. ### Step 3: Build and sign the payment authorization **This step has no fence because it is the client under test that signs it.** Hand `selected.json` to that client along the path it would use in production, and capture what it produces as `payload.json`; the skill supplies no reference signer, because signing with one would test the reference rather than the client. The `Inputs` section names the wallet and its signer as things you bring. Construct the scheme-specific authorization over the selected entry and sign it. For `exact` on EVM the recommended mechanism is an EIP-3009 `TransferWithAuthorization` (EIP-712 typed-data signature) for the exact amount to `payTo`, with a fresh random `nonce` and a `validBefore` inside the entry's `maxTimeoutSeconds` — but it is not the only one. The scheme spec (`specs/schemes/exact/scheme_exact_evm.md`) also defines Permit2 as the universal fallback for tokens without EIP-3009 and ERC-7710 for smart accounts, selected by `assetTransferMethod` in the payload (default order: EIP-3009, then Permit2). Echo every extension the server advertised: the client must include at least the info it received and may append, but may not delete or overwrite it. Write the result to `payload.json` for the next step. **Expected:** A `PaymentPayload` in `payload.json` with `x402Version: 2`, the selected entry under `accepted`, and a scheme-specific `payload` carrying the signature and authorization; advertised extensions echoed intact. **On failure:** If signing throws on the address or amount, check the `asset` checksum and that the amount is an atomic-unit string, not a decimal. A decimal-typed amount underpays by a factor of the token's decimals and is a frequent silent bug. ### Step 4: Retry with the signed payment Base64-encode the `PaymentPayload` and resend the same request with it in the `PAYMENT-SIGNATURE` header. The endpoint (or its facilitator) verifies the signature and settles. ```bash set -o pipefail # Step 3 has no fence — it is the client under test that signs — so check its artifact arrived. # `base64 missing.json | tr -d` exits 0 (the pipeline reports tr), which would otherwise send an # empty PAYMENT-SIGNATURE and read back as a settlement failure. jq -e '.x402Version == 2 and .accepted != null and .payload != null' payload.json > /dev/null 2>&1 \ || { echo 'payload.json is not a PaymentPayload: the client under test produced no signed payment'; exit 1; } # base64 without -w0: GNU wraps at 76 columns, BSD and busybox reject -w — strip newlines instead curl -s -H "PAYMENT-SIGNATURE: $(base64 payload.json | tr -d '\n')" \ https://x402.example.testnet/resource -D headers.txt -o body.json head -1 headers.txt | grep -q ' 200' || { echo "not a 200: $(head -1 headers.txt)"; exit 1; } # the settlement rides a header too, and needs the same decode as Step 1 grep -i '^payment-response:' headers.txt | sed 's/^[^:]*:[[:space:]]*//' | tr -d '\r' | base64 -d | jq . > settlement.json # assert the settlement before believing it: an absent header, a `null` body and a SettleResponse # with no transaction all leave a block that merely prints, exiting 0 jq -e '.success == true and ((.transaction // "") | length) > 0' settlement.json > /dev/null \ || { echo 'settled-unverified: no settlement transaction'; exit 1; } jq -r '.transaction' settlement.json # the hash Step 5 verifies ``` **Expected:** HTTP `200`. The response carries a settlement result in the base64-encoded `PAYMENT-RESPONSE` header, decoded above into `settlement.json` (`SettleResponse`: `success: true`, a non-empty `transaction` hash, and the `network`; `amount` and `payer` are optional and may be omitted). **On failure:** A repeated `402` means verification refused the payment — inspect the reason. A common cause is the client sending the legacy `X-PAYMENT` header name where the endpoint only reads `PAYMENT-SIGNATURE` (or the reverse); try the other name once and record which the endpoint accepts. The status check above separates that case from the next one by message, so read which of the two the block printed. If the response is `200` but carries no settlement transaction — no `PAYMENT-RESPONSE` header at all, a `SettleResponse` without a `transaction`, or one reporting `success: false` — the assertion above exits non-zero: record `settled-unverified` and stop. There is no hash for Step 5 to check, and a `200` without a settlement is not a completed payment. ### Step 5: Verify the settlement independently on chain Do not trust the endpoint's own word that money moved. Take the `transaction` hash from the `SettleResponse` and confirm it on chain: correct `payTo`, correct `asset`, amount within what was authorized, and finality. ```bash # EVM: fetch the receipt from a Base Sepolia RPC and read the Transfer log curl -s -X POST https://sepolia.base.org -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<transaction>"]}' \ | jq '{status: .result.status, block: .result.blockNumber, logs: .result.logs}' # status 0x1 = success. In the ERC-20 Transfer log: .address must equal the `asset` # contract, topics[2] is the recipient (must equal payTo, zero-padded) and .data is # the value in atomic units. Check .address too — a Transfer of the right amount to # the right address from the wrong token contract is exactly the mismatch below. # Finality: the receipt carries no head height, so ask for one and subtract. curl -s -X POST https://sepolia.base.org -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \ | jq -r '"head " + .result' # confirmations = head - .result.blockNumber, both hex; require what that network needs. # SVM: the equivalent is getTransaction, reading the token-balance delta rather # than a log. curl -s -X POST https://api.devnet.solana.com -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getTransaction","params":["<signature>",{"encoding":"json","maxSupportedTransactionVersion":0}]}' \ | jq '{err: .result.meta.err, pre: .result.meta.preTokenBalances, post: .result.meta.postTokenBalances}' # err null = success; for the balance entry whose owner is payTo AND whose mint is # `asset` — both, in one filter — post minus pre must equal the authorized amount. # A destination account the transaction creates has no pre entry at all, which # reads as a pre of 0. scheme_exact_svm.md is what licenses filtering on the owner: # "Destination MUST equal the Associated Token Account PDA for (owner = payTo, # mint = asset)", so payTo is the wallet, not the token account. Deriving that PDA # and matching the entry's account address is the stricter check, if you have a # derivation to hand. # err null is inclusion, not finality: poll getSignatureStatuses for the # commitment level you require. ``` **Expected:** The on-chain transfer matches `payTo`, `asset`, and the authorized amount, and has reached finality on the stated `network`. **On failure:** If the hash is absent, unconfirmed, or pays a different address or amount than authorized, record `settlement-mismatch` — the endpoint claimed a settlement the chain does not show. This is the failure the whole procedure exists to catch. ### Step 6 (variant): MCP transport If the client speaks x402 over MCP rather than HTTP, the same loop applies with different envelopes (`specs/transports-v2/mcp.md`): the payment terms arrive in a tool *result* with `isError: true`, in `result.structuredContent` (primary) and `result.content[0].text` (JSON fallback), instead of a `PAYMENT-REQUIRED` header; the signed payment is sent in the request's `_meta["x402/payment"]` instead of a `PAYMENT-SIGNATURE` header; and the settlement comes back in the result's `_meta["x402/payment-response"]`. The scheme, signing, and on-chain verification steps are unchanged. Treat this as a transport variant, not the primary path. **Expected:** Terms parsed from `result.structuredContent` of an `isError: true` tool result; payment sent in `_meta["x402/payment"]`; settlement read from `_meta["x402/payment-response"]` and verified on chain as in Step 5. **On failure:** If an `isError: true` result carries no `structuredContent` or `content[0].text` that decodes to `PaymentRequired` terms, the server is not advertising x402 over this transport — fall back to the HTTP path or stop. ## Validation - [ ] The `402` carried a `PAYMENT-REQUIRED` header that base64-decoded to JSON with `x402Version: 2` and at least one complete `accepts` entry. - [ ] The client selected a scheme+network it supports and refused the rest rather than misreading them. - [ ] Step 2's gate was exercised on its negative cases, not only on a challenge it accepts: an `exact` entry on a network prefix the client does not implement, a challenge mixing mainnet and testnet entries with `mainnet_optin` false (the testnet entry must be the one selected, not a refusal), and a challenge with an empty `accepts` array. Each must stop the run with the documented reason rather than select anything. - [ ] The signed authorization used an atomic-unit amount and echoed all advertised extensions. - [ ] The retry with `PAYMENT-SIGNATURE` returned `200` with a `SettleResponse` carrying a real transaction hash. - [ ] The settlement was confirmed independently on chain (address, asset, amount, finality) — not taken on the endpoint's word. - [ ] Any signed offers in the challenge or receipts in the response were checked against the `offer-receipt` extension (`specs/extensions/extension-offer-and-receipt.md`): offers live in `extensions["offer-receipt"].info.offers[]`, the receipt in `extensions["offer-receipt"].info.receipt`. For an EIP-712 signature,
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub