usdf 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- usdf-0.1.2/.gitignore +6 -0
- usdf-0.1.2/LICENSE +21 -0
- usdf-0.1.2/PKG-INFO +124 -0
- usdf-0.1.2/README.md +105 -0
- usdf-0.1.2/pyproject.toml +36 -0
- usdf-0.1.2/src/usdf/__init__.py +102 -0
- usdf-0.1.2/src/usdf/_http.py +91 -0
- usdf-0.1.2/src/usdf/accept.py +187 -0
- usdf-0.1.2/src/usdf/client.py +842 -0
- usdf-0.1.2/src/usdf/errors.py +56 -0
- usdf-0.1.2/src/usdf/estimate.py +123 -0
- usdf-0.1.2/src/usdf/langchain.py +142 -0
- usdf-0.1.2/src/usdf/sse.py +49 -0
- usdf-0.1.2/src/usdf/types.py +407 -0
- usdf-0.1.2/src/usdf/x402.py +555 -0
usdf-0.1.2/.gitignore
ADDED
usdf-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 USDF contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
usdf-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: usdf
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Client for the USDF metered inference gateway: run a prompt paid from a prepaid key or per request with x402 in USDF or USDG, OpenAI-compatible chat completions with receipts, batch jobs, quotes, balances, keys, links, agents, and a LangChain chat model.
|
|
5
|
+
Project-URL: Homepage, https://usdf.fi
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: inference,langchain,openai-compatible,receipts,robinhood-chain,usdf,x402
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Provides-Extra: httpx
|
|
13
|
+
Requires-Dist: httpx>=0.27; extra == 'httpx'
|
|
14
|
+
Provides-Extra: langchain
|
|
15
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
16
|
+
Provides-Extra: wallet
|
|
17
|
+
Requires-Dist: eth-account>=0.13; extra == 'wallet'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# usdf
|
|
21
|
+
|
|
22
|
+
Client for the USDF metered inference gateway, and a LangChain chat model. No required dependencies: the standard library's `urllib`, or `httpx` when it is installed. Python 3.9+. Paying from a wallet uses `eth-account`; the LangChain model uses `langchain-core`.
|
|
23
|
+
|
|
24
|
+
1 USDF = $1 of compute, redeemable 1:1 for USDG at any time. Every request is held for its worst case before it runs, billed from the provider's reported usage at the published price sheet, and answered with a receipt.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install usdf
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Run a prompt
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from usdf import USDF
|
|
34
|
+
|
|
35
|
+
# Pay per request from a wallet, no account: an eth-account account (or a private key).
|
|
36
|
+
client = USDF(wallet=wallet)
|
|
37
|
+
result = client.run("deepseek-v3.2", "Hello")
|
|
38
|
+
print(result.text, result.receipt.paid.usdf, result.receipt.asset)
|
|
39
|
+
|
|
40
|
+
# Or charge a prepaid balance with an API key.
|
|
41
|
+
prepaid = USDF(api_key="sk_...")
|
|
42
|
+
r = prepaid.run("deepseek-v3.2", "Hello", max_tokens=256)
|
|
43
|
+
print(r.text, r.receipt.cost.usdf, "USDF", r.receipt.request_id)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`run(model, prompt_or_messages, max_tokens=, system=, temperature=, provider=, on_text=)` returns `.text`, `.receipt` (the gateway's receipt; the full body is `receipt.raw`), `.id`, `.completion` and, over x402, `.payment` (the decoded `PAYMENT-RESPONSE`). `on_text` streams the answer piece by piece.
|
|
47
|
+
|
|
48
|
+
Unset, `api_key`, `base_url` (the gateway origin, default `https://api.usdf.fi`), `session_token`, `rpc_url`, `max_amount` and `pay_to` come from `USDF_API_KEY`, `USDF_BASE_URL`, `USDF_SESSION_TOKEN`, `USDF_RPC_URL`, `USDF_MAX_AMOUNT` and `USDF_PAY_TO`. A wallet given explicitly is the payer; the environment's key is not read then.
|
|
49
|
+
|
|
50
|
+
### Paying from a wallet (x402)
|
|
51
|
+
|
|
52
|
+
The gateway answers an unpaid request with 402 and two accepted payments for the same amount: USDF through Permit2 and USDG through an EIP-3009 authorization. Neither costs the payer gas. The client reads the wallet's USDF allowance to Permit2 first, through `rpc_url` (or `USDF_RPC_URL`; unset, the chain's public RPC `https://rpc.mainnet.chain.robinhood.com`), which must answer for chain 4663: when it covers the amount it pays in USDF, otherwise in USDG, which needs no approval. A first-time payer whose USDF is not approved therefore pays USDG rather than a USDF payment the gateway would refuse. `asset="USDF"` or `"USDG"` fixes the choice; `approve=True` signs and sends the one approval a USDF payer ever needs, `approve(Permit2, max)` on USDF, through the same RPC. The receipt carries `paid` (what was charged, the quote), `usage_cost`, and the payment's `asset`, `method`, `payer` and `payment_tx`.
|
|
53
|
+
|
|
54
|
+
The 402 comes from whatever server answers at the base URL, so the client signs nothing on its word alone. It pays only: on Robinhood Chain (`eip155:4663`); USDF (`0x4A35a5eC2A50A4d2957B2F2366E017f526cAB318`) through Permit2 or USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`) through EIP-3009 under USDG's own domain (`Global Dollar`, version `1`); to the payee the gateway names: its own 402 when the base URL is the gateway (`https://api.usdf.fi`), else the address the gateway's `GET https://api.usdf.fi/v1/config` publishes (`x402.pay_to`), else the gateway's hot wallet (`0x13009Bd7116f8C955f92409467743ee67AB1990b`); `pay_to` (or `USDF_PAY_TO`, comma-separated) pins another payee, and then only that one; with a validity window of at most an hour; and at most `max_amount` base units per payment (`USDF_MAX_AMOUNT`; default 1000000, 1 USDF). Anything else raises `USDFError` with type `payment_refused` and a code (`untrusted_network`, `untrusted_asset`, `untrusted_domain`, `untrusted_payee`, `amount_over_limit`, `untrusted_amount`, `untrusted_timeout`, `wrong_chain`) before any approval, read or signature. The exact scheme charges the quote, the request's worst case at its `max_tokens`, so a lower `max_tokens` is the way to a smaller payment.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from eth_account import Account
|
|
58
|
+
from usdf import USDF
|
|
59
|
+
|
|
60
|
+
client = USDF(wallet=Account.from_key(key), rpc_url="https://rpc.mainnet.chain.robinhood.com", approve=True)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`sign_exact_payment` (which applies the same checks), `check_accept`, `payment_limits`, `choose_accept`, `wallet_signer`, `read_permit2_allowance`, `approve_permit2`, the header codecs and the constants (`CHAIN_ID`, `USDF_ADDRESS`, `USDG_ADDRESS`, `GATEWAY_PAYEE`, `PUBLIC_RPC_URL`, `DEFAULT_MAX_AMOUNT`) are exported for other signers.
|
|
64
|
+
|
|
65
|
+
## LangChain
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from usdf.langchain import USDFChat
|
|
69
|
+
|
|
70
|
+
llm = USDFChat(model="deepseek-v3.2", wallet=wallet) # or api_key="sk_..."
|
|
71
|
+
reply = llm.invoke("Hello")
|
|
72
|
+
print(reply.content, reply.response_metadata["receipt"]["request_id"], reply.usage_metadata)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`pip install usdf langchain` (or `usdf[langchain]` for `langchain-core` alone). `stream()` yields the answer as it arrives and ends with a chunk carrying the receipt. The settings are those of `USDF`: `api_key`, `wallet`, `base_url`, `asset`, `approve`, `rpc_url`, `max_amount`, `pay_to`, plus `max_tokens`, `temperature` and `provider` (privacy and routing asks). `api_key` is a secret (masked, left out of `model_dump`, named in `lc_secrets`) and `wallet` is left out of the model's repr and of the serialized form LangChain hands to callbacks and tracers.
|
|
76
|
+
|
|
77
|
+
## Chat completions
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
client = USDF(api_key="sk_...")
|
|
81
|
+
c = client.chat.completions.create("deepseek-v3.2", [{"role": "user", "content": "Hello"}], max_tokens=256)
|
|
82
|
+
print(c.content, c.receipt.cost.usdf)
|
|
83
|
+
|
|
84
|
+
s = client.chat.completions.create("deepseek-v3.2", messages, max_tokens=256, stream=True)
|
|
85
|
+
for chunk in s:
|
|
86
|
+
print((chunk["choices"][0]["delta"].get("content") if chunk["choices"] else "") or "", end="")
|
|
87
|
+
print(s.receipt.cost.usdf if s.receipt else "stopped early")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Keyword arguments are sent as given (`max_tokens`, `temperature`, `tools`, `provider`, ...). A stream exposes `request_id` from the first byte and `receipt` once read to the end; `interrupted` is set when the provider cut the answer (billed, receipt `partial`); a stream that produced nothing raises `USDFError` `upstream_unavailable` and nothing is charged. A request served on attested hardware carries `receipt.attestation`; `client.receipts.attestation(id)` returns the provider's signed receipt.
|
|
91
|
+
|
|
92
|
+
## Batch jobs
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
job = client.batches.create(batch_model, messages, max_tokens=256) # a model whose price carries batch_input
|
|
96
|
+
done = client.batches.wait(job["id"]) # the completion; done.receipt.batch is True
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Before and after a call
|
|
100
|
+
|
|
101
|
+
`quote(model, prompt=, max_tokens=)` (no key), `balance()`, `receipts.get(id)` (with `log`, the request's leaf in the usage log), `receipts.list(limit=, model=, status=)` (a key's own, or with only a session the account's), `models.list()`, `pricing()`, `embeddings.create(model, input)`. `usdf.estimate` reproduces the gateway's hold arithmetic.
|
|
102
|
+
|
|
103
|
+
## The account (session)
|
|
104
|
+
|
|
105
|
+
Everything a key can do (requests, quotes, the balance, receipts, tools) needs only the key: create one on the dashboard at usdf.fi. The account's own calls take a session: sign in at usdf.fi and pass its session token, `USDF(session_token=...)` or `USDF_SESSION_TOKEN` (it lasts about an hour). There is no email and password sign-in. `client.auth.info()` reads `GET /v1/auth` (the sign-in in force and every account path); `client.auth.sign_out()` ends the session at the gateway.
|
|
106
|
+
|
|
107
|
+
- `keys.create(label=, budget_units=, rate_limit_rpm=)`, `keys.list(revoked=)`, `keys.revoke(id)`, `keys.rotate(id)` (a new key with the old one's controls; the old one is revoked), `keys.limits(id)`, `keys.update(id, spend_limit_hour_units=, spend_limit_day_units=, spend_limit_month_units=, allowed_models=, paused=, no_logs=, private_tier=, webhook_url=)`, `keys.pause(id)`, `keys.resume(id)`.
|
|
108
|
+
- `account.get()`, `account.overview()`, `account.activity(model=, status=, from_=, to=, limit=)`, `account.activity_csv(...)`, `account.settings()`, `account.update_settings(...)`, `account.analytics(days=)`, `account.withdraw(units, chain_id=)` (to the linked wallet).
|
|
109
|
+
- `links.create(units, memo=, expires_in_days=)` (the token is returned once), `links.list()`, `links.get(token)`, `links.claim(token)`, `links.cancel(link_id)`.
|
|
110
|
+
- `agents.create(slug, name, **policy)`, `agents.list()`, `agents.get(slug)`, `agents.update(slug, **policy)`, `agents.fund(slug, units, direction=, idempotency_key=)`, `agents.pay(slug, to, units, memo=, idempotency_key=)`, `agents.payments(slug)`, `agents.rotate_key(slug)`.
|
|
111
|
+
- `zap.quote(token, amount, recipient=, slippage_bps=)`: ETH or a token with a liquidity pool against USDG, into USDF, with the transactions to send. No key.
|
|
112
|
+
- `pool.read()`: the USDF/USDG pool's price, tick, whether it sits in the seeded band, its liquidity and reserves. No key.
|
|
113
|
+
|
|
114
|
+
Money: base units (1 unit = $0.000001 = 0.000001 USDF); amounts are `Amount(units, usd, usdf)` or `{units, usd, usdf}`.
|
|
115
|
+
|
|
116
|
+
## Errors
|
|
117
|
+
|
|
118
|
+
Every refusal raises `USDFError` with the gateway's `status`, `type`, `code`, `message`, `request_id` and `retry_after`. Nothing is charged for a refusal.
|
|
119
|
+
|
|
120
|
+
## Changes
|
|
121
|
+
|
|
122
|
+
**0.1.2.** A wallet payment goes to the payee the gateway names, not to a hot wallet fixed in the package: the gateway rotated its custody hot wallet on 2026-10-26, and 0.1.1, which pays only the retired one (`0x52Fe...9F05`), refuses the new one's 402 with `untrusted_payee` before signing anything. The 402's payee is taken as it stands from `https://api.usdf.fi`; through any other base URL it must be the one `https://api.usdf.fi/v1/config` publishes, or the new hot wallet (`GATEWAY_PAYEE`). `pay_to` and `USDF_PAY_TO` still pin a payee.
|
|
123
|
+
|
|
124
|
+
**0.1.1.** The default base URL is `https://api.usdf.fi`, the gateway's own domain; 0.1.0 used the hosting provider's hostname for the same gateway, which still answers. `base_url` or `USDF_BASE_URL` still points the client elsewhere. No other change.
|
usdf-0.1.2/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# usdf
|
|
2
|
+
|
|
3
|
+
Client for the USDF metered inference gateway, and a LangChain chat model. No required dependencies: the standard library's `urllib`, or `httpx` when it is installed. Python 3.9+. Paying from a wallet uses `eth-account`; the LangChain model uses `langchain-core`.
|
|
4
|
+
|
|
5
|
+
1 USDF = $1 of compute, redeemable 1:1 for USDG at any time. Every request is held for its worst case before it runs, billed from the provider's reported usage at the published price sheet, and answered with a receipt.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install usdf
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Run a prompt
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from usdf import USDF
|
|
15
|
+
|
|
16
|
+
# Pay per request from a wallet, no account: an eth-account account (or a private key).
|
|
17
|
+
client = USDF(wallet=wallet)
|
|
18
|
+
result = client.run("deepseek-v3.2", "Hello")
|
|
19
|
+
print(result.text, result.receipt.paid.usdf, result.receipt.asset)
|
|
20
|
+
|
|
21
|
+
# Or charge a prepaid balance with an API key.
|
|
22
|
+
prepaid = USDF(api_key="sk_...")
|
|
23
|
+
r = prepaid.run("deepseek-v3.2", "Hello", max_tokens=256)
|
|
24
|
+
print(r.text, r.receipt.cost.usdf, "USDF", r.receipt.request_id)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`run(model, prompt_or_messages, max_tokens=, system=, temperature=, provider=, on_text=)` returns `.text`, `.receipt` (the gateway's receipt; the full body is `receipt.raw`), `.id`, `.completion` and, over x402, `.payment` (the decoded `PAYMENT-RESPONSE`). `on_text` streams the answer piece by piece.
|
|
28
|
+
|
|
29
|
+
Unset, `api_key`, `base_url` (the gateway origin, default `https://api.usdf.fi`), `session_token`, `rpc_url`, `max_amount` and `pay_to` come from `USDF_API_KEY`, `USDF_BASE_URL`, `USDF_SESSION_TOKEN`, `USDF_RPC_URL`, `USDF_MAX_AMOUNT` and `USDF_PAY_TO`. A wallet given explicitly is the payer; the environment's key is not read then.
|
|
30
|
+
|
|
31
|
+
### Paying from a wallet (x402)
|
|
32
|
+
|
|
33
|
+
The gateway answers an unpaid request with 402 and two accepted payments for the same amount: USDF through Permit2 and USDG through an EIP-3009 authorization. Neither costs the payer gas. The client reads the wallet's USDF allowance to Permit2 first, through `rpc_url` (or `USDF_RPC_URL`; unset, the chain's public RPC `https://rpc.mainnet.chain.robinhood.com`), which must answer for chain 4663: when it covers the amount it pays in USDF, otherwise in USDG, which needs no approval. A first-time payer whose USDF is not approved therefore pays USDG rather than a USDF payment the gateway would refuse. `asset="USDF"` or `"USDG"` fixes the choice; `approve=True` signs and sends the one approval a USDF payer ever needs, `approve(Permit2, max)` on USDF, through the same RPC. The receipt carries `paid` (what was charged, the quote), `usage_cost`, and the payment's `asset`, `method`, `payer` and `payment_tx`.
|
|
34
|
+
|
|
35
|
+
The 402 comes from whatever server answers at the base URL, so the client signs nothing on its word alone. It pays only: on Robinhood Chain (`eip155:4663`); USDF (`0x4A35a5eC2A50A4d2957B2F2366E017f526cAB318`) through Permit2 or USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`) through EIP-3009 under USDG's own domain (`Global Dollar`, version `1`); to the payee the gateway names: its own 402 when the base URL is the gateway (`https://api.usdf.fi`), else the address the gateway's `GET https://api.usdf.fi/v1/config` publishes (`x402.pay_to`), else the gateway's hot wallet (`0x13009Bd7116f8C955f92409467743ee67AB1990b`); `pay_to` (or `USDF_PAY_TO`, comma-separated) pins another payee, and then only that one; with a validity window of at most an hour; and at most `max_amount` base units per payment (`USDF_MAX_AMOUNT`; default 1000000, 1 USDF). Anything else raises `USDFError` with type `payment_refused` and a code (`untrusted_network`, `untrusted_asset`, `untrusted_domain`, `untrusted_payee`, `amount_over_limit`, `untrusted_amount`, `untrusted_timeout`, `wrong_chain`) before any approval, read or signature. The exact scheme charges the quote, the request's worst case at its `max_tokens`, so a lower `max_tokens` is the way to a smaller payment.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from eth_account import Account
|
|
39
|
+
from usdf import USDF
|
|
40
|
+
|
|
41
|
+
client = USDF(wallet=Account.from_key(key), rpc_url="https://rpc.mainnet.chain.robinhood.com", approve=True)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`sign_exact_payment` (which applies the same checks), `check_accept`, `payment_limits`, `choose_accept`, `wallet_signer`, `read_permit2_allowance`, `approve_permit2`, the header codecs and the constants (`CHAIN_ID`, `USDF_ADDRESS`, `USDG_ADDRESS`, `GATEWAY_PAYEE`, `PUBLIC_RPC_URL`, `DEFAULT_MAX_AMOUNT`) are exported for other signers.
|
|
45
|
+
|
|
46
|
+
## LangChain
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from usdf.langchain import USDFChat
|
|
50
|
+
|
|
51
|
+
llm = USDFChat(model="deepseek-v3.2", wallet=wallet) # or api_key="sk_..."
|
|
52
|
+
reply = llm.invoke("Hello")
|
|
53
|
+
print(reply.content, reply.response_metadata["receipt"]["request_id"], reply.usage_metadata)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`pip install usdf langchain` (or `usdf[langchain]` for `langchain-core` alone). `stream()` yields the answer as it arrives and ends with a chunk carrying the receipt. The settings are those of `USDF`: `api_key`, `wallet`, `base_url`, `asset`, `approve`, `rpc_url`, `max_amount`, `pay_to`, plus `max_tokens`, `temperature` and `provider` (privacy and routing asks). `api_key` is a secret (masked, left out of `model_dump`, named in `lc_secrets`) and `wallet` is left out of the model's repr and of the serialized form LangChain hands to callbacks and tracers.
|
|
57
|
+
|
|
58
|
+
## Chat completions
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
client = USDF(api_key="sk_...")
|
|
62
|
+
c = client.chat.completions.create("deepseek-v3.2", [{"role": "user", "content": "Hello"}], max_tokens=256)
|
|
63
|
+
print(c.content, c.receipt.cost.usdf)
|
|
64
|
+
|
|
65
|
+
s = client.chat.completions.create("deepseek-v3.2", messages, max_tokens=256, stream=True)
|
|
66
|
+
for chunk in s:
|
|
67
|
+
print((chunk["choices"][0]["delta"].get("content") if chunk["choices"] else "") or "", end="")
|
|
68
|
+
print(s.receipt.cost.usdf if s.receipt else "stopped early")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Keyword arguments are sent as given (`max_tokens`, `temperature`, `tools`, `provider`, ...). A stream exposes `request_id` from the first byte and `receipt` once read to the end; `interrupted` is set when the provider cut the answer (billed, receipt `partial`); a stream that produced nothing raises `USDFError` `upstream_unavailable` and nothing is charged. A request served on attested hardware carries `receipt.attestation`; `client.receipts.attestation(id)` returns the provider's signed receipt.
|
|
72
|
+
|
|
73
|
+
## Batch jobs
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
job = client.batches.create(batch_model, messages, max_tokens=256) # a model whose price carries batch_input
|
|
77
|
+
done = client.batches.wait(job["id"]) # the completion; done.receipt.batch is True
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Before and after a call
|
|
81
|
+
|
|
82
|
+
`quote(model, prompt=, max_tokens=)` (no key), `balance()`, `receipts.get(id)` (with `log`, the request's leaf in the usage log), `receipts.list(limit=, model=, status=)` (a key's own, or with only a session the account's), `models.list()`, `pricing()`, `embeddings.create(model, input)`. `usdf.estimate` reproduces the gateway's hold arithmetic.
|
|
83
|
+
|
|
84
|
+
## The account (session)
|
|
85
|
+
|
|
86
|
+
Everything a key can do (requests, quotes, the balance, receipts, tools) needs only the key: create one on the dashboard at usdf.fi. The account's own calls take a session: sign in at usdf.fi and pass its session token, `USDF(session_token=...)` or `USDF_SESSION_TOKEN` (it lasts about an hour). There is no email and password sign-in. `client.auth.info()` reads `GET /v1/auth` (the sign-in in force and every account path); `client.auth.sign_out()` ends the session at the gateway.
|
|
87
|
+
|
|
88
|
+
- `keys.create(label=, budget_units=, rate_limit_rpm=)`, `keys.list(revoked=)`, `keys.revoke(id)`, `keys.rotate(id)` (a new key with the old one's controls; the old one is revoked), `keys.limits(id)`, `keys.update(id, spend_limit_hour_units=, spend_limit_day_units=, spend_limit_month_units=, allowed_models=, paused=, no_logs=, private_tier=, webhook_url=)`, `keys.pause(id)`, `keys.resume(id)`.
|
|
89
|
+
- `account.get()`, `account.overview()`, `account.activity(model=, status=, from_=, to=, limit=)`, `account.activity_csv(...)`, `account.settings()`, `account.update_settings(...)`, `account.analytics(days=)`, `account.withdraw(units, chain_id=)` (to the linked wallet).
|
|
90
|
+
- `links.create(units, memo=, expires_in_days=)` (the token is returned once), `links.list()`, `links.get(token)`, `links.claim(token)`, `links.cancel(link_id)`.
|
|
91
|
+
- `agents.create(slug, name, **policy)`, `agents.list()`, `agents.get(slug)`, `agents.update(slug, **policy)`, `agents.fund(slug, units, direction=, idempotency_key=)`, `agents.pay(slug, to, units, memo=, idempotency_key=)`, `agents.payments(slug)`, `agents.rotate_key(slug)`.
|
|
92
|
+
- `zap.quote(token, amount, recipient=, slippage_bps=)`: ETH or a token with a liquidity pool against USDG, into USDF, with the transactions to send. No key.
|
|
93
|
+
- `pool.read()`: the USDF/USDG pool's price, tick, whether it sits in the seeded band, its liquidity and reserves. No key.
|
|
94
|
+
|
|
95
|
+
Money: base units (1 unit = $0.000001 = 0.000001 USDF); amounts are `Amount(units, usd, usdf)` or `{units, usd, usdf}`.
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Every refusal raises `USDFError` with the gateway's `status`, `type`, `code`, `message`, `request_id` and `retry_after`. Nothing is charged for a refusal.
|
|
100
|
+
|
|
101
|
+
## Changes
|
|
102
|
+
|
|
103
|
+
**0.1.2.** A wallet payment goes to the payee the gateway names, not to a hot wallet fixed in the package: the gateway rotated its custody hot wallet on 2026-10-26, and 0.1.1, which pays only the retired one (`0x52Fe...9F05`), refuses the new one's 402 with `untrusted_payee` before signing anything. The 402's payee is taken as it stands from `https://api.usdf.fi`; through any other base URL it must be the one `https://api.usdf.fi/v1/config` publishes, or the new hot wallet (`GATEWAY_PAYEE`). `pay_to` and `USDF_PAY_TO` still pin a payee.
|
|
104
|
+
|
|
105
|
+
**0.1.1.** The default base URL is `https://api.usdf.fi`, the gateway's own domain; 0.1.0 used the hosting provider's hostname for the same gateway, which still answers. `base_url` or `USDF_BASE_URL` still points the client elsewhere. No other change.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "usdf"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Client for the USDF metered inference gateway: run a prompt paid from a prepaid key or per request with x402 in USDF or USDG, OpenAI-compatible chat completions with receipts, batch jobs, quotes, balances, keys, links, agents, and a LangChain chat model."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["usdf", "inference", "openai-compatible", "x402", "receipts", "langchain", "robinhood-chain"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
httpx = ["httpx>=0.27"]
|
|
22
|
+
# A wallet that pays per request over x402 (eth-account), and the LangChain chat model.
|
|
23
|
+
wallet = ["eth-account>=0.13"]
|
|
24
|
+
langchain = ["langchain-core>=0.3"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://usdf.fi"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/usdf"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["src/usdf", "README.md", "LICENSE", "pyproject.toml"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Client for the USDF metered inference gateway."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_BASE_URL, SDK_VERSION, USDF, ChatCompletionStream, RunResult, X402
|
|
4
|
+
from .errors import USDFError
|
|
5
|
+
from .estimate import cost_units, estimate_prompt_tokens, max_cost_units, max_prompt_tokens, units
|
|
6
|
+
from .types import (
|
|
7
|
+
Account,
|
|
8
|
+
Amount,
|
|
9
|
+
ApiKey,
|
|
10
|
+
Balance,
|
|
11
|
+
ChatCompletion,
|
|
12
|
+
CreatedKey,
|
|
13
|
+
KeyLimits,
|
|
14
|
+
Pricing,
|
|
15
|
+
PricingModel,
|
|
16
|
+
Quote,
|
|
17
|
+
Receipt,
|
|
18
|
+
ReceiptLookup,
|
|
19
|
+
Session,
|
|
20
|
+
X402Receipt,
|
|
21
|
+
)
|
|
22
|
+
from .x402 import (
|
|
23
|
+
CHAIN_ID,
|
|
24
|
+
DEFAULT_MAX_AMOUNT,
|
|
25
|
+
GATEWAY_PAYEE,
|
|
26
|
+
GATEWAY_ORIGIN,
|
|
27
|
+
PERMIT2_ADDRESS,
|
|
28
|
+
PUBLIC_RPC_URL,
|
|
29
|
+
USDF_ADDRESS,
|
|
30
|
+
USDG_ADDRESS,
|
|
31
|
+
X402_EXACT_PERMIT2_PROXY,
|
|
32
|
+
PaymentLimits,
|
|
33
|
+
Signer,
|
|
34
|
+
check_accept,
|
|
35
|
+
choose_accept,
|
|
36
|
+
decode_payment_required,
|
|
37
|
+
decode_payment_response,
|
|
38
|
+
decode_uint256,
|
|
39
|
+
encode_payment_signature,
|
|
40
|
+
approve_permit2,
|
|
41
|
+
eth_account_signer,
|
|
42
|
+
permit2_allowance_call,
|
|
43
|
+
permit2_approval_call,
|
|
44
|
+
payment_limits,
|
|
45
|
+
read_permit2_allowance,
|
|
46
|
+
sign_exact_payment,
|
|
47
|
+
wallet_signer,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"USDF",
|
|
52
|
+
"USDFError",
|
|
53
|
+
"X402",
|
|
54
|
+
"DEFAULT_BASE_URL",
|
|
55
|
+
"SDK_VERSION",
|
|
56
|
+
"RunResult",
|
|
57
|
+
"ChatCompletionStream",
|
|
58
|
+
"Account",
|
|
59
|
+
"Amount",
|
|
60
|
+
"ApiKey",
|
|
61
|
+
"Balance",
|
|
62
|
+
"ChatCompletion",
|
|
63
|
+
"CreatedKey",
|
|
64
|
+
"KeyLimits",
|
|
65
|
+
"Pricing",
|
|
66
|
+
"PricingModel",
|
|
67
|
+
"Quote",
|
|
68
|
+
"Receipt",
|
|
69
|
+
"ReceiptLookup",
|
|
70
|
+
"Session",
|
|
71
|
+
"X402Receipt",
|
|
72
|
+
"cost_units",
|
|
73
|
+
"estimate_prompt_tokens",
|
|
74
|
+
"max_cost_units",
|
|
75
|
+
"max_prompt_tokens",
|
|
76
|
+
"units",
|
|
77
|
+
"PERMIT2_ADDRESS",
|
|
78
|
+
"X402_EXACT_PERMIT2_PROXY",
|
|
79
|
+
"CHAIN_ID",
|
|
80
|
+
"USDF_ADDRESS",
|
|
81
|
+
"USDG_ADDRESS",
|
|
82
|
+
"GATEWAY_PAYEE",
|
|
83
|
+
"GATEWAY_ORIGIN",
|
|
84
|
+
"PUBLIC_RPC_URL",
|
|
85
|
+
"DEFAULT_MAX_AMOUNT",
|
|
86
|
+
"PaymentLimits",
|
|
87
|
+
"payment_limits",
|
|
88
|
+
"check_accept",
|
|
89
|
+
"Signer",
|
|
90
|
+
"choose_accept",
|
|
91
|
+
"decode_payment_required",
|
|
92
|
+
"decode_payment_response",
|
|
93
|
+
"decode_uint256",
|
|
94
|
+
"encode_payment_signature",
|
|
95
|
+
"eth_account_signer",
|
|
96
|
+
"permit2_allowance_call",
|
|
97
|
+
"permit2_approval_call",
|
|
98
|
+
"sign_exact_payment",
|
|
99
|
+
"wallet_signer",
|
|
100
|
+
"read_permit2_allowance",
|
|
101
|
+
"approve_permit2",
|
|
102
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""HTTP transport: httpx when installed (and not disabled), the standard library's urllib otherwise.
|
|
2
|
+
|
|
3
|
+
Both return the same shape so the client never knows which ran: a status, lower-cased headers, and
|
|
4
|
+
either the whole body or an iterator of byte chunks for streams.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, Iterator, Optional
|
|
15
|
+
|
|
16
|
+
try: # optional
|
|
17
|
+
import httpx # type: ignore
|
|
18
|
+
except Exception: # pragma: no cover - depends on the environment
|
|
19
|
+
httpx = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Response:
|
|
24
|
+
status: int
|
|
25
|
+
headers: Dict[str, str]
|
|
26
|
+
body: bytes
|
|
27
|
+
|
|
28
|
+
def json(self) -> Any:
|
|
29
|
+
if not self.body:
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(self.body.decode("utf-8"))
|
|
33
|
+
except ValueError:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class StreamResponse:
|
|
39
|
+
status: int
|
|
40
|
+
headers: Dict[str, str]
|
|
41
|
+
chunks: Iterator[bytes]
|
|
42
|
+
close: Any
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pick_transport(name: Optional[str]) -> str:
|
|
46
|
+
name = name or os.environ.get("USDF_TRANSPORT") or ("urllib" if os.environ.get("USDF_NO_HTTPX") else None)
|
|
47
|
+
if name == "httpx":
|
|
48
|
+
if httpx is None:
|
|
49
|
+
raise RuntimeError("httpx is not installed; pip install usdf[httpx]")
|
|
50
|
+
return "httpx"
|
|
51
|
+
if name == "urllib":
|
|
52
|
+
return "urllib"
|
|
53
|
+
return "httpx" if httpx is not None else "urllib"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Transport:
|
|
57
|
+
def __init__(self, name: Optional[str] = None):
|
|
58
|
+
self.name = pick_transport(name)
|
|
59
|
+
self._client = httpx.Client(timeout=None) if self.name == "httpx" else None
|
|
60
|
+
|
|
61
|
+
def request(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> Response:
|
|
62
|
+
if self._client is not None:
|
|
63
|
+
r = self._client.request(method, url, headers=headers, content=body, timeout=timeout)
|
|
64
|
+
return Response(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.content)
|
|
65
|
+
req = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
68
|
+
return Response(res.status, {k.lower(): v for k, v in res.headers.items()}, res.read())
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
return Response(e.code, {k.lower(): v for k, v in e.headers.items()}, e.read())
|
|
71
|
+
|
|
72
|
+
def stream(self, method: str, url: str, headers: Dict[str, str], body: Optional[bytes], timeout: float) -> StreamResponse:
|
|
73
|
+
if self._client is not None:
|
|
74
|
+
req = self._client.build_request(method, url, headers=headers, content=body, timeout=timeout)
|
|
75
|
+
r = self._client.send(req, stream=True)
|
|
76
|
+
return StreamResponse(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.iter_bytes(), r.close)
|
|
77
|
+
req = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
78
|
+
try:
|
|
79
|
+
res = urllib.request.urlopen(req, timeout=timeout)
|
|
80
|
+
except urllib.error.HTTPError as e:
|
|
81
|
+
data = e.read()
|
|
82
|
+
return StreamResponse(e.code, {k.lower(): v for k, v in e.headers.items()}, iter([data]), lambda: None)
|
|
83
|
+
|
|
84
|
+
def chunks() -> Iterator[bytes]:
|
|
85
|
+
while True:
|
|
86
|
+
piece = res.read1(8192) if hasattr(res, "read1") else res.read(8192)
|
|
87
|
+
if not piece:
|
|
88
|
+
return
|
|
89
|
+
yield piece
|
|
90
|
+
|
|
91
|
+
return StreamResponse(res.status, {k.lower(): v for k, v in res.headers.items()}, chunks(), res.close)
|