settled-x402
Check an x402 quote against independent evidence before your agent signs it.
settled-x402 asks Settled about every x402 payment before your agent signs it. Settled is an independent x402 index. It probes endpoints itself, buys from them with real USDC, and checks what comes back against each listing. For the payment in hand, the guard checks:
- The payTo. Is it the one Settled's own probe gets from the same URL at about the same time? A quote that pays someone else is how a spoofed endpoint, or a man in the middle, gets paid.
- Delivery. Did Settled's scout get real content back when it paid this endpoint, and did that content match the listing? The settlement transaction comes with the answer.
- The seller. Has the payTo ever been used on Base? Does the seller run a clone farm? What did paying agents report?
- The price. Is it higher than the one Settled was quoted?
Payments with a verdict you block, avoid by default, are stopped before anything is signed.
npm install settled-x402
Node 20 or later, no dependencies, ES modules (require() works on Node 20.19+ and 22.12+).
Set it up with @x402/fetch or @x402/axios
Wrap your x402 client in an x402HTTPClient and guard that. The guard then knows both the URL you requested and the exact option your client is about to pay:
import { x402Client, x402HTTPClient, wrapFetchWithPayment } from '@x402/fetch';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';
import { settledGuard } from 'settled-x402';
const client = new x402Client();
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.PRIVATE_KEY) });
const httpClient = settledGuard(new x402HTTPClient(client));
const fetchWithPayment = wrapFetchWithPayment(fetch, httpClient);
wrapAxiosWithPayment(axios.create(), httpClient) works the same way.
When the guard blocks a payment, nothing is signed. The call rejects with an error whose message contains Payment creation aborted: Settled: avoid for <url>: <reasons>.
You can also guard the plain x402Client with settledGuard(client). The guard then sees only the URL the 402 names (resource.url), which the seller writes. It still checks that URL's payTo against Settled. But a seller can leave the URL out and get unknown, which isn't blocked unless you block unknown. Prefer the x402HTTPClient form.
With x402-fetch (v1) or any payment wrapper that takes a fetch
import { wrapFetchWithPayment } from 'x402-fetch';
import { withSettled } from 'settled-x402';
const fetchWithPayment = wrapFetchWithPayment(withSettled(fetch, { network: 'base' }), walletClient);
withSettled sits under the payment wrapper. When an endpoint answers 402 with an x402 quote, the quote is checked before the wrapper sees it. A blocked verdict throws SettledBlockedError, whose error.result holds the full result, so the wrapper never signs.
withSettled can't see which option the wrapper will pick, so it judges every option in the quote, from the header and from the body, and reports the riskiest. Set network to the networks your wallet pays on so that options on other chains don't count.
With Coinbase AgentKit
AgentKit's x402 actions build their own x402 client and pay through the global fetch, so there is no client to hand to settledGuard. Guard the global fetch instead, and give the agent Settled's actions:
npm install settled-x402 @coinbase/agentkit
import { AgentKit, x402ActionProvider } from '@coinbase/agentkit';
import { guardGlobalFetch } from 'settled-x402';
import { settledActionProvider } from 'settled-x402/agentkit';
guardGlobalFetch({ network: 'base' });
const agentkit = await AgentKit.from({
walletProvider,
actionProviders: [x402ActionProvider(), settledActionProvider()],
});
guardGlobalFetch(options) puts withSettled under every fetch in the process. When make_http_request_with_x402 or retry_http_request_with_x402 gets a quote with a blocked verdict, the action fails with Settled: avoid for <url>: <reasons> before anything is signed. make_http_request fails the same way, so the agent finds out before it asks to pay. Anything that isn't a 402 with an x402 quote passes through untouched. Calling it again changes nothing, and the function it returns puts the original fetch back.
settledActionProvider(options) adds two actions. Neither pays anything.
SettledActionProvider_check_x402_endpoint | url, method (GET or POST) | The endpoint's quote, read without paying, with Settled's verdict and reasons, judged on the wallet's network |
SettledActionProvider_find_x402_endpoints | query, maxPriceUsd, limit | Live endpoints on the wallet's network, with price, quality score and whether Settled's test purchase delivered |
It takes pass (or reads SETTLED_PASS from the environment), network, timeoutMs, skipHosts, includeQuery and baseUrl. Without a pass, both actions count against the free 300 calls a day.
Discovery
AgentKit's discover_x402_services reads a facilitator's /discovery/resources list. Settled serves that format at https://settled.tools/discovery/resources, limited to endpoints it has checked: live, priced in USDC, probed in the last 3 days, and not failing Settled's last test purchase. Endpoints Settled has bought from successfully come first, and each item's metadata.settled holds the evidence.
- AgentKit 0.10 takes any URL as the facilitator. Tell the agent to use
https://settled.tools.
- Unreleased AgentKit (its main branch) only takes facilitators you register:
x402ActionProvider({ registeredFacilitators: { settled: 'https://settled.tools' } }). The agent then asks for settled.
Check one endpoint
import { check } from 'settled-x402';
const result = await check('https://api.example.com/paid', { quote: paymentRequired, network: 'base' });
result.verdict;
result.reasons;
result.options;
result.details;
Without a quote, the guard can only report Settled's record for the URL. To have the quote read for you, use inspect. It requests the endpoint without paying (GET, or POST with { method: 'POST' }) and checks the 402 it gets back:
import { inspect } from 'settled-x402';
const result = await inspect('https://api.example.com/paid', { network: 'base' });
result.endpoint;
From the command line
$ npx settled-x402 check https://api.bitrefill.com/x402/gift-cards/search
PAY · https://api.bitrefill.com/x402/gift-cards/search
quote $0.002 on base to 0x480C…846A (4 options)
options base: $0.002 to 0x480C…846A · payTo matches Settled
(arbitrum: $0.002 to 0x480C…846A · same address as on Settled's network)
(polygon: $0.002 to 0x480C…846A · same address as on Settled's network)
(solana: $0.002 to 5yASLj…NgRn · not on record at Settled)
the verdict covers the options on base; pick another with --network
Settled live, delivered · $0.002 on base to 0x480C…846A · scout's last purchase 2026-10-01 (tx 0x62cd…5a29)
why payTo matches what Settled sees for this endpoint (0x480C…846A)
a real payment by Settled's scout came back with content, tx 0x62cd…5a29
details https://settled.tools/v1/free/check?url=https%3A%2F%2Fapi.bitrefill.com%2Fx402%2Fgift-cards%2Fsearch
The command reads the endpoint's 402 quote without paying, then asks Settled about it.
- Network: by default the verdict covers the options on the network Settled has on record. Use
--network solana to judge another.
- Exit codes: 0 pay, 2 caution, 3 avoid, 4 unknown, 1 usage or internal error.
- Other flags:
--json prints the full result; --post reads a POST endpoint's quote.
Settled's MCP server over stdio
For Claude Desktop, Cursor and other clients that start MCP servers as a command:
{
"mcpServers": {
"settled": { "command": "npx", "args": ["-y", "settled-x402", "mcp"] }
}
}
Once you hold a day pass, add "env": { "SETTLED_PASS": "<pass>" }. Clients that support remote MCP servers can connect to https://settled.tools/mcp directly. In Claude Code that's claude mcp add --transport http settled https://settled.tools/mcp.
Verdicts
pay | The payTo matches what Settled sees, and nothing on record is against it | paid |
caution | Worth a look, not damning. Examples: a price above what Settled was quoted, a different address on another EVM chain, no on-chain history for the payTo, a paid call that didn't match its listing, failing probes | paid |
avoid | Strong evidence against paying: a payTo on Settled's network and asset that isn't the one Settled sees, a scout payment that came back empty, a clone farm, or most paying agents reporting failure. With a pass it also covers honeypot listings and circular payments | blocked |
unknown | Nothing to judge by. Settled has no quote for this network or URL, can't be reached, or your free checks are used up | paid |
skipped | Not sent to Settled: payments to settled.tools itself, skipHosts, and local or private addresses | paid, never blocked |
To block more, pass block, for example settledGuard(httpClient, { block: ['avoid', 'caution'] }). Add 'unknown' to fail closed when Settled can't vouch for a payment.
What the payTo check can and can't catch
The guard compares the option you're paying with what Settled's prober gets from the same URL at about the same time. Settled reuses a probe that is under 5 minutes old; otherwise it probes while you wait.
It catches:
- A quote that pays someone other than the one Settled sees on the same network, in any asset, including a different address hidden next to the right one.
- A quote whose header and body disagree.
- A 402 that claims to be a settled.tools route but pays someone else.
It can't catch:
- The endpoint itself switching its payTo for everyone. Settled sees the switch too. The scout's receipts were for the old address.
- Options on networks Settled has no quote for. Settled records the cheapest option of each endpoint. On other EVM chains the guard compares addresses: a different address is
caution, because honest sellers do this. In a sample of 385 live x402 quotes, 13 of the 93 sellers offering several EVM chains used a different address on one of them. On other chains, such as Solana when Settled recorded Base, the payTo can't be compared, so the verdict is unknown.
A seller that honestly uses different addresses for different assets on one network will get avoid for the asset Settled didn't record.
After a seller legitimately changes its payTo, payments can be blocked for up to about 10 minutes, until both caches refresh.
Options
settledGuard(client, options), withSettled(fetch, options), guardGlobalFetch(options), check(url, options) and inspect(url, options) all take:
block | ['avoid'] | Verdicts that stop the payment |
pass | none | A Settled day pass: switches to /v1/preflight and removes the daily cap |
network | all | Networks your wallet pays on, such as 'base' or ['base', 'eip155:137'], or 'auto' for the one Settled has on record. Used only when the selected option isn't known (withSettled, check). Without it, an endpoint that also offers a chain Settled has no quote for, like Bitrefill's Solana option, comes out unknown |
onVerdict | none | Called with every result, blocked or not. It isn't awaited |
timeoutMs | 8000 | How long to wait for Settled. A timeout gives unknown |
cacheTtlMs | 300000 | How long an answer is reused for the same URL |
skipHosts | [] | Hosts never sent to Settled, such as 'api.mycompany.com' or '*.mycompany.com' |
includeQuery | false | Also send the URL's query string to Settled |
allowPrivateHosts | false | Also send localhost and private-network URLs. Only useful with your own baseUrl |
fetch | globalThis.fetch | The fetch used to reach Settled. Pass one that uses your proxy if your agent needs one |
baseUrl | https://settled.tools | |
inspect also takes method ('GET' or 'POST'), body (a string; default '{}' for POST) and quoteTimeoutMs (default 15000).
Free, or with a day pass
Without a pass, the guard uses Settled's free check:
- 300 checks a day per client, counted by IP address and reset at 00:00 UTC.
- The 300 are shared with Settled's free MCP tools and
/v1/free/endpoints.
- At most 30 checks a minute.
When the free checks run out, verdicts are unknown and payments go through unless you block unknown.
A day pass costs $0.05 in USDC over x402. It lasts 24 hours and allows 120 requests a minute with no daily cap. It switches the guard to Settled's preflight, which adds seller settlement-integrity and honeypot checks. Buy one with the client you already have:
const res = await fetchWithPayment('https://settled.tools/v1/pass', { method: 'POST' });
const { pass } = await res.json();
const guarded = settledGuard(new x402HTTPClient(client), { pass });
What is sent to Settled
Sent: only the origin and path of the endpoint you're about to pay, with no query string, fragment or credentials, plus your IP address as with any request. Your keys, the payment and your request body are never sent. Local and private-network addresses are never sent.
The AgentKit search action also sends its keyword, network and price limit. inspect and the AgentKit check action read the quote from the endpoint directly, not through Settled.
Added to Settled's public index: an endpoint Settled hasn't seen before. Settled probes it itself, then adds it. Put the hosts of endpoints you'd rather keep out in skipHosts.
Signed: Settled's answers are signed (EIP-191). result.answer holds the answer, which you can verify at https://settled.tools/v1/verify or with any EVM library.
Limits of the evidence
- Delivery: Settled's scout only buys endpoints priced at $0.05 or less, and at most about once a week each. Pricier endpoints have no delivery evidence.
- Not advice: the data is observational and best-effort, and it is not financial advice. If you think a verdict is wrong, tell @settledfyi.
License
MIT