Skip to content
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,80 @@

All notable changes to blockrun-llm will be documented in this file.

## Unreleased

### Added
- **x402 `upto` (Permit2) on Base: pay the actual cost, not the quote.** Under
`exact` the wallet signs a fixed pre-call quote, so a discount the gateway
only learns after the call — a DeepSeek prompt-cache hit is $0.028/M input
instead of $0.14/M — could never reach an x402 caller. When a 402 also offers
`upto` (listed after `exact`), `LLMClient` / `AsyncLLMClient` chat calls
(non-stream and stream) now sign a Permit2 `PermitWitnessTransferFrom` for the
ceiling and the gateway settles the actual amount.

Upto settles the actual cost, which is not always below the exact quote:
exact prices output at a tenth of `max_tokens`, so a long answer settles for
more under upto (never more than the ceiling, the full `max_tokens`), and a
short or cache-hit one for less. `payment_scheme="exact"` keeps the fixed
quote. Upto is taken when the offer carries
`extra.facilitatorAddress` for USDC on a network in `EVM_NETWORKS`; the wallet
holds the ceiling; and Permit2 may already pull it, or the 402 declares
`eip2612GasSponsoring` — then a gasless EIP-2612 permit approving Permit2 for
exactly the ceiling rides along and the facilitator submits it, so a wallet
with no ETH works. (Exactly the ceiling: the upto proxy reverts with
`Permit2612AmountMismatch` on any other value, and CDP rejects MaxUint256.)
Balance / allowance / nonce come from public Base RPCs with a 3 s timeout
each. Anything else — no RPC, a signing error, a ceiling over a spend limit —
signs `exact` as before (debug log only). A payment the gateway rejects
before serving anything is retried once with `exact`, and that client stays
on exact for that network for 10 minutes. A rejection that follows a 502/503
replay of the same upto payment is never retried with exact (the first send
may have settled; paying exact too would charge twice). A 402 offering only
upto, when upto cannot be used, raises rather than signing the ceiling as an
exact transfer. Exact spend limits cap on the signed amount (the header's,
which includes the transaction fee), not the body's base price. Streams book
the ceiling whatever their pre-settlement `PAYMENT-RESPONSE` says.

Per wallet and network only one gas-sponsored upto payment is in flight: a
second permit over the same USDC nonce reverted on-chain in a live test of
the TS SDK. The SDK records the nonce each permit signs over and, while the
on-chain nonce has not moved and the permit's deadline has not passed, pays
exact for any call that would need another. Concurrent calls in one client
(threads or asyncio) claim a per-wallet preflight marker before their chain
read, so only one of them can sign a permit; the others pay upto only if
Permit2's allowance already covers their ceiling, else exact. Solana, the Anthropic client and
every non-chat endpoint are unchanged.

Signing matches the official `@x402/evm` 2.28.0 client byte for byte: the
tests pin a vector it generated (`scripts/gen-upto-vector.mjs`).

Opt out with `payment_scheme="exact"` or `BLOCKRUN_PAYMENT_SCHEME=exact`.
- **A ceiling is not a charge.** `ChatResponse.payment_scheme` /
`cost_is_ceiling` (also on stream chunks), `get_spending()["ceiling_usd"]`,
a `cost_basis` field on cost-log rows (`upto_settled` / `upto_ceiling`) and a
`(upto ceiling)` marker in `transactions.log`. An upto call books the settled
amount when `PAYMENT-RESPONSE` reports one, else the ceiling, labeled. Spend
limits count the ceiling in full.

### Changed
- Exact chat payments book and cap on the signed requirement's amount when the
402 also offers upto (the body's `price` is then the upto ceiling), and the
async client books a paid chat call the same way the sync one does.
- `extract_payment_details` picks the first non-`upto` requirement instead of
blindly taking `accepts[0]`.
- The upto→exact retry happens only on a definite verification failure
(`Payment verification failed`, `PAYMENT_INVALID` / `PAYMENT_UNFUNDED`, or a
fresh `payment-required` challenge), never on `PAYMENT_REPLAY` or a body
pointing at an earlier paid use (`recoverable` / `poll_url` / `job_id`): that
authorization was already served and paid for, and exact would charge twice.
A `PAYMENT_REPLAY` raises a `PaymentError` carrying the gateway's message and
`poll_url`, not "check your wallet balance". `extract_payment_details` now
raises `ValueError` for an upto-only 402 (chat opts in with
`allow_upto=True`), and `create_payment_payload` refuses a non-`exact`
`scheme`, so no non-chat endpoint signs an upto ceiling as an EIP-3009
transfer.
- `get_balance()` reads its USDC contract and RPC list from `EVM_NETWORKS`.

## 1.17.1 — 2026-09-30

### Fixed
Expand Down
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,12 +416,70 @@ That single call does all of this under the hood:

One call, no separate pay step.

#### Pay the actual cost: x402 `upto` (Base)

By default (`payment_scheme="auto"`) a chat call whose 402 also offers the x402
`upto` scheme pays with a Permit2 signature for a **ceiling** instead of an
`exact` EIP-3009 transfer for a fixed quote. The gateway then settles the
**actual** cost after the call — never more than the ceiling — so discounts it
only learns afterwards, such as prompt-cache hits, reach you.

Upto is not always cheaper. The `exact` quote prices output at a tenth of your
`max_tokens`; under upto you pay for the output you actually got. A short or
cache-hit answer costs less than exact, a long one costs more (up to the
ceiling, which prices the full `max_tokens`). If you need the fixed quote, pass
`payment_scheme="exact"`. The gateway lists `exact` first; upto is taken only
when all of these hold:

- the 402 offers an EVM `upto` requirement with `extra.facilitatorAddress`, for
USDC on a network the SDK signs on (Base, Base Sepolia);
- your wallet holds at least the ceiling in USDC;
- Permit2 can already pull the ceiling from your wallet, **or** the gateway
declares the `eip2612GasSponsoring` extension. In that case the SDK also signs
a gasless EIP-2612 permit approving the canonical Permit2 contract
(`0x0000…78BA3`) for exactly the ceiling, and the facilitator submits it —
**no ETH needed**;
- the ceiling fits your `max_cost_per_call` / `max_session_cost`.

Otherwise — or on any RPC or signing error — the SDK signs `exact` exactly as
before. If the gateway rejects an upto payment before serving anything, the
request is retried once with `exact`, and that client pays exact on that network
for the next 10 minutes. It is never retried with exact after a 502/503 replay
of the same upto payment, because the first send may already have settled.
A 402 that offers only upto, when upto cannot be used, raises instead of being
signed as exact. Solana is unchanged (exact only).

**Per wallet, only one gas-sponsored upto payment can be in flight.** Its permit
lands only when that call settles, and until then the chain still shows the old
USDC permit nonce — a second permit over the same nonce would revert on-chain.
So concurrent or not-yet-settled calls that would need a permit pay `exact`
(the SDK watches the on-chain nonce, and gives up on a permit at its deadline;
concurrent calls in one client claim the permit slot before reading the chain).
Calls where Permit2 can already pull the ceiling are unaffected. These guards
are per process: several processes paying from one wallet can still collide
(a call fails; nobody is overcharged), so run one paying process per wallet or
use `exact` there.

```python
LLMClient(payment_scheme="exact") # opt out; or BLOCKRUN_PAYMENT_SCHEME=exact
```

The signed ceiling is an upper bound, not a charge. `ChatResponse.payment_scheme`
says which scheme paid; when the gateway's `PAYMENT-RESPONSE` reports the settled
amount, `cost_usd` is that amount; when it does not (always, for streams, whose
header arrives before settlement), `cost_usd` is the ceiling and
`cost_is_ceiling` is `True`. `get_spending()["ceiling_usd"]` is the part of
`total_usd` booked at a ceiling, cost-log rows carry `cost_basis`
(`upto_settled` / `upto_ceiling`), and `transactions.log` marks ceiling rows.
Spend limits count ceilings in full, so they err on the safe side.

### What it costs, and how to verify it

- **Pay-as-you-go, per call.** You pay only the gateway price of each request
(see [Available Models](#available-models)). The free NVIDIA models are `$0`.
- **Track spend.** `client.get_spending()` returns this session's
`{total_usd, calls}`. On the API-key rail the gateway does not tell the client
`{total_usd, calls, ceiling_usd}` (`ceiling_usd`: see
[upto](#pay-the-actual-cost-x402-upto-base)). On the API-key rail the gateway does not tell the client
what a call cost, so treat that total as a floor and
[user.blockrun.ai/dashboard](https://user.blockrun.ai/dashboard) as the
authority. Every paid call also appends a line to
Expand Down Expand Up @@ -1647,6 +1705,7 @@ optional.
| `SOLANA_RPC_URL` / `SOLANA_RPC_HEADERS` / `SOLANA_RPC_API_KEY` | RPC for blockhash + mint info while signing | BlockRun's free proxy |
| `BLOCKRUN_CHAT_TIMEOUT` | Chat HTTP timeout, in seconds | `600` |
| `BLOCKRUN_MAX_COST_PER_CALL` / `BLOCKRUN_MAX_SESSION_COST` | Opt-in spend limits (wallet rail) | unlimited |
| `BLOCKRUN_PAYMENT_SCHEME` | `auto` (prefer x402 `upto`, which settles the actual cost, when the gateway offers it and the wallet can use it) or `exact` (always the fixed quote) | `auto` |

`BLOCKRUN_API_KEY_URL` is deliberately not `BLOCKRUN_API_URL`: that one names an
x402 gateway, and an API-key client must never follow it and send your key to a
Expand Down
1 change: 1 addition & 0 deletions blockrun_llm/anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
account=self._account,
recipient=details["recipient"],
amount=details["amount"],
scheme=details.get("scheme"),
network=details.get("network", "eip155:8453"),
resource_url=resource.get("url", f"{self._api_url}/v1/messages"),
resource_description=resource.get("description", "BlockRun AI API call"),
Expand Down
22 changes: 20 additions & 2 deletions blockrun_llm/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,16 @@ def save_to_cache(
wallet: str | None = None,
network: str | None = None,
client_kind: str | None = None,
cost_basis: str | None = None,
) -> None:
"""
Save a paid API response locally.

``cost_basis`` labels a cost that is not a plain exact charge: an x402 upto
call records ``"upto_settled"`` (the gateway reported the settled amount)
or ``"upto_ceiling"`` (``cost_usd`` is the signed ceiling — an upper bound,
not a confirmed charge). ``None`` (exact) writes the same rows as before.

1. Hash-keyed cache file (for TTL-based dedup)
2. Human-readable data file (browsable archive of every paid call)
3. Cost log entry (with billing metadata when supplied)
Expand All @@ -167,14 +173,16 @@ def save_to_cache(
"response": response,
"cost_usd": cost_usd,
}
if cost_basis is not None:
entry["cost_basis"] = cost_basis

try:
_cache_path(key).write_text(json.dumps(entry, default=str))
except OSError:
pass

# Save human-readable copy to ~/.blockrun/data/
_save_readable(endpoint, body, response, cost_usd)
_save_readable(endpoint, body, response, cost_usd, cost_basis=cost_basis)

# Append to the cost log (never overwritten). Pull model from the body
# if the caller didn't pass one explicitly.
Expand All @@ -185,6 +193,7 @@ def save_to_cache(
wallet=wallet,
network=network,
client_kind=client_kind,
cost_basis=cost_basis,
)


Expand All @@ -193,6 +202,8 @@ def _save_readable(
body: dict[str, Any],
response: dict[str, Any],
cost_usd: float,
*,
cost_basis: str | None = None,
) -> None:
"""Save a human-readable JSON file to ~/.blockrun/data/."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
Expand All @@ -204,6 +215,8 @@ def _save_readable(
"request": body,
"response": response,
}
if cost_basis is not None:
entry["cost_basis"] = cost_basis
try:
(DATA_DIR / filename).write_text(json.dumps(entry, indent=2, default=str))
except OSError:
Expand All @@ -218,12 +231,15 @@ def _append_cost_log(
wallet: str | None = None,
network: str | None = None,
client_kind: str | None = None,
cost_basis: str | None = None,
) -> None:
"""Append one JSONL row to ``~/.blockrun/cost_log.jsonl``.

The full schema is::

{ts, endpoint, cost_usd, model, wallet, network, client_kind}
{ts, endpoint, cost_usd, model, wallet, network, client_kind, cost_basis}

``cost_basis`` is present only for x402 upto calls (see ``save_to_cache``).

Older rows that only carry ``{ts, endpoint, cost_usd}`` are still readable
— missing fields surface as ``None`` in summary / export views.
Expand All @@ -247,6 +263,8 @@ def _append_cost_log(
entry["network"] = network
if client_kind is not None:
entry["client_kind"] = client_kind
if cost_basis is not None:
entry["cost_basis"] = cost_basis
f.write(json.dumps(entry) + "\n")
except OSError:
pass
Expand Down
Loading
Loading