Assess an x402 endpoint before agents pay it
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.
Real output from the bundled demo target (npm run demo:server).
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
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.
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
How a scan runs
-
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.
-
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.
-
Check binding and each option
resource.url is compared with the URL under test, and every accepts entry is validated field by field.
-
Repeat to detect drift
The same request is sent again (
--repeat, two samples by default) and the terms are compared, ignoring known ephemeral fields. -
Apply your policy
Every advertised option is compared with the allowed schemes, networks, recipients, assets and maximum amount you set.
-
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-reportor sign as 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.
| Behavior | Passive scan | Authorized active probe |
|---|---|---|
| Unauthenticated requests to the resource | 2 by default | 2 by default |
| Sends a PAYMENT-SIGNATURE | One, deliberately invalid | |
| Calls facilitator /verify | Once, with --facilitator | |
| Calls facilitator /settle | ||
| Needs a wallet or private key | ||
| Requires the owner's approval reference |
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 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 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.
| 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 |
{
"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 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.
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 withisValid: 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.
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.
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.
{
"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 explains why each one matters and how to fix it.
| Check | Default severity | What it verifies |
|---|---|---|
transport.reachableEndpoint 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_402Unauthenticated request requires payment | high | A request without any payment header returns HTTP 402 Payment Required. |
transport.payment_required_headerPAYMENT-REQUIRED header is present | high | The 402 response carries the PAYMENT-REQUIRED header defined by the x402 V2 HTTP transport. |
transport.payment_required_encodingPAYMENT-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_limitResponse 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.httpsTransport 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.redirectPayment 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_sdkOfficial 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_requiredPaymentRequired 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_urlPayment 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_requirementsAccepted 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_requirementsPayment choices are unambiguous | low | No two accepts entries are identical. |
freshness.timeout_windowAuthorization windows are reasonably bounded | medium | Every maxTimeoutSeconds value is at most 3600 seconds (one hour). |
stability.payment_requirementsPayment 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. |
| Check | Default severity | What it verifies |
|---|---|---|
policy.schemeSchemes 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.networkNetworks 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.recipientRecipients 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.assetAssets 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_amountAmounts 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. |
| Check | Default severity | What it verifies |
|---|---|---|
security.invalid_signature_fail_closedInvalid 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_statusInvalid payment uses a standard failure status | medium | The response to the intentionally invalid payment is HTTP 400 or 402. |
safety.active_authorizationActive 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_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. |
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 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.
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.
Sources
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.