Verify x402 payments offline, without a node or a key
Give Settlens a captured payment and the requirement it was made for. It recovers the EIP-3009 signer offline and checks that the authorization pays the advertised recipient the advertised amount within its time window. It also checks the facilitator that is supposed to do the same.
Real output. The payment was signed with a throwaway test key for this page.
settlens verify-payment payment.json --now 2026-10-13T10:00:00Z
VERIFIED exact EVM payment authorization
Recovered signer: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a
Signature valid: true
Recipient bound: true
Amount bound: true
Within window: true (at 1791885600)
In short
Settlens Verify checks a captured x402 payment offline. For the exact EVM scheme it recovers the EIP-3009 / EIP-712 signer and checks recipient, amount, asset and the validity window; for Solana it checks every ed25519 signer slot. It reads no chain state: balances, nonce use and settlement are not checked.
What Verify proves
-
Who signed it
For the exact scheme on eip155 networks, the EIP-3009 TransferWithAuthorization signer is recovered and must equal authorization.from.
-
What it pays, and to whom
authorization.to must equal payTo and authorization.value must equal amount. The asset is bound too: it is the EIP-712 verifying contract.
-
When it is valid
The check time must fall strictly between validAfter and validBefore, as EIP-3009 requires, so expired or not-yet-valid authorizations are rejected.
-
Solana signers
For solana networks, every signature slot is checked against its account key with ed25519, in legacy and versioned transactions. An empty fee-payer slot is reported as pending: the facilitator signs it at settlement.
-
Facilitator capabilities
GET /supported is checked for schema, x402 V2 support, signer metadata and coverage of what your resource advertises.
-
Facilitator fails closed
With authorization, Settlens sends /verify an all-zero signature and expects isValid to be false. It never calls /settle.
How EVM verification works
Everything Settlens needs is in the requirement and the payload. No RPC call, no block explorer, no key.
-
Rebuild the domain
Name and version come from
requirement.extra, the chain id from theeip155:<id>network, the verifying contract fromrequirement.asset. Withoutextra.nameandextra.versionthe payment is rejected. -
Recover the signer
The EIP-712 digest of the
TransferWithAuthorizationis computed and the signer recovered from the 65-byte signature. High-s signatures are refused, as USDC and OpenZeppelin's ECDSA library refuse them. -
Bind and time-check
Recipient and amount are compared with the requirement, and the window with the check time: the current clock, or the time given with
--now. Each result is reported separately.
Example: a valid payment and a tampered one
verify-payment reads a JSON file with the requirement and the payload. It also accepts the x402 field names paymentRequirements and paymentPayload.
{
"requirement": {
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds": 60,
"extra": { "name": "USDC", "version": "2" }
},
"payload": {
"signature": "0x03d6ea795b1a4d72ac92b359a7794c718f98caa5a57f6de53ab962ac3a63572600c44fb8ffdc405a13b9b7326f0e5891d3858c6a92780af43ad95c70e9e506221b",
"authorization": {
"from": "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
"to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"value": "10000",
"validAfter": "1791885175",
"validBefore": "1791888835",
"nonce": "0x2222222222222222222222222222222222222222222222222222222222222222"
}
}
}
To build payment-tampered.json, the value was raised from 10000 to 25000 after signing. Two things break at once: the amount no longer matches the requirement, and the signature no longer recovers to the payer, because the signed message changed.
settlens verify-payment payment-tampered.json --now 2026-10-13T10:00:00Z
REJECTED exact EVM payment authorization
Recovered signer: 0x742f0030214a80ec488d5cf429a3b603ca97d3ba
Signature valid: false
Recipient bound: true
Amount bound: false
Within window: true (at 1791885600)
Reasons:
- Recovered signer 0x742f0030214a80ec488d5cf429a3b603ca97d3ba does not match authorization.from 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a.
- authorization.value does not match requirement.amount.
| Code | Meaning |
|---|---|
| 0 | VERIFIED: every check passed. |
| 1 | REJECTED: at least one check failed. The reasons are listed. |
| 2 | The file could not be used, for example a network other than eip155 or solana, or a missing requirement or payload. |
Solana transactions
When the requirement's network starts with solana:, verify-payment reads payload.transaction, a Base64 serialized transaction. It detects legacy and versioned (v0) messages and checks each signature slot against the account key at the same position with ed25519. Each signer is printed in base58 with its status: valid, invalid or pending.
Partially signed payloads verify. In the x402 exact scheme on Solana, the client signs its part and the facilitator, as fee payer, adds its signature at settlement. A PAYMENT-SIGNATURE payload therefore arrives with an empty fee-payer slot. Settlens reports an empty slot as pending, not as a failure. The transaction is VERIFIED when at least one signature is present, every present signature is valid, and the number of slots matches the message header. The example here is such a payload, built with throwaway test keys.
This check proves who signed the transaction. It does not decode instructions, so it does not compare the transfer with payTo, amount or asset.
settlens verify-payment solana-payment.json
VERIFIED Solana transaction (v0)
Fee payer: 3EyEVVQ7iRg5KbiArQc83W7yJzjhjJjwUXpyHaisVD58 (signature pending: added by the facilitator at settlement)
Signer 0: 3EyEVVQ7iRg5KbiArQc83W7yJzjhjJjwUXpyHaisVD58 pending
Signer 1: J2mxWfLWmRqMtDRA5DtwaaW3ZATnijyXvgy9f2XpYWeb valid
Signatures valid: true
Facilitator checks
A facilitator verifies and settles payments for resource servers. Settlens checks what it advertises and, with authorization, whether its /verify endpoint rejects a forged payment. Background: x402 facilitator security.
Capabilities: GET /supported
settlens facilitator <base-url> reads /supported and checks the HTTP status, JSON media type, HTTPS, response size, the SupportedResponse schema, at least one x402 V2 kind and duplicate kinds. Add --facilitator to a scan and Settlens also checks that every scheme and network your resource advertises is covered, with signer metadata for each network.
settlens facilitator http://127.0.0.1:4402/facilitator --no-write
Settlens x402 assessment c693475c
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
Total: 8 pass · 0 fail · 0 warn · 0 skip
Fail-closed: POST /verify
During an authorized active probe with --facilitator, Settlens posts the same deliberately invalid payload to /verify. The answer must be a VerifyResponse with a boolean isValid, and isValid must be false. A facilitator that accepts an all-zero signature is a critical verification bypass.
settlens scan http://127.0.0.1:4402/secure \
--facilitator http://127.0.0.1:4402/facilitator-accept \
--active --authorized --authorization-ref DEMO-APPROVAL-003 --no-write
Settlens x402 assessment 01f41c77
Targets 1 · Active 1 · Settlement calls 0
127.0.0.1 GET http://127.0.0.1:4402/secure
26 pass · 1 fail · 0 warn · 0 skip
FAIL critical Facilitator /verify rejects an invalid signature
The facilitator reported isValid=true for an intentionally invalid, all-zero signature.
Total: 26 pass · 1 fail · 0 warn · 0 skip
| Check | Default severity | What it verifies |
|---|---|---|
facilitator.reachableFacilitator /supported is reachable | high | GET <facilitator>/supported returned an HTTP response within the timeout. Redirects are not followed. |
facilitator.response_body_limitFacilitator capability response fits the evidence limit | high | The /supported response is at most 1 MiB. |
facilitator.supported_statusFacilitator exposes capabilities successfully | high | GET /supported returned HTTP 200. |
facilitator.content_typeFacilitator capabilities use a JSON media type | medium | The /supported Content-Type is application/json or another +json media type. |
facilitator.httpsFacilitator capability transport is protected | medium | The facilitator URL uses HTTPS, or loopback HTTP for local testing. |
facilitator.supported_schemaSupportedResponse matches the x402 core shape | high | The body has a non-empty kinds array (each with a positive integer x402Version, a scheme and a network, CAIP-2 for V2 kinds), an extensions array of strings and a signers object mapping networks to string arrays. |
facilitator.v2_capabilityFacilitator advertises x402 V2 capabilities | high | At least one supported kind has x402Version 2. |
facilitator.duplicate_kindsFacilitator capabilities are unambiguous | low | No x402Version, scheme and network combination appears twice in kinds. |
facilitator.requirement_coverageFacilitator covers advertised payment requirements | high | Every scheme/network pair advertised by the resource appears in the facilitator's /supported kinds for the same x402Version. Skipped when /supported is invalid or unavailable. |
facilitator.signer_coverageFacilitator publishes signer coverage | medium | Every advertised network has a non-empty signer list in /supported, either for the exact network or for its namespace wildcard (for example eip155:*). |
| Check | Default severity | What it verifies |
|---|---|---|
facilitator.verify_reachableFacilitator /verify is reachable | medium | During an authorized active probe, POST <facilitator>/verify with the intentionally invalid payload completed. Reported as a warning only when it could not complete. |
facilitator.verify_response_schemaFacilitator /verify returns a conformant VerifyResponse | high | The /verify answer to the intentionally invalid payload is a JSON object with a boolean isValid field. |
security.facilitator_rejects_invalid_signatureFacilitator /verify rejects an invalid signature | critical | /verify returns isValid=false for the intentionally invalid, all-zero signature. Settlens never calls /settle. |
The same checks for AI agents
The Settlens MCP server exposes these verifications as offline tools: verify_payment_signature for EVM authorizations and verify_solana_transaction for Solana transactions. Neither needs network access or a key.
verify_payment_signature also accepts the raw Base64 PAYMENT-SIGNATURE header and a now time, which lets an auditor check a captured authorization against the moment it was used rather than today.
To verify one payment in the browser, paste the requirement and the payload in the free EIP-3009 signature verifier.
Check a capture at the time it was used
By default verify-payment checks the window against the current clock, so a capture verified after its validBefore is reported as outside its window. Pass --now with Unix seconds or an ISO 8601 time to check it at the moment it was used; the output prints the time it used.
What Verify does not check
- No RPC, no chain state. Settlens does not read balances, so a verified authorization can still fail to settle for lack of funds.
- No replay detection. It does not check whether the EIP-3009 nonce was already used on chain, or whether a Solana transaction was already processed.
- No settlement. It does not prove a transfer happened. Settlens never calls
/settleand never broadcasts a transaction. - Not the token contract. The domain name and version are taken from the requirement; Settlens does not confirm they match the contract deployed at
asset. - EOA signatures only. Only 65-byte ECDSA signatures are recovered. Smart-contract wallet signatures (EIP-1271) are not supported and are rejected.
- Solana: signatures only. Instructions, amounts and recipients are not decoded. A pending slot, such as the fee payer's before settlement, is reported but cannot be checked until it is signed.
- EIP-3009 only on EVM. The exact EVM scheme also defines Permit2 and ERC-7710 payloads; Settlens does not verify them.
verify-paymenttreats every eip155 requirement as EIP-3009 and does not check that its scheme isexact. - One forged payload class. The
/verifyprobe sends one all-zero payload. Passing it does not prove the facilitator verifies every payment correctly.
Questions about verification
Does Settlens need an RPC endpoint or an API key?
No. Verification is pure computation on the requirement and the payload: the EIP-712 domain is rebuilt from the requirement, and the signer is recovered locally.
Can it tell me whether a payment settled?
No. It verifies the authorization, not the transfer. Settlement, balances and nonce use live on chain, and Settlens does not read the chain.
Why does a tampered amount also break the signature?
The signature covers the amount. Change it and the recovered address no longer matches authorization.from, so both the signature and the amount checks fail.
Which payment formats are supported?
The exact scheme with EIP-3009 TransferWithAuthorization on eip155 networks, and Solana transactions, fully or partially signed, for the signer checks. Permit2 and ERC-7710 payloads are not supported. Other networks stop with exit code 2.
Does the facilitator probe settle anything?
No. It calls /verify once with a payload that cannot be valid. Settlens never calls /settle.
Sources
Check a facilitator or a payment flow
In a pilot we run the capability scan, the authorized /verify probe and offline verification on payments you capture.