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 ADDED
@@ -0,0 +1,6 @@
1
+ dist
2
+ build
3
+ *.egg-info
4
+ __pycache__
5
+ .pytest_cache
6
+ .venv
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)