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.
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
# 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
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: 1runs 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 answers429 trial_exhaustedwithRetry-Afterand is not charged. A larger trial batch gets the normal 402 offer withdetails.reasontrial_too_large. See 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
400before 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.
x402HttpxClientandwrapHttpxWithPaymentfromx402.http.clientsdo steps 2 to 4 insidehttpx; 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.