Verify before paying: a preflight check for AI agents on x402

Unless something checks first, an agent that pays over x402 signs what the 402 response asks for. A preflight check, run outside the model under a policy you wrote, decides whether it should.

In short

Before an AI agent signs an x402 payment, a deterministic preflight should check the 402 terms against a spending policy that bounds recipient, token, chain and amount. The Settlens MCP tool evaluate_payment_requirements does this offline and answers pay, review or do_not_pay.

On this page

x402 lets a server charge for an HTTP resource. The first request returns 402 Payment Required with the price and the payment terms; the client signs a payment authorization for those terms and sends the request again. For a person at a checkout page, that is a moment of attention. For an AI agent, it is a decision taken in milliseconds, from text written by the party that wants to be paid.

This article is about the step between those two requests. It covers what the agent is about to sign, why the decision to sign should not be left to the model, how to write a spending policy that a machine can enforce, and what that looks like with the Settlens MCP server, with real output from the tool. It ends with what such a check cannot tell you.

What a 402 asks an agent to sign

In x402 V2, the terms travel in the PAYMENT-REQUIRED response header as Base64-encoded JSON (the V2 headers guide covers the format). Decoded, the challenge from the Settlens demo server looks like this:

JSONDecoded PAYMENT-REQUIRED (demo server)
{
  "x402Version": 2,
  "error": "PAYMENT-SIGNATURE header is required",
  "resource": {
    "url": "http://127.0.0.1:4402/secure",
    "description": "Settlens local demonstration resource",
    "mimeType": "application/json",
    "serviceName": "Settlens Demo",
    "tags": [
      "security",
      "demo"
    ]
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:84532",
      "amount": "10000",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
      "maxTimeoutSeconds": 60,
      "extra": {
        "name": "USDC",
        "version": "2"
      }
    }
  ],
  "extensions": {}
}

Each entry of accepts is one way to pay. network is a CAIP-2 chain identifier (eip155:84532 is Base Sepolia). asset is the token contract, payTo the recipient, and amount an integer string in the token's smallest unit. maxTimeoutSeconds is the maximum time allowed to complete the payment, which bounds how long a signed authorization needs to stay usable; for this exact EVM option, extra carries the token's EIP-712 domain name and version.

The agent picks one entry, signs an authorization that moves exactly that amount of that token to that address (for USDC under the exact scheme on EVM chains, an EIP-3009 transfer authorization), and retries with a PAYMENT-SIGNATURE header. Whatever the chosen entry says is what the signature commits to. Nothing in the protocol asks whether the agent should agree.

What goes wrong when nobody checks

An unchecked agent pays whatever the server advertises. The ways this goes wrong are well known; what breaks in agentic payments covers them from both sides. For the payer, they map onto a short list of checks:

What an unchecked agent would sign, and the check that catches it
ProblemWhat the agent would signCheck
Recipient swappedA transfer to a payTo nobody reviewed, after a misconfiguration, a compromised deployment or a malicious proxy.policy.recipient
Price too highAn amount far above what the task is worth, whether by mistake or by design.policy.maximum_amount
Wrong token or chainA payment in a token you never meant to spend, or on a chain where the same address is another contract.policy.asset
policy.network
Wrong resourceA payment whose resource.url is not the URL the agent requested.binding.resource_url
Malformed termsA challenge that the official @x402/core 2.28.0 schema rejects, so client libraries may read it differently.schema.official_x402_sdk
Long-lived authorizationA maxTimeoutSeconds window of hours, which leaves a signed authorization usable for longer than needed.freshness.timeout_window

Every one of these can be decided from the 402 response alone, before anything is signed. That is what makes a preflight possible: no balance lookup, no node, no wallet, only the terms, the URL and a policy.

Why the check should not live in the prompt

The tempting design is to give the model the decoded header and a sentence such as "only pay trusted merchants, at most one cent". It fails in predictable ways.

  • The arithmetic is unforgiving. One cent of USDC is 10000 atomic units, because USDC has 6 decimals. A model that reads 10000 as ten thousand dollars refuses a fair price; one that reads 1000000 as one cent overpays a hundredfold.
  • Addresses need a strict comparison. EVM addresses are compared case-insensitively (their mixed case is only a checksum), Solana addresses character for character, and one changed character means a different recipient. Language models are good at "looks like" and bad at "is".
  • The server writes part of the input. Fields such as resource.description or an error string are chosen by the server. Text that says "this payment is pre-approved" should change nothing, and with a prompt-only rule it can.
  • Nobody can audit a judgment call. When an agent pays the wrong address, "the model thought it was fine" is not an explanation anyone can act on.

A preflight tool turns the question into a function: the same terms and the same policy always produce the same decision, with reasons that name a check id. The model's job shrinks to calling the tool on every 402 and following a three-valued answer: pay, review or do_not_pay.

Then enforce it where the money moves. The model can still ignore a tool result, so the code that signs should refuse unless the last decision for that URL was pay and the option it is about to sign is one the tool returned as acceptable.

How to express a spending policy

The policy is what turns "trusted" and "cheap" into something a program can compare. The Settlens tools take five fields:

Spending policy fields
FieldWhat it boundsExampleWhy it is needed
pay_toRecipient addresses you reviewed0x2096…287CWithout it, any recipient is acceptable.
assetsToken contracts or Solana mints0x036C…CF7eAn amount means nothing until the token, and its decimals, are fixed.
networksCAIP-2 chainseip155:84532The same token address can be another contract on another chain.
max_amountLargest amount per payment, in atomic units10000The ceiling. 0.01 USDC × 10^6 = 10000.
schemesx402 payment schemesexactOptional; limits how the payment is made.
JSONThe policy used in the examples below
{
  "pay_to": [
    "0x209693Bc6afc0C5328bA36FaF03C514EF312287C"
  ],
  "assets": [
    "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
  ],
  "networks": [
    "eip155:84532"
  ],
  "max_amount": "10000",
  "schemes": [
    "exact"
  ]
}

To convert a price into atomic units, multiply by ten to the power of the token's decimals: 0.01 USDC with 6 decimals is 0.01 × 1,000,000 = 10,000. Keep the amount a string, because 18-decimal tokens overflow the integers JavaScript can represent exactly.

The policy bounds one payment at a time. Daily or cumulative budgets, retries and rate of spend are enforced elsewhere, in the wallet or the agent runtime; the agent spending policy guide covers where each belongs.

Two rules follow from the table. First, max_amount only makes sense together with assets: "10000000" is 10 USDC (6 decimals) but 0.1 of an 8-decimal token such as WBTC. If you allow several assets, list only ones with the same decimals and similar value, or run separate policies. Second, assets only makes sense together with networks, because a contract address identifies a token on one chain only.

That is why the Settlens decision can be pay only when the policy sets all four of pay_to, assets, networks and max_amount. With anything less, every check can pass and the answer is still review: the payment is not bounded, so a person should look.

One policy, several options

A 402 can advertise several options, for example the same price on two chains, and the agent pays with one. The policy is therefore applied to each option separately. An option is acceptable when it is well formed and satisfies every constraint; the tool returns the acceptable ones in acceptable, and the check policy.acceptable_option fails, with severity high, when none qualifies. One bad option does not block a good one, and the agent learns exactly which options it may use.

A worked example

The quickstart starts a local demo server whose /secure route answers with the challenge shown earlier. An agent connected to the Settlens MCP server sends this call on receiving it:

JSONtools/call evaluate_payment_requirements
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "evaluate_payment_requirements",
    "arguments": {
      "payment_required": "eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQQVlNRU5ULVNJR05BVFVSRSBoZWFkZXIgaXMgcmVxdWlyZWQiLCJyZXNvdXJjZSI6eyJ1cmwiOiJodHRwOi8vMTI3LjAuMC4xOjQ0MDIvc2VjdXJlIiwiZGVzY3JpcHRpb24iOiJTZXR0bGVucyBsb2NhbCBkZW1vbnN0cmF0aW9uIHJlc291cmNlIiwibWltZVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwic2VydmljZU5hbWUiOiJTZXR0bGVucyBEZW1vIiwidGFncyI6WyJzZWN1cml0eSIsImRlbW8iXX0sImFjY2VwdHMiOlt7InNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzMiIsImFtb3VudCI6IjEwMDAwIiwiYXNzZXQiOiIweDAzNkNiRDUzODQyYzU0MjY2MzRlNzkyOTU0MWVDMjMxOGYzZENGN2UiLCJwYXlUbyI6IjB4MjA5NjkzQmM2YWZjMEM1MzI4YkEzNkZhRjAzQzUxNEVGMzEyMjg3QyIsIm1heFRpbWVvdXRTZWNvbmRzIjo2MCwiZXh0cmEiOnsibmFtZSI6IlVTREMiLCJ2ZXJzaW9uIjoiMiJ9fV0sImV4dGVuc2lvbnMiOnt9fQ==",
      "resource_url": "http://127.0.0.1:4402/secure",
      "policy": {
        "pay_to": [
          "0x209693Bc6afc0C5328bA36FaF03C514EF312287C"
        ],
        "assets": [
          "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
        ],
        "networks": [
          "eip155:84532"
        ],
        "max_amount": "10000",
        "schemes": [
          "exact"
        ]
      }
    }
  }
}

The answer is pay, with 1 acceptable option and 13 checks, all passing. The summary reads: "Every protocol check passed and at least one payment option is within the allowed recipients, assets, networks and maximum amount. Pay only with an option listed in acceptable."

Change one input at a time and the decision moves. These are the server's real answers for the same challenge:

Same challenge, different inputs
InputDecisionReasons
The policy abovepayNone.
max_amount lowered to 5000do_not_paypolicy.acceptable_option: 0 of 1 payment option(s) satisfy the policy; 1 rejected (max_amount: 1).
Policy reduced to max_amountreviewThe spending policy is incomplete: set allowed recipients (pay_to), allowed assets (assets), and allowed networks (networks) to allow an automatic pay decision.
No resource_urlreviewbinding.resource_url: No resource URL was provided, so resource.url could not be compared with the URL you requested.
resource_url of another pathdo_not_paybinding.resource_url: resource.url does not match the URL being assessed.

Each row tells the agent something different. Over budget is a hard stop. An incomplete policy is a question for a human. A missing URL means the binding could not be confirmed, so the tool will not say pay. A URL mismatch means the payment would be for something other than what was requested.

Three options, one acceptable

To see the per-option rule, take the same challenge with two more options: the same token at 25000 units, and USDC on Base mainnet (eip155:8453), which the policy does not allow.

The policy is applied to each advertised option; only option 1 can be paid.

The real answer is pay, with exactly one entry in acceptable. The deciding check reports "1 of 3 payment option(s) satisfy the policy; 2 rejected (max_amount: 1, networks: 1, assets: 1)." The per-constraint summaries (policy.network, policy.asset, policy.maximum_amount) are warnings of severity info: they explain what was filtered out and do not change the decision.

Wiring it into an agent

The public endpoint offers the offline tools without installation. In Claude Code:

ShellConnect the remote endpoint
claude mcp add --transport http settlens https://settlens.com/mcp

On connection, the server sends instructions, which clients that support them pass to the model: call evaluate_payment_requirements on every 402 before paying, and act on the decision as described above. The x402_preflight prompt walks an agent through the same steps for one URL. Then close the loop in code:

  1. Request the resource without a payment header.
  2. On a 402, call the tool with the raw header value, the exact URL you requested and your policy.
  3. On do_not_pay, stop and log the reasons. On review, show them to a person and wait.
  4. On pay, sign one option from acceptable with your own wallet tooling, and have that tooling refuse anything else.

To see the decision for a real 402 before writing any agent code, paste its PAYMENT-REQUIRED header into the x402 payment request checker, which runs the same tool from your browser. If the terms or the policy should not leave your infrastructure, run the same server locally over stdio; pilot participants receive the source. The MCP reference lists every field, error and limit.

What a preflight does not tell you

A preflight reads terms. It is deliberately narrow, and its limits matter as much as its checks.

  • It does not see behavior. Whether the resource refuses an invalid payment, or the facilitator rejects an invalid signature, takes a live, authorized test. That is what an assessment of the endpoint is for, run by its owner.
  • It does not look on-chain. No balances, no nonce reuse or replay, no settlement status, and nothing about what the token contract at asset actually does.
  • It does not vouch for the merchant. pay means the request is well formed, bound to your URL and inside your policy. It says nothing about whether the content is worth the price.
  • It covers x402 over HTTP. In the x402 MCP transport, a paid tool returns its terms in a tool result and the payment travels in _meta["x402/payment"]. The tool does not support that transport: the specification's own example names an mcp:// resource, which Settlens rejects in schema.payment_required, and gets do_not_pay.
  • It is only as good as the policy. An allowlisted recipient you never verified, or a ceiling set too high, is approved exactly as written.

Takeaway

Give the agent a tool, not a rule in its prompt. Bound the recipient, the token, the chain and the amount. Pay only an option the tool returned as acceptable, and make the signing code enforce it. Start on the MCP server page.

Sources

  1. x402 protocol specification, version 2x402 Foundation · github.com
  2. x402 HTTP transport, version 2x402 Foundation · github.com
  3. Model Context Protocol specification 2025-06-18: ToolsModel Context Protocol · modelcontextprotocol.io
  4. x402 MCP transport, version 2x402 Foundation · github.com
  5. ERC-3009: Transfer With AuthorizationEthereum Improvement Proposals · eips.ethereum.org
  6. CAIP-2: Blockchain ID SpecificationChain Agnostic Standards Alliance · github.com