Metadata-Version: 2.4
Name: aether-chain-client
Version: 0.2.9
Summary: Python client for the Aether chain: ML-DSA-44 keys, payments, buying and selling paid APIs (x402 aether-memo / aether-prepaid) and the service directory.
License: MIT
Project-URL: Homepage, https://github.com/whoyoujoshin/aether/tree/main/clients/python#readme
Project-URL: Repository, https://github.com/whoyoujoshin/aether
Project-URL: Issues, https://github.com/whoyoujoshin/aether/issues
Keywords: aether,ai-agents,agent-payments,x402,mcp,blockchain,cosmos,post-quantum,ml-dsa,usdc
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: dilithium-py<2,>=1.4
Requires-Dist: mnemonic>=0.21

# aether-chain-client

Python client for the [Aether](https://github.com/whoyoujoshin/aether) chain: ML-DSA-44 keys, sending and receiving AETH, buying from and selling paid APIs (x402 `aether-memo`, `aether-prepaid` and `aether-pull`) and the on-chain service directory. It talks to a node's CometBFT RPC (port 26657) only.

```python
import os
from aether_client import AetherClient, Key, fetch_paid, find_services, rate_service, withdraw_prepaid

client = AetherClient("http://localhost:26657", "aether-testnet-1")
key = Key.from_mnemonic(os.environ["AETHER_MNEMONIC"])  # same address as `aetherd keys add` / `agentmcp`

sent = client.send(key, "aether1...", "0.5 AETH", memo="invoice-42")
done = client.wait_for_transaction(sent.hash)            # confirmed | failed | pending (timeout)
# Retrying after an error: client.rebroadcast(sent.signed) -- never a second send().

res = fetch_paid(client, key, "https://api.example.com/forecast", max_amount="0.05 AETH", prepay="1 AETH")
# Or, where offered, pay from your own account under a capped, 7-day allowance the seller collects from later:
res2 = fetch_paid(client, key, "https://api.example.com/forecast", max_amount="0.05 AETH", pull_allowance="1 AETH")
services = find_services(client, query="weather", max_price="0.1 AETH", trusted=[owner_address])
# services[i].reputation: payments, payers, ratings (can be faked), trusted_ratings (can't)
# res.receipt: the seller's signed receipt and whether it matches exactly what was sent and received
rate_service(client, key, "https://api.example.com", 5)  # only raters who paid a service count
back = withdraw_prepaid(client, key, "https://api.example.com", withdrawal_id="w1")  # unspent prepaid balance
```

## Selling

Charge per request from a Python service -- all three payment schemes, the `/.well-known/x402` manifest for the service directory, and withdrawals of unspent prepaid balances. Compatible with the Go paywall and every Aether buyer.

```python
import os
from fastapi import FastAPI, Request
from aether_client import AetherClient, Key, Paywall

client = AetherClient("http://localhost:26657", "aether-testnet-1")
pw = Paywall(client, "aether1...", "0.01 AETH", name="Weather", description="Forecasts by city",
             prepaid_ledger="ledger.json",           # customers' balances: back it up
             min_deposit="0.1 AETH",
             payout_key=Key.from_mnemonic(os.environ["PAYOUT_MNEMONIC"]),  # pays withdrawals; keep a small float in it
             receipt_key=payee_key,  # sign a receipt for every paid response (or a delegated key + receipt_delegation)
             pull_collector_key=Key.from_mnemonic(os.environ["COLLECTOR_MNEMONIC"]), pull_credit="1 AETH")  # aether-pull: needs no funds
pw.start_collecting()  # collects what aether-pull buyers owe, in batches
api = FastAPI()

@api.get("/forecast")
async def forecast(request: Request):
    return {"paid_by": request.scope["aether"].payer}

app = pw.asgi(api, free=["/health"])   # Flask/Django: app.wsgi_app = pw.wsgi(app.wsgi_app); who paid: environ["aether.payment"]
```

The ledger is one JSON file (the Go paywall's format) for one process. Chain lookups block, so the ASGI adapter runs them in a thread.

Amounts always carry a unit (`"1.5 AETH"`, `"1500000uaeth"`).

**USDC.** `AetherClient(rpc, chain_id, usdc_channel="channel-N")` (Aether's end of its channel to Noble) also knows USDC: Noble's `uusdc` over exactly that channel, never a lookalike that arrived another way. For USDC that arrives another route, give its denom trace and its denom on the issuing chain instead: `usdc_path="transfer/channel-2", usdc_base_denom="erc20:0x0C382e685bbeeFE5d3d9C29e29E341fEE8E84C5d", usdc_issuer="Injective"` (`usdc_base_denom` defaults to `uusdc`). Then `send(key, to, "5 USDC")` sends USDC, `balances(address)` lists each asset, `wait_for_payment` matches in the asset of `min_amount`, and `fetch_paid` pays services priced in USDC with `max_amount` (and `prepay` / `pull_allowance`) in USDC; an amount in another asset than the service charges is refused with `ASSET_MISMATCH` before anything is paid. Results carry `asset`, `amount`, `balance`, `owed` and `allowance` in that asset's base units; the `*_uaeth` fields are set only for AETH. Without the channel, `"5 USDC"` is an unknown unit. The seller kit charges USDC too: `Paywall(client, pay_to, "0.05 USDC", usdc_channel="channel-N")` charges Noble USDC for everything (payments, deposits and balances, pull allowances and collections, withdrawals), `min_deposit` and `pull_credit` must then be in USDC, and 402s, the manifest and receipts carry the asset exactly as `cmd/paywall`'s do.

Tests: `python -m unittest discover -s tests` (checked against `../testdata/vectors.json`, generated by the Go code).
