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-08-07)
pip install subetha==0.2.0Status: 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-08-07). Until that is reconciled upstream, treat 0.2.0 as the installable version and the README’s pin line as stale (python/README.md).
Published subetha==0.2.0 exports
Section titled “Published subetha==0.2.0 exports”The published PyPI artifact exposes exactly the following, based on the
registry-verified subetha==0.2.0 surface (verified 2026-08-07):
| 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 |
Current source addition (not asserted as published PyPI surface)
Section titled “Current source addition (not asserted as published PyPI surface)”The pinned source also re-exports the following from subetha:
| source-level import | implementation module | kind |
|---|---|---|
from subetha import HistoryBinding |
history_client.py |
dataclass |
from subetha import create_history_binding |
history_client.py |
function |
from subetha import verify_history_binding |
history_client.py |
function |
These helpers are source-only and must not be imported from the published
subetha==0.2.0 artifact until a release containing them is published and
independently verified.
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, attempts (including approval events), and configured limits — 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 (only http/https URLs are accepted); 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.
approve_above and max_per_payment are integer token base units, not display
units; callers must account for the token’s decimals themselves. If a quoted
amount is above approve_above, approve_payment receives an ApprovalRequest
and must return a truthy value. The callback is user-provided (the example’s
ask_human is not supplied by this package); None, a false return, an
exception, or an approval_timeout_s timeout refuses the payment. A caller may
provide either private_key or a signer callable of (bytes) -> bytes, but
not both; payer_address is required with signer. Calling pay() without
either signing source raises SubethaError, while quote() can run without
one.
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 loopback setup with a 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)Quote.offer and PayResult.payment are optional. Check the result status and
that the field is not None before dereferencing them; the example assumes a
successful local payment flow.
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.