Payer quickstart — Python
The subetha package on PyPI is a payer-only reference client: it decodes
a 402 offer, decides under an operator-configured spending policy, signs,
pays, and keeps an audit trail. It touches no zk code and no BUSL code — its
only dependencies are httpx, eth-account, and eth-utils. The example below uses loopback URLs, chain 31337, and placeholder keys. The
code mirrors the pinned package README; the flow
expectations describe what the pinned sources define — the
local end-to-end tutorial is where you actually run
them.
Prerequisites
Section titled “Prerequisites”- Python >= 3.11 — the version floor declared by the pinned
pyproject.toml. - A running local stack for the reproducible path this page follows:
- anvil on chain 31337 at
http://127.0.0.1:8545, - a provider answering 402s (the in-repo demo
provider listens on
:4031), - a facilitator settling for that provider
(loopback
:4032).
- anvil on chain 31337 at
Bringing that stack up — and funding the payer account with test tokens — is the local end-to-end tutorial’s job. Without the provider and facilitator running, the code below constructs fine but has no 402-gated resource to pay for.
1. Install
Section titled “1. Install”pip install subetha==0.2.0Requires Python >= 3.11; Apache-2.0. The installable version at ship time is
0.2.0 (registry-verified 2026-08-07), and the package is experimental
(0.x) — the API may change in breaking ways between 0.x minor releases.
Note one documented source divergence: the package README at the pinned
commit still recommends the pin subetha>=0.1,<0.2, which excludes 0.2.0 —
treat 0.2.0 as current and that pin line as stale (details in the
Python API reference).
2. Configure the client with a spending policy
Section titled “2. Configure the client with a spending policy”The SpendingPolicy is the safety layer: it is operator-configured, never
agent-configured — hosts, network, token, and budget caps come from you (or
the SUBETHA_* environment), not from anything an agent says at runtime.
The optional approve_payment callback is any callable that takes an
ApprovalRequest and returns True to approve. Define one before
constructing the client — the simplest runnable version asks on stdin:
def ask_human(info) -> bool: # info is an ApprovalRequest: url, quoted, token, network, pay_to, … answer = input(f"Pay {info.quoted} base units to {info.url}? [y/N] ") return answer.strip().lower() == "y"from subetha import SubethaClient, SpendingPolicy
client = SubethaClient( private_key="0x…", # placeholder — local anvil dev account only rpc_url="http://127.0.0.1:8545", # local anvil (chain 31337) mode="permit", # gasless default; or "self-transfer" policy=SpendingPolicy( allowed_hosts=["127.0.0.1"], # first line of defense — keep it tight allowed_network="eip155:31337", max_per_payment=100_000, # token base units max_total=5_000_000, # per network|token pair ), approve_above=10_000, # optional human-in-the-loop threshold approve_payment=ask_human, # True = approve)Alternatively, SubethaClient.from_env() builds the client from the same
SUBETHA_* variables the TypeScript tools read:
# Placeholders — local dev values only; never commit a real key.export SUBETHA_PAYER_PK=0x…export SUBETHA_RPC_URL=http://127.0.0.1:8545export SUBETHA_ALLOWED_HOSTS=127.0.0.1export SUBETHA_ALLOWED_NETWORK=eip155:31337export SUBETHA_MAX_PER_PAYMENT=100000export SUBETHA_MAX_TOTAL=5000000The full variable table (defaults included) is in the configuration reference.
3. Quote, pay, report
Section titled “3. Quote, pay, report”url = "http://127.0.0.1:4031/api/complete" # a locally running provider
# Quote: fetch the offer without paying — never needs the private key.q = client.quote(url, method="POST", body='{"prompt":"hi"}')print(q.offer.quoted, q.offer.fee, q.offer.approval_required)
# Pay: policy gate → 402 → offer selection → optional approval → sign → paid request.r = client.pay(url, method="POST", body='{"prompt":"hi"}')print(r.status, r.payment.amount, r.resource)
# Report: totals, payments, and attempts (including approval events).print(client.report())On success, the output is schematically similar to:
<quoted amount> <fee or None> <approval flag>200 <amount> <resource body>{"payments": [{"network": "eip155:31337", "status": "accepted", …}]}The exact resource body and report fields depend on the provider and client
state. The payment is accepted when the paid request returns; finalization
(the proof-gated mint) happens later on the facilitator. In flow terms, the
first request drew a 402 offer, the client paid it, and the re-sent request
came back accepted — resource served, settlement acknowledged on the
request path. Finalization is asynchronous, and no client response ever
reports finalized
(accepted vs finalized).
Safety behavior you get for free
Section titled “Safety behavior you get for free”Guarantees stated by the pinned client module and README:
- Host allowlist before any request leaves — and redirects are refused, so a payment cannot be steered onto a host you did not allowlist.
- Budget accounting at the irreversibility point. A definitive settle rejection on the permit path releases the reservation (no funds moved); a paid request that then fails with an HTTP error keeps the spend counted, on the safe side.
- One payment at a time (internal lock); reserved
PAYMENT-*/X-PAYMENTheaders in your own header dict are rejected. - Redacted errors — every failure normalizes to
SubethaErrorwith secrets scrubbed from the message.
Next steps
Section titled “Next steps”- Local end-to-end tutorial — run the full 402 → payment → accepted → finalized flow against the local stack.
- Python API reference — every exported name, dataclass fields, and failure semantics.
- Provider quickstart — the service on the other side of the 402.