Python API reference
The subetha package on PyPI is the payer side only: decode a 402 offer,
decide under an operator-configured spending policy, sign, pay, report. The
provider, facilitator, and settlement layers are TypeScript and live in the
product repository; paying touches no zk code and no BUSL code — the package
depends only on httpx, eth-account, and eth-utils. It is a reference
implementation written against the
wire protocol alone. Apache-2.0; Python >= 3.11.
subetha 0.2.0 (PyPI, verified 2026-07-27)
pip install subethaStatus: experimental (0.x). The package’s own README states the API may change in breaking ways between 0.x minor releases, and stability will be declared at 1.0.
Known source divergence (documented, not resolved here). The package README at the pinned commit still recommends the pin
subetha>=0.1,<0.2, which excludes the 0.2.0 release that is current on PyPI (registry-verified 2026-07-27). Until that is reconciled upstream, treat 0.2.0 as the installable version and the README’s pin line as stale (python/README.md).
Exported names
Section titled “Exported names”from subetha import … exposes exactly the following
(python/src/subetha/__init__.py):
| export | kind | description |
|---|---|---|
SubethaClient |
class | the payer client (below) |
SpendingPolicy |
class | operator-configured spending policy (below) |
SCHEME |
constant | "subetha-zerc20" |
SubethaError |
exception | all client failures normalize to this; messages are redacted before they reach the caller |
Offer |
dataclass | quoted, token, network, gasless, list_price?, fee?, bps?, expires_at?, approval_required? |
Quote |
dataclass | status, offer?, error? |
Payment |
dataclass | amount, token, network, fee?, burn_tx_hash?, settle? |
PayResult |
dataclass | status, resource, truncated, payment? |
ApprovalRequest |
dataclass | what the human is asked to approve (and the identity the approval is bound to): url, quoted, token, network, pay_to, challenge_nonce, fee? |
Requirements |
dataclass | validated view of a subetha-zerc20 offer; raw keeps the exact dict received in the 402 for the verbatim echo |
SubethaClient
Section titled “SubethaClient”(python/src/subetha/client.py)
SubethaClient( *, private_key=None, # the paying account (permit mode: only signs, no gas) rpc_url="http://127.0.0.1:8545", mode="permit", # "permit" (gasless, default) or "self-transfer" policy=None, # SpendingPolicy; a default local-only policy when unset approve_above=None, # base units; quoted amounts above this need a human yes approve_payment=None, # callable(ApprovalRequest) -> bool approval_timeout_s=120.0, timeout_s=30.0, on_pre_submit=None, # callable(Requirements) hook before the paying request signer=None, # external signing callable (mutually exclusive with private_key) payer_address=None, # required with signer; invalid without it)Public methods:
| method | description |
|---|---|
SubethaClient.from_env(env=None) |
classmethod: build from the SUBETHA_* environment (same variable names as the TypeScript tools — see the configuration reference) |
quote(url, method="GET", body=None) -> Quote |
fetch the offer without paying; never needs the private key. Sets Offer.approval_required when the quoted amount is above approve_above |
pay(url, method="GET", body=None, headers=None) -> PayResult |
full payment flow: policy gate → 402 → offer selection → optional approval → sign/broadcast → paid request. One payment at a time (internal lock); reserved PAYMENT-*/X-PAYMENT headers are rejected |
report() -> dict |
totals, payments, and attempts (including approval events) — the policy’s audit trail |
Failure semantics the client guarantees (from the module and README at the pin): redirects are refused so a payment cannot be steered off the host allowlist; a definitive settle rejection on the permit path releases the budget reservation (no funds moved); a paid request that then fails with an HTTP error keeps the spend in the budget on the safe side; every error message is redacted before it reaches the caller.
SpendingPolicy
Section titled “SpendingPolicy”(python/src/subetha/policy.py)
SpendingPolicy( *, allowed_hosts=("localhost", "127.0.0.1"), # or "*" to allow all allowed_network="eip155:31337", allowed_token=None, max_per_payment=None, # base units max_total=None, # base units, per network|token pair)The policy enforces at two points: check_url(raw_url) before any request
leaves (hostname allowlist), and filter_offers(offers) at payment selection
(network/token pins, per-payment and total caps). Spend accounting happens at
the irreversibility point, and report() exposes the audit trail. The
remaining methods (record_spend, release, record_payment,
record_attempt) are the accounting hooks the client drives.
Example (local stack)
Section titled “Example (local stack)”From the package README at the pinned commit — a local, non-production setup (loopback URLs, dev placeholder key):
from subetha import SubethaClient, SpendingPolicy
client = SubethaClient( private_key="0x…", # the paying account (permit: only signs, no gas) rpc_url="http://127.0.0.1:8545", mode="permit", # gasless (default); or "self-transfer" policy=SpendingPolicy( allowed_hosts=["127.0.0.1"], # first line of defense — keep it tight max_per_payment=100_000, # token base units max_total=5_000_000, ), approve_above=10_000, # optional human-in-the-loop approve_payment=lambda info: ask_human(info), # True = approve)
q = client.quote("http://127.0.0.1:4031/api/complete", method="POST", body='{"prompt":"hi"}')print(q.offer.quoted, q.offer.fee, q.offer.approval_required)
r = client.pay("http://127.0.0.1:4031/api/complete", method="POST", body='{"prompt":"hi"}')print(r.status, r.payment.amount, r.resource)
print(client.report()) # totals / payments / attempts (incl. approvals)Related pages
Section titled “Related pages”- Configuration reference — the
SUBETHA_*environmentfrom_env()reads. - Wire protocol reference — the formats this client implements.
- TypeScript API — the npm counterpart packages.