# Python with x402: a free trial call, then one paid call step by step

Last updated: 2026-10-12

One free trial call, then one paid call to `POST /v1/verify/quote` (US$0.01) with `httpx` and the official [`x402`](https://pypi.org/project/x402/) package, showing each step of an x402 payment: read the offer, check it, sign it, send the request again, read the settlement, then check the receipt for free. Use this to call AttestPage from a Python agent, or to see what an x402 client does for you.

Payments run on Base (`eip155:8453`), in USDC.

## Install

Python 3.10 or later.

```sh
python3 -m venv .venv && . .venv/bin/activate
pip install "x402[evm,httpx]"
```

The `evm` extra brings `eth-account` for signing; the `httpx` extra brings `httpx`.

The trial call needs no wallet. To pay, set `ATTESTPAGE_X402_KEY` to the private key (`0x` and 64 hex digits) of a Base wallet holding a little USDC (real money). The key stays in this process.

## python-x402.py

```python
# One free trial call, then one paid AttestPage call with httpx and x402, step by step.
import os
import sys

import httpx
from eth_account import Account
from x402 import x402ClientSync
from x402.http import x402HTTPClientSync
from x402.mechanisms.evm.exact import register_exact_evm_client

BASE = os.environ.get("ATTESTPAGE_URL", "https://vehcdj664efetfrsolne5umanq.srv.us").rstrip("/")
NETWORK = "eip155:8453"  # Base
MAX_USD = 0.01  # the most this script will pay; verify/quote is listed at US$0.01

url = f"{BASE}/v1/verify/quote"
body = {
    "url": sys.argv[1] if len(sys.argv) > 1 else "https://www.iana.org/help/example-domains",
    "quote": sys.argv[2] if len(sys.argv) > 2 else "These domains may be used as illustrative examples in documents",
}
http = httpx.Client(timeout=60)


def receipt_valid(receipt):
    # Checking a receipt's signature is free.
    return http.post(f"{BASE}/v1/receipt/verify", json={"receipt": receipt}).json()["valid"]


def fail(res):
    # An error reply that is not JSON (an HTML 502, say) has no error object: show the status and the start of the body.
    try:
        body = res.json()
    except ValueError:
        body = None
    error = body.get("error") if isinstance(body, dict) else None
    if not isinstance(error, dict):
        raise SystemExit(f"{res.status_code}: {res.text[:200]}")
    raise SystemExit(f"{res.status_code} {error.get('code')}: {error.get('message')}")


# 1. Free trial: the header Quoteproof-Trial: 1 runs a paid route without payment, a few calls a day.
res = http.post(url, json=body, headers={"Quoteproof-Trial": "1"})
if res.status_code == 429:
    print("trial: used up for today, retry after", res.headers.get("retry-after"), "seconds")
else:
    if res.status_code >= 400:
        fail(res)
    answer = res.json()
    print("trial match:", answer["result"]["match"], "tier:", answer["tier"], "receipt valid:", receipt_valid(answer["receipt"]))

key = os.environ.get("ATTESTPAGE_X402_KEY")
if not key:
    print("set ATTESTPAGE_X402_KEY to pay for the next call")
    sys.exit(0)

client = x402ClientSync()
register_exact_evm_client(client, Account.from_key(key))
x402 = x402HTTPClientSync(client)

# 2. Send the request with no payment. A valid request gets 402, with the offer in PAYMENT-REQUIRED.
res = http.post(url, json=body)
if res.status_code != 402:
    raise SystemExit(f"expected 402, got {res.status_code}: {res.text[:200]}")
offer = x402.get_payment_required_response(res.headers.get, res.content)

# 3. Check the offer before signing: right network, price within MAX_USD (USDC has 6 decimals).
accepts = [a for a in offer.accepts if a.network == NETWORK and int(a.amount) <= MAX_USD * 1e6]
if not accepts:
    raise SystemExit(f"no acceptable offer: {offer.accepts}")
print(f"price: US${int(accepts[0].amount) / 1e6} on {accepts[0].network}, pay to {accepts[0].pay_to}")

# 4. Sign the offer and send the same request again with PAYMENT-SIGNATURE.
payment = x402.create_payment_payload(offer.model_copy(update={"accepts": accepts}))
res = http.post(url, json=body, headers=x402.encode_payment_signature_header(payment))
if res.status_code >= 400:
    fail(res)
answer = res.json()

# 5. A settled call carries PAYMENT-RESPONSE: the transaction, payer and network.
settlement = x402.get_payment_settle_response(res.headers.get)
print("settled:", settlement.transaction, "from", settlement.payer, "on", settlement.network)
print("match:", answer["result"]["match"], "score:", answer["result"]["score"], "page:", answer["page"]["content_kind"])
print("receipt valid:", receipt_valid(answer["receipt"]))
```

## Run

```sh
python python-x402.py
ATTESTPAGE_X402_KEY=0x... python python-x402.py
ATTESTPAGE_X402_KEY=0x... python python-x402.py https://example.com "Example Domain"
```

With no key it makes the trial call and stops before paying.

## Notes

- **Free trial.** The header `Quoteproof-Trial: 1` runs a paid route without payment, a few calls per caller per UTC day, with at most 3 items in a batch. Over the day's limit it answers `429 trial_exhausted` with `Retry-After` and is not charged. A larger trial batch gets the normal 402 offer with `details.reason` `trial_too_large`. See [errors](https://vehcdj664efetfrsolne5umanq.srv.us/docs/errors).
- **Only the price.** An unpaid request with an empty body (`{}`) also gets the 402 offer, so a client can read a route's price before it has a real input.
- **Bad input is free.** A request that fails validation gets `400` before any offer and is never charged.
- **Settled only on success.** The payment settles only when the route answers with a status below 400.
- **Spent signatures.** A payment signature pays for one call. Sending it again gets `409 payment_already_used`; sign a new offer instead.
- **Shorter.** `x402HttpxClient` and `wrapHttpxWithPayment` from `x402.http.clients` do steps 2 to 4 inside `httpx`; check the price yourself, as in step 3, with a payment policy.
- **Other routes.** The same steps work for every paid route; only the body and the price change. See [README.md](/docs/examples#routes-and-prices).
