Skip to content

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.

  • 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).

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.

Terminal window
pip install subetha==0.2.0

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

Terminal window
# Placeholders — local dev values only; never commit a real key.
export SUBETHA_PAYER_PK=0x
export SUBETHA_RPC_URL=http://127.0.0.1:8545
export SUBETHA_ALLOWED_HOSTS=127.0.0.1
export SUBETHA_ALLOWED_NETWORK=eip155:31337
export SUBETHA_MAX_PER_PAYMENT=100000
export SUBETHA_MAX_TOTAL=5000000

The full variable table (defaults included) is in the configuration reference.

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).

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-PAYMENT headers in your own header dict are rejected.
  • Redacted errors — every failure normalizes to SubethaError with secrets scrubbed from the message.