Skip to main content

settled-x402 (Python)

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. The same guard holds every payment to the spending limits you set: a cap per call, a daily budget that stops the agent, and an "ask me above $X" dial. Every result also carries the price's tier: common (free), rare (up to $1 a call) or legendary (over $1). The free check covers 25 calls a day; with renew=True the guard keeps a pass of its own, bought with the agent's wallet for $1 a month, and never runs out.

This is the Python twin of the npm package settled-x402: the same rules, the same verdicts, the same messages, and the same spend ledger and pass files, so an agent written in either language gets the same answer and one machine keeps one tally and one pass.

pip install settled-x402

Python 3.10 or later. No dependencies of its own; it reaches Settled with the standard library. The guard works with the x402 package 2.x (pip install "x402[clients]").

Set it up with the x402 client

Wrap your x402 client in an x402HTTPClient (or x402HTTPClientSync) and guard that. The guard then knows both the URL you requested and the exact option your client is about to pay:

from x402.client import x402Client
from x402.http.x402_http_client import x402HTTPClient
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.evm.exact import register_exact_evm_client   # your scheme, as in x402's docs
from settled_x402 import settled_guard

client = x402Client()
register_exact_evm_client(client, signer)

http = settled_guard(x402HTTPClient(client), max_usd_per_call=1, daily_budget_usd=5)
async with x402HttpxClient(http) as session:
    res = await session.get("https://api.example.com/paid")

The sync client works the same way with requests:

from x402.client import x402ClientSync
from x402.http.x402_http_client import x402HTTPClientSync
from x402.http.clients.requests import x402_requests
from settled_x402 import settled_guard, SettledBlockedError

http = settled_guard(x402HTTPClientSync(client), max_usd_per_call=1)
session = x402_requests(http)
try:
    res = session.get("https://api.example.com/paid")
except SettledBlockedError as e:
    print(e)                 # Settled: avoid for https://api.example.com/paid: this quote's payTo on base (0x0000…dEaD) is not the payTo Settled sees ...
    print(e.result.verdict, e.blocked_by, e.result.reasons)

When the guard blocks a payment, nothing is signed. The call raises SettledBlockedError, which is also x402's PaymentAbortedError and the PaymentError of x402's httpx transport and requests adapter, so handlers you already have catch it. error.result holds the full result.

You can also guard the plain x402Client with settled_guard(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.

When a toolkit builds its own x402 client

Agent frameworks' x402 tools often create the client themselves and never hand it to you. guard_all() guards every x402 client created after it is called, with the same options, and returns a function that undoes it:

from settled_x402 import guard_all

guard_all(max_usd_per_call=0.25, daily_budget_usd=5, only_hosts=["api.exa.ai", "*.agent402.tools"])
# ... build the agent; every x402Client / x402ClientSync it makes is guarded

For an httpx stack with no client object in reach at all, SettledAsyncTransport / SettledTransport sit under x402's transport and read the quote out of the 402 before the payment layer sees it. They can't tell which option will be paid, so they judge every option, report the riskiest, and hold the most expensive option to your limits:

import httpx
from x402.http.clients.httpx import x402_httpx_transport
from settled_x402 import SettledAsyncTransport

inner = SettledAsyncTransport(network="base", max_usd_per_call=1)
session = httpx.AsyncClient(transport=x402_httpx_transport(client, transport=inner))

Check one endpoint

check asks Settled about a URL and compares a quote you already have. Nothing is paid. inspect reads the endpoint's 402 for you first (GET by default), then checks it:

from settled_x402 import check, inspect, acheck, ainspect

r = check("https://api.example.com/paid", quote=payment_required)   # payment_required: the 402's body/header, dict or x402 model
r = inspect("https://api.example.com/paid", method="POST")
print(r.verdict, r.blocked, r.rarity, r.reasons)
print(r.quote.pay_to, r.settled.pay_to, r.options)                  # the comparison, option by option
print(r.to_dict())                                                   # JSON-ready

acheck and ainspect are the same off the event loop. Both apply your limits without counting or asking, so an agent that checks first learns whether a payment would be stopped (r.spend.decision) or the operator asked ("ask").

From the command line

settled-x402 check https://api.example.com/paid            # reads the 402 (nothing is paid) and asks Settled
settled-x402 check https://api.example.com/paid --post --json
settled-x402 check https://api.example.com/paid --network base
settled-x402 spend                                         # today's tally from the spend ledger
settled-x402 pass                                          # the pass a guard with renew keeps: term, expiry, when bought
python -m settled_x402 check <url>                         # the same without the console script
PAY · https://x402.ottoai.services/whois-lookup · rare loot (up to $1 a call)
  quote     $0.001 on base to 0x0E84…b808 (2 options)
  options   base: $0.001 to 0x0E84…b808 · payTo matches Settled
            (solana: $0.001 to 6XcSfq…DPN5 · not on record at Settled)
  Settled   live, delivered · $0.001 on base to 0x0E84…b808 · scout's last purchase 2026-10-02 (tx 0xf17c…9b60)
  why       payTo matches what Settled sees for this endpoint (0x0E84…b808)
            a real payment by Settled's scout came back with content, tx 0xf17c…9b60
  details   https://settled.tools/v1/free/check?url=https%3A%2F%2Fx402.ottoai.services%2Fwhois-lookup

Exit codes: 0 pay, 2 caution, 3 avoid, 4 unknown, 1 usage or internal error. Environment: SETTLED_PASS, SETTLED_PAYER_TOKEN, SETTLED_BASE_URL, SETTLED_ALLOW_PRIVATE_HOSTS=1, SETTLED_BUDGET_FILE, SETTLED_PASS_FILE. check uses the pass a renewing guard keeps when no --pass or SETTLED_PASS is given.

Verdicts

Verdict What it means By default
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 seller on hold (it took the scout's payment twice in 30 days and delivered nothing), 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, skip_hosts, and local or private addresses paid, never blocked

To block more, pass block, for example settled_guard(http, block=["avoid", "caution"]). Add "unknown" to fail closed when Settled can't vouch for a payment.

Spending limits

The verdict is about the seller. The limits are about your money, and the same guard enforces both, where the payment is about to be signed:

settled_guard(http,
    max_usd_per_call=1,       # never sign a call priced over $1
    daily_budget_usd=5,       # stop once today's calls would add up to more than $5
    ask_above=0.25,           # ask before paying more than 25¢ for one call
    ask=lambda q: operator_says_yes(f"Pay ${q.price_usd} ({q.rarity}) to {q.url}? ${q.remaining_usd} of today's budget left."),
)
Option What it does
max_usd_per_call A call priced over this is stopped. 0 pays nothing: free endpoints only
daily_budget_usd A call that would take today's total (UTC) over this is stopped. The total counts what the guard let through, which is the most that can have been paid. It resets at 00:00 UTC
ask_above The softer dial. A call priced over this is put to the operator first; 0 asks about every paid call. With ask, your function gets the call (SpendQuestion) and answers True or False; with the async client, ask may be a coroutine function and runs on your event loop. Without it, the guard asks on the terminal when the process has one (ask_timeout_s, default 120; silence is a no), and otherwise stops the call, so a headless agent with ask_above and no ask pays only below the line
budget_file Where the daily total lives, so a restart doesn't reset it: default SETTLED_BUDGET_FILE, else ~/.settled-x402/spend.json, the same file the npm package uses, so a Node guard and a Python guard on one machine share one tally. False keeps it in memory
ledger Your own store for the total (an object with load() and save(state)), for agents that run as several processes
only_hosts The loadout: the only hosts the agent may pay, such as ["api.exa.ai", "*.agent402.tools"]. A quote from anywhere else is stopped whatever Settled says (blocked_by == "loadout"); settled.tools itself stays payable, so a pass still goes through

A stopped call raises SettledBlockedError like a blocked verdict, and the message leads with what stopped it (Settled: over your daily budget for <url>: $2.50 would take today's spend to $6.00, over your daily_budget_usd ($5.00; $3.50 spent so far); the budget resets at 00:00 UTC). The error's blocked_by is "limit", "operator" or "loadout", and spend says which limit, what the call cost and what is left today. spent_today() reads the tally, and settled-x402 spend prints it.

Three things to know. A quote whose price the guard can't read in USD (an asset it doesn't know) can't be held to a limit, so with any limit set it is stopped. The daily total is kept per ledger: guards with the same budget_file share it (within one process their reads and writes are serialised; across processes a call let through at the same instant by another process can be missed, so give agents that run as several processes a ledger of their own), and guards with memory ledgers each keep their own. And a quote with several options is held to its most expensive one when the guard can't tell which will be paid (the transports); settled_guard knows the selected option and holds the payment to exactly that price.

The x402 client has a cap of its own, set_spend_controls({"max_amount_per_payment": "$1"}), $1 per payment by default. It is a cap per payment on the client; it has no daily total, no asking and no verdict. Both can be on at once.

Price tiers

Every result says what a call costs as a tier, so an agent choosing between endpoints, a log line or a dashboard can use one word for the cost class instead of a number:

Tier Price per call Color
common free grey
rare up to $1 blue
legendary over $1 gold

The line at $1 is where the x402 client's own default cap sits (maxAmountPerPayment: "$1"), so a legendary call is one the client would refuse out of the box. It is also a caution on its own ("it costs $2.50 per call"), and ask_above=1 is the dial that makes the operator confirm every one. The tier is result.rarity (from the quote's price, else Settled's recorded price; an endpoint Settled gets for free is common), rarity_of(price_usd) for any price, and RARITIES for labels and colors. The names come from the loadout builder, which shows an agent's services as loot; the API's /v1/loot?tier= uses the same three.

Options

settled_guard(client, **options), guard_all(**options), SettledAsyncTransport(**options), check(url, quote=None, **options) and inspect(url, **options) all take:

Option Default
block ["avoid"] Verdicts that stop the payment
pass_token none A Settled pass (a day, 30 days or a year): switches to /v1/preflight and removes the daily cap
renew off Keep a pass going without a human: True (a 30-day pass, at most $1), a term ("day", "month", "year") or a dict with pass, up_to_usd, pass_file, store, on_renew, on_error, pay (details). Guards and transports only; check and inspect use pass_token
payer_token none The agent's payer record as a token (x4t_… from POST /x401/token, 24 hours): the free check counts the client by wallet, 300 a day instead of 25 per address. A refused token falls back to an anonymous check with a note; a pass takes precedence
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 (the transports, check). Without it, an endpoint that also offers a chain Settled has no quote for comes out unknown
on_verdict none Called with every result, blocked or not; its errors are ignored
timeout_s 8 How long to wait for Settled. A timeout gives unknown
cache_ttl_s 300 How long an answer is reused for the same URL
skip_hosts [] Hosts never sent to Settled, such as "api.mycompany.com" or "*.mycompany.com"
include_query False Also send the URL's query string to Settled
allow_private_hosts False Also send localhost and private-network URLs. Only useful with your own base_url
fetch urllib How to reach Settled: fetch(url, headers, timeout_s) -> (status, headers, text). Pass your own to go through a proxy
post urllib How the renewer reaches Settled's pass routes: post(url, headers, body, timeout_s) -> (status, headers, text)
base_url https://settled.tools
max_usd_per_call none Stop any call priced over this many dollars (Spending limits)
daily_budget_usd none Stop once today's calls would add up to more than this
ask_above none Ask the operator before paying more than this for one call
ask terminal prompt Who to ask: ask(question) -> bool, with url, price_usd, rarity, remaining_usd and the full result
ask_timeout_s 120 How long the terminal prompt waits; silence is a no
budget_file SETTLED_BUDGET_FILE, else ~/.settled-x402/spend.json Where the daily total is kept; False for memory only
ledger the file Your own store for the total: load(), save(state)
only_hosts any host The loadout: the only hosts the agent may pay; everything else is stopped (settled.tools stays payable)

inspect also takes method ("GET" or "POST"), body (a string; default "{}" for POST) and quote_timeout_s (default 15).

Free, or with a pass

Without a pass, the guard uses Settled's free check:

  • 25 checks a day for an anonymous client, counted by IP address and reset at 00:00 UTC; 300 a day for an agent that shows a Settled payer record (free, counted by wallet; https://settled.tools/for-agents#payer-record).
  • To show the record from the guard, pass payer_token: a token from POST https://settled.tools/x401/token with the record (it lasts 24 hours and is bound to the wallet). The guard sends it as Authorization: Bearer on each free check; result.payer_record says whether Settled accepted it, and a refused token falls back to an anonymous check rather than losing the verdict.
  • Those are shared with Settled's free MCP tools and /v1/free/endpoints. Every free answer says how many are left (free.left_today) and, near the cap, how to continue.
  • At most 30 checks a minute.

When the free checks run out, verdicts are unknown and payments go through unless you block unknown.

A pass removes the daily cap, allows 120 requests a minute and switches the guard to Settled's preflight, which adds seller settlement-integrity and honeypot checks. It is paid in USDC over x402, and there are three terms (terms of sale):

Term Route Price
a day POST /v1/pass $0.05
30 days POST /v1/pass/month $1
a year POST /v1/pass/year $10

A pass that renews itself

The simplest way to hold one is to let the guard buy it:

http = settled_guard(x402HTTPClientSync(client), renew=True)

Before the first check, and again an hour before each pass runs out, the guard buys a 30-day pass with the client it guards: it POSTs to /v1/pass/month, reads the price in Settled's 402, and signs only if that price is within up_to_usd, which is the term's list price unless you set it (renew={"up_to_usd": 1.5} says: keep renewing through a price rise up to $1.50; a 402 above it is not paid, the guard carries on with the free check and on_error says why). The pass is kept in SETTLED_PASS_FILE, else ~/.settled-x402/pass.json, the same file the npm package uses, so a restart does not buy another one and settled-x402 pass shows it. A pass Settled refuses (expired, invalid) is forgotten and the next check buys a new one. A pass_token you give by hand alongside renew is used first, until Settled refuses it. Several checks arriving at once share one purchase; a failed purchase is retried after 10 minutes.

That purchase is the guard's own. It is held to up_to_usd and to nothing else: not your only_hosts, not max_usd_per_call, not the daily budget, which are for the agent's spending. result.renew on every result says which term is kept, when the pass in hand expires and the last renewal error, if any; on_renew is called once per purchase with the pass, its expiry and the price.

settled_guard(http, renew={
    "pass": "year",                 # "day" | "month" (default) | "year"
    "up_to_usd": 12,                # the most to pay for one term; default: the term's list price
    "pass_file": "/var/lib/agent/settled-pass.json",   # or False for memory only; or "store": an object with load() and save(state)
    "on_renew": lambda r: log.info("Settled pass renewed until %s for $%s", r["expires_at"], r["price_usd"]),
    "on_error": lambda e: log.warning("%s", e),
})

With the async x402Client, the purchase is signed on your event loop while the check's worker thread waits, like an async ask handler. Under SettledTransport / SettledAsyncTransport there is no client to pay with, so give pay, the call that pays (your x402 session's post, or any callable taking a URL and answering a response); the guard sees that purchase's 402 like any other and lets it through within the cap, and the price is read with a plain POST and held to the cap before pay is called, whatever pay is built on. A year pass costs $10 in one payment, over the x402 client's default cap of $1 per payment, so for "pass": "year" raise the client's spend_controls (client.set_spend_controls({"max_amount_per_payment": "$10"}), or False); otherwise the client refuses the purchase and on_error carries its message.

stored_pass() returns the pass a renewing guard keeps on the machine, while it is valid, for code that cannot renew but should use it; the CLI does that on its own. To buy a pass yourself instead, with the client you already have:

res = session.post("https://settled.tools/v1/pass/month")
http = settled_guard(x402HTTPClientSync(client), pass_token=res.json()["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. inspect reads 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 skip_hosts.

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 buys from each seller in turn, usually that seller's cheapest endpoint: sellers listed up to $0.25 about once a week, sellers whose endpoints all cost $0.25 to $1.00 about once every 60 days, and nothing over $1.00. So delivery evidence covers at least one endpoint per seller, not every endpoint, can be weeks old for dearer sellers, and does not exist for endpoints over $1.00.
  • 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

Metadata

Release files for settled-x402 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for settled-x402 0.2.0
File Size Uploaded Provenance
settled_x402-0.2.0.tar.gz 65.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for settled-x402 0.2.0
File Interpreter ABI Platform
settled_x402-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 117.9 kB

Release files / settled_x402-0.2.0.tar.gz

Download URL settled_x402-0.2.0.tar.gz
Size 65.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6271e7fe159870dc121baaa922b7b9f101c0b621f5be2c70591eb2221548b7e3
BLAKE2b-256 checksum
How to use checksums
2c7a741b4705d5531e3b3af759ce5b58bfc323f2ccae5008a111e0b44b8d022c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / settled_x402-0.2.0-py3-none-any.whl

Download URL settled_x402-0.2.0-py3-none-any.whl
Size 52.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
108701e35dbe9840aa3c00e2dc2771358813829596367edac6a09b4bb900c648
BLAKE2b-256 checksum
How to use checksums
7556fa6b14efa8af7b6d331e111228ef339ca1468748966d943ed81d7ae42a3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for settled-x402 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page