AttestPage · Docs · Pricing · OpenAPI · llms.txt

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