Metadata-Version: 2.5
Name: settled-x402
Version: 0.2.0
Summary: Check an x402 quote against Settled's independent evidence before your agent pays, and hold it to a spending limit.
Project-URL: Homepage, https://settled.tools/for-agents
Project-URL: Documentation, https://settled.tools/for-agents
Project-URL: Source, https://github.com/justinedwardvolmer/settled-skill
Project-URL: Settled API, https://settled.tools/openapi.json
Author-email: Settled <developer@settled.tools>
License-Expression: MIT
License-File: LICENSE
Keywords: Base,Settled,USDC,agents,guard,payments,spending limit,x402
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: requests>=2.31; extra == 'dev'
Requires-Dist: x402[clients]>=2.0; extra == 'dev'
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == 'httpx'
Provides-Extra: requests
Requires-Dist: requests>=2.31; extra == 'requests'
Provides-Extra: x402
Requires-Dist: x402>=2.0; extra == 'x402'
Description-Content-Type: text/markdown

# settled-x402 (Python)

**Check an x402 quote against independent evidence before your agent signs it.**

`settled-x402` asks [Settled](https://settled.tools) 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](#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](#price-tiers): 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](#free-or-with-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`](https://www.npmjs.com/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.

```bash
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`](https://pypi.org/project/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:

```python
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`:

```python
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:

```python
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:

```python
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:

```python
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

```bash
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:

```python
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](https://settled.tools/loadout), 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](#free-or-with-a-pass)). 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](#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](https://settled.tools/terms)):

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

```python
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.

```python
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:

```python
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](https://x.com/settledfyi).

## License

MIT
