---
title: "Verify x402 payments offline: EIP-3009 and Solana | Settlens"
description: "Verify captured x402 payments offline: EIP-3009 signer recovery, recipient, amount and time-window binding, Solana signer checks and facilitator checks."
url: https://settlens.com/product/verify
last_updated: 2026-10-13
---

# Verify x402 payments offline, without a node or a key

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

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.

[Request a pilot](https://settlens.com/contact) [Verify from an agent (MCP)](https://settlens.com/mcp)

Real output. The payment was signed with a throwaway test key for this page.

**settlens verify-payment**

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

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

_Diagram: Verification pipeline. On the left: rebuild the EIP-712 domain from extra.name, extra.version, the asset and the chain id; hash the TransferWithAuthorization; compute the EIP-712 digest; recover the signer from the 65-byte signature, rejecting high-s values; the signer must equal authorization.from. On the right: authorization.to must equal payTo, value must equal amount, and the check time must be strictly between validAfter and validBefore. The payment is verified only if every check holds._

_Left: the cryptographic path. Right: the binding checks against the requirement. The result is VERIFIED only when all of them pass._

1. **Rebuild the domain**

   Name and version come from `requirement.extra`, the chain id from the `eip155:<id>` network, the verifying contract from `requirement.asset`. Without `extra.name` and `extra.version` the payment is rejected.

2. **Recover the signer**

   The EIP-712 digest of the `TransferWithAuthorization` is computed and the signer recovered from the 65-byte signature. High-s signatures are refused, as USDC and OpenZeppelin's ECDSA library refuse them.

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

**payment.json (test key)**

```json
{
  "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.

**A tampered amount is caught twice**

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

_verify-payment exit codes_

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

**A client payload before settlement**

```text
$ 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](https://settlens.com/learn/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.

**Capability scan of the demo facilitator**

```text
$ 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](https://settlens.com/product/assess#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.

**A facilitator that accepts a forged payment**

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

_Facilitator capability checks (10)_

| Check | Default severity | What it verifies |
| --- | --- | --- |
| `facilitator.reachable` ; Facilitator /supported is reachable | high | GET \<facilitator>/supported returned an HTTP response within the timeout. Redirects are not followed. |
| `facilitator.response_body_limit` ; Facilitator capability response fits the evidence limit | high | The /supported response is at most 1 MiB. |
| `facilitator.supported_status` ; Facilitator exposes capabilities successfully | high | GET /supported returned HTTP 200. |
| `facilitator.content_type` ; Facilitator capabilities use a JSON media type | medium | The /supported Content-Type is application/json or another +json media type. |
| `facilitator.https` ; Facilitator capability transport is protected | medium | The facilitator URL uses HTTPS, or loopback HTTP for local testing. |
| `facilitator.supported_schema` ; SupportedResponse 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_capability` ; Facilitator advertises x402 V2 capabilities | high | At least one supported kind has x402Version 2. |
| `facilitator.duplicate_kinds` ; Facilitator capabilities are unambiguous | low | No x402Version, scheme and network combination appears twice in kinds. |
| `facilitator.requirement_coverage` ; Facilitator 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_coverage` ; Facilitator 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:\*). |

_Facilitator /verify probe, authorized only (3)_

| Check | Default severity | What it verifies |
| --- | --- | --- |
| `facilitator.verify_reachable` ; Facilitator /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_schema` ; Facilitator /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_signature` ; Facilitator /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](https://settlens.com/mcp) 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](https://settlens.com/tools/eip-3009-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 `/settle` and 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-payment` treats every eip155 requirement as EIP-3009 and does not check that its scheme is `exact`.
- **One forged payload class.** The `/verify` probe 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.

## Related

- **[Assess](https://settlens.com/product/assess)**

  Passive conformance and policy scan, plus the authorized fail-closed probe.

- **[CI gate](https://settlens.com/product/ci)**

  GitHub composite Action with a fail-on threshold, SARIF and evidence artifacts.

- **[Monitor](https://settlens.com/product/monitor)**

  Scheduled passive scans compared to a baseline, with regression alerts.

- **[Evidence](https://settlens.com/product/evidence)**

  ed25519 attestations over the report digest, verifiable offline.

Background: [EIP-3009 explained](https://settlens.com/learn/eip-3009) · [verify one payment in the browser](https://settlens.com/tools/eip-3009-verifier) · [MCP tool reference](https://settlens.com/docs/mcp) · [Settlens for facilitators](https://settlens.com/use-cases/facilitators) · [verify before paying](https://settlens.com/blog/mcp-verify-before-paying).

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

[Request a pilot](https://settlens.com/contact) [Use the MCP server](https://settlens.com/mcp)

## Sources

1. [ERC-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) — Ethereum Improvement Proposals
2. [EIP-712: Typed structured data hashing and signing](https://eips.ethereum.org/EIPS/eip-712) — Ethereum Improvement Proposals
3. [x402 exact scheme on EVM](https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_evm.md) — x402 Foundation
4. [x402 exact scheme on Solana](https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_svm.md) — x402 Foundation
5. [ERC-1271: Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271) — Ethereum Improvement Proposals
