---
title: "MCP preflight: verify x402 requests before paying | Settlens"
description: "Why AI agents need a deterministic check before signing an x402 payment, how to write a spending policy in atomic units, and a worked example with real output."
url: https://settlens.com/blog/mcp-verify-before-paying
last_updated: 2026-10-13
published: 2026-10-13
author: "Alessandro Gagliardi"
---

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

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

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.

[x402](https://settlens.com/learn/what-is-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](https://settlens.com/mcp), 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](https://settlens.com/learn/x402-v2-headers) covers the format). Decoded, the challenge from the Settlens demo server looks like this:

**Decoded PAYMENT-REQUIRED (demo server)**

```json
{
  "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](https://settlens.com/learn/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](https://settlens.com/blog/agentic-payments-what-breaks) 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_

| Problem | What the agent would sign | Check |
| --- | --- | --- |
| Recipient swapped | A transfer to a `payTo` nobody reviewed, after a misconfiguration, a compromised deployment or a malicious proxy. | `policy.recipient` |
| Price too high | An amount far above what the task is worth, whether by mistake or by design. | `policy.maximum_amount` |
| Wrong token or chain | A 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 resource | A payment whose `resource.url` is not the URL the agent requested. | `binding.resource_url` |
| Malformed terms | A challenge that the official @x402/core 2.28.0 schema rejects, so client libraries may read it differently. | `schema.official_x402_sdk` |
| Long-lived authorization | A `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_

| Field | What it bounds | Example | Why it is needed |
| --- | --- | --- | --- |
| `pay_to` | Recipient addresses you reviewed | `0x2096…287C` | Without it, any recipient is acceptable. |
| `assets` | Token contracts or Solana mints | `0x036C…CF7e` | An amount means nothing until the token, and its decimals, are fixed. |
| `networks` | CAIP-2 chains | `eip155:84532` | The same token address can be another contract on another chain. |
| `max_amount` | Largest amount per payment, in atomic units | `10000` | The ceiling. 0.01 USDC × 10^6 = 10000. |
| `schemes` | x402 payment schemes | `exact` | Optional; limits how the payment is made. |

**The policy used in the examples below**

```json
{
  "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](https://settlens.com/learn/agent-spending-policy) 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](https://settlens.com/docs/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:

**tools/call evaluate_payment_requirements**

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

| Input | Decision | Reasons |
| --- | --- | --- |
| The policy above | `pay` | None. |
| `max_amount` lowered to `5000` | `do_not_pay` | policy.acceptable_option: 0 of 1 payment option(s) satisfy the policy; 1 rejected (max_amount: 1). |
| Policy reduced to `max_amount` | `review` | The spending policy is incomplete: set allowed recipients (pay_to), allowed assets (assets), and allowed networks (networks) to allow an automatic pay decision. |
| No `resource_url` | `review` | binding.resource_url: No resource URL was provided, so resource.url could not be compared with the URL you requested. |
| `resource_url` of another path | `do_not_pay` | binding.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.

_Diagram: Three payment options pass through one policy. Option 1, exact on eip155:84532 for 10000 units, is acceptable. Option 2, for 25000 units, is rejected for max_amount. Option 3, on eip155:8453 with another asset, is rejected for networks and assets. policy.acceptable_option passes with 1 of 3 options._

_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:

**Connect the remote endpoint**

```shell
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](https://settlens.com/tools/x402-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](https://settlens.com/docs/mcp) 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](https://settlens.com/product/assess) 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](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 HTTP transport, version 2](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md) — x402 Foundation
3. [Model Context Protocol specification 2025-06-18: Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) — Model Context Protocol
4. [x402 MCP transport, version 2](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/mcp.md) — x402 Foundation
5. [ERC-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) — Ethereum Improvement Proposals
6. [CAIP-2: Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) — Chain Agnostic Standards Alliance
