<!-- Generated raw Markdown for /reference/python; product pin 638fa1e9ca7face19a442cef43754f0d627c7705. -->

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](/reference/protocol/) alone. Apache-2.0; Python >= 3.11.

<VersionBadge name="subetha" version="0.2.0" registry="PyPI" verified="2026-07-27" />

```bash
pip install subetha
```

**Status: 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`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/python/README.md)).

## Exported names

`from subetha import …` exposes exactly the following
([`python/src/subetha/__init__.py`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/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`

([`python/src/subetha/client.py`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/python/src/subetha/client.py))

```python
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](/reference/configuration/)) |
| `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`

([`python/src/subetha/policy.py`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/python/src/subetha/policy.py))

```python
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)

From the package README at the pinned commit — a **local, non-production**
setup (loopback URLs, dev placeholder key):

```python
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

- [Configuration reference](/reference/configuration/) — the `SUBETHA_*`
  environment `from_env()` reads.
- [Wire protocol reference](/reference/protocol/) — the formats this client
  implements.
- [TypeScript API](/reference/typescript/) — the npm counterpart packages.
