---
title: "x402 endpoint testing: passive scan and probe | Settlens"
description: "x402 testing for paid APIs: scan for x402 V2 conformance, resource binding, drift and your payment policy, then check it fails closed with an authorized probe."
url: https://settlens.com/product/assess
last_updated: 2026-10-13
---

# Assess an x402 endpoint before agents pay it

> **In short:** Settlens Assess tests an x402 V2 paid endpoint from the client's side: the HTTP 402 handshake, PAYMENT-REQUIRED validated with the official @x402/core SDK, the resource binding, every payment option, drift and your policy. With the owner's written approval, one invalid signature checks that it fails closed.

Settlens runs x402 testing from the client's side: it requests your paid resource, decodes PAYMENT-REQUIRED, validates it with the official @x402/core SDK, checks that it is bound to the URL you requested and compares every advertised option with your recipient, network, asset and price policy. Passive by default. With the owner's written approval, one deliberately invalid payment checks that the resource fails closed.

[Request a pilot](https://settlens.com/contact) [Read the CLI reference](https://settlens.com/docs/cli)

Real output from the bundled demo target (npm run demo:server).

**settlens scan (demo target)**

```text
$ settlens scan http://127.0.0.1:4402/secure
Settlens x402 assessment 86148897
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
```

## What a passive scan checks

A passive scan sends ordinary unauthenticated requests. It never sends a payment signature, and it refuses one if you try to add it as a custom header.

- **The 402 handshake**

  The unauthenticated request must return HTTP 402 with a PAYMENT-REQUIRED header that decodes as Base64 JSON.

- **Official SDK and V2 shape**

  The decoded object must pass the schema of the pinned @x402/core 2.28.0 SDK and carry x402Version 2, a resource and a non-empty accepts list.

- **Resource binding**

  resource.url must equal the URL you scanned, path and query included, so a payment cannot be steered to another resource.

- **Every payment option**

  Scheme, CAIP-2 network, positive atomic amount, asset, payTo and maxTimeoutSeconds are validated. Duplicates and windows over one hour are flagged.

- **Drift between requests**

  Identical requests (two by default) must return the same resource, options and extensions, so silent price or recipient changes surface.

- **Transport hygiene**

  HTTPS outside loopback, redirects reported and never followed, response bodies read up to 1 MiB and never copied into the report.

- **Your payment policy**

  Allowed schemes, networks, recipients, assets and a maximum amount. In a scan, every advertised option must comply.

- **Facilitator coverage**

  With --facilitator, Settlens reads GET /supported and checks that each advertised scheme and network is covered, with signer metadata.

  [Facilitator checks](https://settlens.com/product/verify#facilitator)

## How a scan runs

1. **Request the resource without payment**

   Settlens sends the configured method (GET by default) with no payment header, an 8-second timeout and redirects disabled. Custom headers are only sent to hosts you allowlist.

2. **Decode and validate PAYMENT-REQUIRED**

   The header is size-checked, Base64-decoded and parsed as JSON, then validated twice: by the official @x402/core 2.28.0 schema and by Settlens' own V2 core-shape checks.

3. **Check binding and each option**

   resource.url is compared with the URL under test, and every accepts entry is validated field by field.

4. **Repeat to detect drift**

   The same request is sent again (`--repeat`, two samples by default) and the terms are compared, ignoring known ephemeral fields.

5. **Apply your policy**

   Every advertised option is compared with the allowed schemes, networks, recipients, assets and maximum amount you set.

6. **Write the evidence**

   The CLI prints a summary and writes JSON, HTML and SARIF reports. The JSON report carries a SHA-256 integrity digest you can check with `verify-report` or sign as [evidence](https://settlens.com/product/evidence).

## Passive scan and authorized probe

Both modes run the same passive checks first. The active probe adds exactly one request to the resource and, when a facilitator is configured, one call to its /verify endpoint.

_Diagram: Sequence between the Settlens CLI, the paid resource and the facilitator. Passive mode: an unauthenticated GET, a 402 response with PAYMENT-REQUIRED, the same GET again for drift, and an optional GET /supported to the facilitator. Active mode, authorized only: one invalid PAYMENT-SIGNATURE that must not get a 2xx response, then POST /verify with the same payload, which must return isValid false. Settlens never calls /settle._

_Passive requests run on every scan. The blue exchanges run only with --active --authorized --authorization-ref._

_What each mode sends_

| Behavior | Passive scan | Authorized active probe |
| --- | --- | --- |
| Unauthenticated requests to the resource | 2 by default | 2 by default |
| Sends a PAYMENT-SIGNATURE | No | One, deliberately invalid |
| Calls facilitator /verify | No | Once, with --facilitator |
| Calls facilitator /settle | No | No |
| Needs a wallet or private key | No | No |
| Requires the owner's approval reference | No | Yes |

## Example: a mainnet policy against a testnet endpoint

The demo endpoint advertises 10000 atomic units of a token on Base Sepolia (eip155:84532). A policy written for Base mainnet with a lower ceiling catches both differences.

**settlens scan with a local policy**

```text
$ 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 3f9a8296
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.
 
Total: 15 pass · 2 fail · 0 warn · 0 skip
```

Both failures are **high** severity, so the run exits with code `1` at the default `--fail-on high` threshold. That is the same signal the [CI gate](https://settlens.com/product/ci) uses to block a merge.

The recipient matched, so `policy.recipient` passed. EVM addresses are compared case-insensitively; other formats, such as Solana base58 addresses, must match exactly.

Without a policy the same endpoint passes all 13 checks, as in the transcript at the top of this page.

## Write the policy once

Flags cover one-off scans; the configuration file keeps the policy reviewed and versioned next to the endpoint it describes.

_Policy fields_

| Configuration field | CLI flag | Check |
| --- | --- | --- |
| `expected.schemes` | `--expect-scheme` | `policy.scheme` |
| `expected.networks` | `--expect-network` | `policy.network` |
| `expected.payTo` | `--expect-pay-to` | `policy.recipient` |
| `expected.assets` | `--expect-asset` | `policy.asset` |
| `expected.maxAmount` | `--max-amount` | `policy.maximum_amount` |
| `expected.resourceUrl` | Configuration only | `binding.resource_url` |

**settlens.config.json (excerpt)**

```json
{
  "expected": {
    "resourceUrl": "https://merchant.example/api/data",
    "schemes": [
      "exact"
    ],
    "networks": [
      "eip155:8453"
    ],
    "payTo": [
      "0xYourReviewedRecipient"
    ],
    "assets": [
      "0xYourReviewedTokenContract"
    ],
    "maxAmount": "10000"
  }
}
```

> **A scan is stricter than an agent**
>
> An agent pays with one option, so its [MCP preflight](https://settlens.com/mcp) only needs one acceptable option. A scan tests the server, so _every_ advertised option must be within policy. Amounts are atomic units: pair `maxAmount` with an asset allowlist. See [how to write an agent spending policy](https://settlens.com/learn/agent-spending-policy).

## The authorized active probe

A passive scan shows what the resource advertises. Only an invalid payment shows what it does with one. Settlens sends exactly one, and only when you prove you are allowed to.

### Three safety gates

`--active` alone is refused. The CLI also requires `--authorized`, your confirmation that the endpoint owner approved the test, and `--authorization-ref`, the ticket, email or scope identifier of that approval. Missing either one stops the run with exit code `2` before any request is sent. The configuration file has no field that turns active mode on, so a changed config cannot enable it.

### What the probe sends

Settlens builds a payment payload from the first advertised option and makes it unusable: on EVM networks a 65-byte all-zero signature from the zero address with a zero nonce, on Solana 64 zero bytes in place of a transaction, on other networks a placeholder string. It sends that payload once as `PAYMENT-SIGNATURE`. The probe needs a decodable V2 `PAYMENT-REQUIRED` with at least one option first; otherwise it is skipped.

### What must happen

- The resource must not answer with a 2xx status. A 2xx is a **critical** payment bypass.
- It should answer 400 or 402, as the x402 HTTP transport specifies.
- With `--facilitator`, Settlens posts the same payload to `/verify`, which must return a VerifyResponse with `isValid: false`.

The approval reference is recorded in the report. The report scope also declares zero settlement calls and zero private keys used: Settlens has no code path that settles or signs a payment, so these are fixed values, not measurements.

**Safety gates (no request is sent)**

```text
$ settlens scan https://merchant.example/api/data --active
Settlens error: Active testing requires --authorized to confirm explicit owner approval.
$ settlens scan https://merchant.example/api/data --active --authorized
Settlens error: Active testing requires --authorization-ref with a traceable owner approval reference.
```

**Bypass detected on the demo target**

```text
$ settlens scan http://127.0.0.1:4402/bypass \
    --active --authorized --authorization-ref DEMO-APPROVAL-002 --no-write
Settlens x402 assessment bc909b8c
Targets 1 · Active 1 · Settlement calls 0
 
127.0.0.1  GET http://127.0.0.1:4402/bypass
13 pass · 1 fail · 1 warn · 0 skip
  FAIL critical Invalid payment authorization is denied
       The endpoint returned HTTP 200 after an invalid payment authorization.
  WARN medium   Invalid payment uses a standard failure status
       Expected HTTP 400 or 402 for invalid payment, received HTTP 200.
 
Total: 13 pass · 1 fail · 1 warn · 0 skip
```

> **Authorized testing only**
>
> Run the active probe only against endpoints you own or have written permission to test. The demo target's /bypass route is built to unlock with any signature, which is exactly what the probe exists to catch.

**Report excerpt: authorized probe against the demo /secure route**

```json
{
  "scope": {
    "targetCount": 1,
    "activeTargetCount": 1,
    "settlementCalls": 0,
    "privateKeysUsed": 0
  },
  "targets": [
    {
      "mode": "active-authorized",
      "authorization": {
        "confirmed": true,
        "reference": "DEMO-APPROVAL-001"
      },
      "checks": [
        {
          "id": "security.invalid_signature_fail_closed",
          "status": "pass",
          "severity": "critical",
          "message": "The intentionally invalid payment was denied with HTTP 402.",
          "evidence": { "status": 402, "responseBytes": 47 }
        }
      ]
    }
  ]
}
```

## Checks in this stage

Rendered from the same catalog the CLI and the MCP server use. Severities are defaults; the [check reference](https://settlens.com/docs/checks) explains why each one matters and how to fix it.

_Handshake, schema, binding and stability (14 checks)_

| Check | Default severity | What it verifies |
| --- | --- | --- |
| `transport.reachable` ; Endpoint is reachable | high | Settlens received an HTTP response from the target URL within the timeout (8000 ms by default). Redirects are not followed. |
| `transport.status_402` ; Unauthenticated request requires payment | high | A request without any payment header returns HTTP 402 Payment Required. |
| `transport.payment_required_header` ; PAYMENT-REQUIRED header is present | high | The 402 response carries the PAYMENT-REQUIRED header defined by the x402 V2 HTTP transport. |
| `transport.payment_required_encoding` ; PAYMENT-REQUIRED decodes as Base64 JSON | high | The PAYMENT-REQUIRED value is at most 131072 characters, uses only Base64 or Base64URL characters and decodes to valid JSON nested at most 64 levels deep. |
| `transport.response_body_limit` ; Response body stays within the evidence limit | medium | The 402 response body fits within the 1 MiB processing limit. Settlens stops reading at the limit. |
| `transport.https` ; Transport protects payment metadata | medium | The endpoint URL uses HTTPS. Plain HTTP is accepted only for loopback hosts (localhost, 127.0.0.1, ::1) used in local testing. |
| `transport.redirect` ; Payment endpoint does not redirect unexpectedly | medium | Reported as a warning when the endpoint answers with a 3xx redirect. Settlens never follows redirects and records the sanitized Location instead. |
| `schema.official_x402_sdk` ; Official x402 SDK accepts PaymentRequired | high | The decoded PaymentRequired object passes the parsePaymentRequired schema of the pinned official @x402/core 2.28.0 SDK. |
| `schema.payment_required` ; PaymentRequired matches the x402 V2 core shape | high | x402Version equals 2, resource is an object whose url is an absolute http(s) URL, and accepts is a non-empty array. |
| `binding.resource_url` ; Payment requirement is bound to the requested resource | high | resource.url equals the URL that was requested, including path and query (the fragment is ignored). In evaluate_payment_requirements this check is a warning when no resource URL is supplied, because the binding cannot be confirmed. |
| `schema.payment_requirements` ; Accepted payment requirements are well formed | high | Every accepts entry has a non-empty scheme, a CAIP-2 network, a positive integer atomic amount, a non-empty asset and payTo, and a positive maxTimeoutSeconds. On eip155 networks payTo must be a 20-byte hex address. |
| `schema.duplicate_requirements` ; Payment choices are unambiguous | low | No two accepts entries are identical. |
| `freshness.timeout_window` ; Authorization windows are reasonably bounded | medium | Every maxTimeoutSeconds value is at most 3600 seconds (one hour). |
| `stability.payment_requirements` ; Payment requirements remain stable | medium | Repeated identical unauthenticated requests (2 by default) return the same resource, accepts and extensions, ignoring the known ephemeral fields recentBlockhash and lastValidBlockHeight. Skipped when only one sample is available. |

_Policy (5 checks, each evaluated only when its field is set)_

| Check | Default severity | What it verifies |
| --- | --- | --- |
| `policy.scheme` ; Schemes match the local policy | high | Every advertised scheme is in the policy's allowed schemes (case-insensitive). Evaluated only when a scheme policy is set. In the agent tools (evaluate_payment_requirements, scan_endpoint) it summarizes the options instead: pass when every option complies, warn when some do not, fail when none does; the decision then rests on policy.acceptable_option. |
| `policy.network` ; Networks match the local policy | high | Every advertised CAIP-2 network is in the policy's allowed networks (case-insensitive). Evaluated only when a network policy is set. In the agent tools (evaluate_payment_requirements, scan_endpoint) it summarizes the options instead: pass when every option complies, warn when some do not, fail when none does; the decision then rests on policy.acceptable_option. |
| `policy.recipient` ; Recipients match the local policy | high | Every advertised payTo is in the policy's allowed recipients (EVM hex addresses case-insensitively, other formats such as Solana base58 exactly). Evaluated only when a recipient policy is set. In the agent tools (evaluate_payment_requirements, scan_endpoint) it summarizes the options instead: pass when every option complies, warn when some do not, fail when none does; the decision then rests on policy.acceptable_option. |
| `policy.asset` ; Assets match the local policy | high | Every advertised asset (token contract address or mint) is in the policy's allowed assets (EVM hex addresses case-insensitively, other formats such as Solana mints exactly). Evaluated only when an asset policy is set. In the agent tools (evaluate_payment_requirements, scan_endpoint) it summarizes the options instead: pass when every option complies, warn when some do not, fail when none does; the decision then rests on policy.acceptable_option. |
| `policy.maximum_amount` ; Amounts stay within the approved ceiling | high | Every advertised atomic amount is at most the policy's maxAmount. Amounts are compared in atomic units, so pair maxAmount with an asset policy. Evaluated only when maxAmount is set. In the agent tools (evaluate_payment_requirements, scan_endpoint) it summarizes the options instead: pass when every option complies, warn when some do not, fail when none does; the decision then rests on policy.acceptable_option. |

_Authorized active probe (6 checks)_

| Check | Default severity | What it verifies |
| --- | --- | --- |
| `security.invalid_signature_fail_closed` ; Invalid payment authorization is denied | critical | With explicit authorization, Settlens sends one intentionally invalid PAYMENT-SIGNATURE (for example an all-zero EVM signature) and expects the resource not to answer with a 2xx status. |
| `transport.invalid_payment_status` ; Invalid payment uses a standard failure status | medium | The response to the intentionally invalid payment is HTTP 400 or 402. |
| `safety.active_authorization` ; Active probe has explicit authorization | high | An active probe was requested together with the owner-authorization confirmation and a non-empty authorization reference. Otherwise no probe is sent and this check fails. |
| `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. |

## What Assess does not do

- **No settlement, no funds.** Settlens never calls `/settle`, never creates a valid payment and never holds a wallet or payment key.
- **No route discovery.** You name the URLs. Settlens does not crawl or guess paid routes.
- **Sampled behavior.** A scan sees a few requests in a short window. It cannot prove every server-side code path, rate limit or tenant behaves the same.
- **One invalid payload class.** The active probe proves an obviously forged payment is refused. It is not a full test of your signature verification; use [offline verification](https://settlens.com/product/verify) to check captured authorizations.
- **No on-chain view.** Assess does not read balances, nonces, token contracts or transactions.
- **Redirects stop the scan's view.** A 3xx is reported and not followed; scan the canonical URL instead.
- **Not a certification.** A report is evidence of observed behavior, not a security audit or legal opinion. See the [methodology](https://settlens.com/docs/methodology).

## Questions about assessments

**Does a passive scan send a payment?**

No. It sends ordinary requests without any payment header. If you add a PAYMENT-SIGNATURE header yourself, the CLI refuses to run (exit code 2).

**How many requests does a scan make?**

Two to the resource by default (set with --repeat). With --facilitator, one more GET to /supported. The authorized probe adds one request to the resource and, with a facilitator, one POST to /verify.

**Can I enable the active probe from the configuration file?**

No. Active mode exists only as command-line flags, and it needs --active, --authorized and --authorization-ref together. If --active comes without the other two, the CLI stops before sending any request.

**Can a valid payment ever be sent by mistake?**

No. The probe payload carries an all-zero signature, zero bytes or a placeholder instead of a real authorization, Settlens has no payment key to sign with, and it never calls /settle.

**Does a passing scan mean my endpoint is secure?**

It means the endpoint behaved correctly for every property tested during the scan. It does not cover untested code paths, on-chain behavior or your wider application.

**Which x402 version is supported?**

x402 V2 over HTTP: the `PAYMENT-REQUIRED` and `PAYMENT-SIGNATURE` headers, validated with the official `@x402/core` 2.28.0 SDK. Responses without a V2 `PAYMENT-REQUIRED` header, including x402 V1 responses, fail these checks. See the [x402 V1 to V2 migration guide](https://settlens.com/learn/x402-v1-to-v2-migration).

## Related

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

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

- **[Verify](https://settlens.com/product/verify)**

  Offline EIP-3009 and Solana signature checks, and facilitator checks.

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

Reference: [CLI options and exit codes](https://settlens.com/docs/cli) · [configuration file](https://settlens.com/docs/configuration) · [every check](https://settlens.com/docs/checks) · [the x402 V2 headers](https://settlens.com/learn/x402-v2-headers) · [how to test an x402 endpoint](https://settlens.com/blog/how-to-test-x402-endpoint) · [check one PAYMENT-REQUIRED header in your browser](https://settlens.com/tools/x402-checker).

## Assess one paid endpoint first

A scoped pilot on one endpoint, with your policy, an authorized probe if you approve it, and a report your team can verify.

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

## Sources

1. [x402 protocol specification, version 2](https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md) — x402 Foundation
2. [x402 V2 HTTP transport](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md) — x402 Foundation
3. [CAIP-2: Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) — Chain Agnostic Standards Alliance
