Skip to content

feat(x402): settle chat at the actual cost on Base with upto (Permit2) - #75

Open
VickyXAI wants to merge 7 commits into
mainfrom
feat/x402-upto-permit2
Open

VickyXAI wants to merge 7 commits into
mainfrom
feat/x402-upto-permit2

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Why

blockrun.ai settles every x402 chat call with exact: the wallet signs a fixed pre-call quote, so discounts the gateway only learns after the call can never reach x402 callers. A DeepSeek prompt-cache hit is one example ($0.028/M cached input vs $0.14/M). The gateway can offer upto as accepts[1], with exact staying at accepts[0] (BILLING_UPTO_DEEPSEEK / BILLING_UPTO_ALL). With upto it settles the actual amount, which is never more than the signed ceiling.

What

blockrun_llm/x402_upto.py (new) and chat wiring in LLMClient / AsyncLLMClient, covering non-stream and stream calls in both sync and async.

Signing follows the official @x402/evm 2.28.0 UptoEvmScheme / trySignEip2612PermitExtension:

  • Permit2 PermitWitnessTransferFrom: spender 0x4020…0002, witness {to: payTo, facilitator: extra.facilitatorAddress, validAfter: 0}, deadline now + maxTimeoutSeconds, random 256-bit nonce.
  • The optional EIP-2612 permit approves Permit2 for exactly the ceiling. The proxy reverts with Permit2612AmountMismatch otherwise, and CDP rejects MaxUint256. It is attached as extensions.eip2612GasSponsoring.info, merged with @x402/core semantics.
  • The tests pin a vector generated by the official client (scripts/gen-upto-vector.mjs). The payload, both signatures and both EIP-712 digests match it byte for byte.

Selection: never worse than exact. upto is used only when all of these hold:

  • the 402 offers an EVM upto option with extra.facilitatorAddress, for USDC on a network in EVM_NETWORKS;
  • the USDC balance is at least the ceiling;
  • the Permit2 allowance is at least the ceiling, or the 402 declares eip2612GasSponsoring (a wallet with no ETH works);
  • the ceiling passes the spend limits.

In every other case, and on any RPC or signing error, the SDK signs exact as before and logs at debug level only. Other rules:

  • If the gateway rejects an upto payment before serving anything (a 402, or a 4xx payment-verification body), the SDK retries once with the same 402's exact entry. It remembers the rejection for this client and network. If exact is rejected too, the original error surfaces. A 2xx response or a stream already in progress is never retried.
  • Only one gas-sponsored upto payment per wallet and network can be in flight. The SDK records the USDC nonce each permit signs over. While the on-chain nonce hasn't moved and the permit's deadline hasn't passed, any call that would need another permit pays exact. An allowance that already covers the ceiling means upto with no permit.
  • Chain reads go to the public Base RPCs, now shared with get_balance() through EVM_NETWORKS, with a 3 s timeout each. The async client reads without blocking the event loop.
  • Opt out with payment_scheme="exact" or BLOCKRUN_PAYMENT_SCHEME=exact. Solana, AnthropicClient and the non-chat endpoints are unchanged.

Accounting: a ceiling is not a charge.

  • An upto call books the settled amount from PAYMENT-RESPONSE when the gateway reports one. Otherwise it books the ceiling and labels it.
  • Labels: ChatResponse.payment_scheme and cost_is_ceiling (also on stream chunks), get_spending()["ceiling_usd"], cost_basis on cost-log rows, and (upto ceiling) in transactions.log.
  • Spend limits count the ceiling in full.
  • When the 402 also offers upto, exact books and caps on its own amount, because the body's price is then the upto ceiling.

Verification

  • pytest tests/unit on 3.13: 1063 passed. The CI 3.9 command: 952 passed, 11 skipped. black --check . and ruff check . are clean. mypy has no new errors (249 now vs 253 on main).
  • 76 new tests in tests/unit/test_x402_upto.py: the reference vector, selection fallbacks, the sponsored permit being attached or skipped, the nonce guard, rejection with one exact retry, accounting, streaming, and chain reads.
  • Mutation-checked: disabling the nonce guard, the permit-value rule, the rejection retry, the settled-amount cap, the balance check or the price-vs-amount fix each makes a test fail.
  • The chain reads were checked live and read-only against Base public RPCs (sync and async).
  • The live blockrun.ai 402 for deepseek/deepseek-chat does not offer upto yet, so against production this change currently signs exact, exactly as today.

Open points

  • The gateway's PAYMENT-RESPONSE has no settled amount yet, so non-stream upto calls book the labeled ceiling until the server adds it. Streams always will, because their header is sent before settlement.
  • Version is not bumped and nothing is published.

🤖 Generated with Claude Code

1bcMax and others added 6 commits September 30, 2026 22:50
…rse than exact

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) can never reach an
x402 caller. When a 402 also offers `upto`, LLMClient / AsyncLLMClient chat
calls (non-stream and stream) now sign a Permit2 PermitWitnessTransferFrom for
the ceiling and the gateway settles the actual amount.

Taken only when it cannot be worse than exact: an EVM upto offer with
extra.facilitatorAddress for USDC on a network in EVM_NETWORKS, a USDC balance
covering the ceiling, and either a Permit2 allowance covering it or a declared
eip2612GasSponsoring extension (then a gasless EIP-2612 permit for exactly the
ceiling rides along, so a wallet with no ETH works). Any RPC or signing error,
or a ceiling over a spend limit, signs exact as before. A payment rejected
before anything is served is retried once with exact and the client stays on
exact for that network. One gas-sponsored permit per wallet+network in flight,
tracked by the USDC nonce it signs over, since a second permit over the same
nonce reverts on-chain.

Signing is byte-identical to the official @x402/evm 2.28.0 client: the tests
pin a vector that client generated (scripts/gen-upto-vector.mjs).

A ceiling is not a charge: ChatResponse / chunk payment_scheme and
cost_is_ceiling, get_spending()["ceiling_usd"], cost_basis on cost-log rows,
and a marker in transactions.log. A settled amount from PAYMENT-RESPONSE is
booked when reported. Opt out with payment_scheme="exact" or
BLOCKRUN_PAYMENT_SCHEME=exact.
… calls sign one permit

Three concurrent calls in one client all read the same USDC nonce before any
recorded a pending permit and each signed one over it; live, one settled and
two reverted. A call that might sign a permit now claims a per wallet+network
preflight marker (under a lock, no await inside) before its read. A call that
finds it taken never signs a permit: upto only if its own read shows the
allowance covers the ceiling, else exact. Released when the preflight ends; a
signed permit continues as the nonce record.
…2s, book stream ceilings

- A rejection that follows a 502/503 replay of the same upto payment is no
  longer answered with an exact payment. Upto settles after the call is
  served, so the replay's "verification failed" can mean the first send
  settled (Permit2 nonce used). Paying exact on top would charge twice. This
  applies to non-stream and stream, sync and async.
- A 402 that offers only upto, when upto cannot be used, raises PaymentError.
  extract_payment_details falls back to accepts[0], so it was being signed as
  an EIP-3009 transfer for the whole ceiling.
- Streams book the upto ceiling whatever their pre-settlement PAYMENT-RESPONSE
  carries.
- Exact spend limits cap on the signed (header) amount, which includes the tx
  fee, not the body's lower base price.
- An upto rejection keeps the client on exact for 10 minutes, not for its
  whole life.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… reported as a charge

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…quote; note per-process guards

The exact quote prices output at a tenth of max_tokens (OUTPUT_QUOTE_FACTOR),
so a long answer settles for more under upto than exact would have charged.
The "never worse than exact" claim was false, and the docs now say so. They
also say the in-flight permit guards are per process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@VickyXAI VickyXAI changed the title feat(x402): pay the actual cost on Base with upto (Permit2), never worse than exact feat(x402): settle chat at the actual cost on Base with upto (Permit2) Oct 2, 2026
…s in the shared signer

- The upto->exact fallback now needs a definite verification failure: the
  gateway's "Payment verification failed" body, a verify-failure code the chat
  route emits (PAYMENT_INVALID / PAYMENT_UNFUNDED), or a fresh payment-required
  challenge. A first-send PAYMENT_REPLAY, or a body naming an earlier paid use
  (recoverable / poll_url / job_id), means that authorization was already
  served and paid for (e.g. a duplicated send); paying exact on top charged
  twice. Applies to sync/async, stream/non-stream (shared
  _may_fall_back_to_exact). _is_payment_rejection is unchanged.
- A PAYMENT_REPLAY now raises a PaymentError with the gateway's message and
  poll_url instead of "Check your wallet balance".
- extract_payment_details raises ValueError for an upto-only 402 (chat opts in
  with allow_upto=True and keeps its own clearer PaymentError), and
  create_payment_payload refuses a non-exact scheme; every signer passes the
  requirement's scheme. No non-chat endpoint signs an upto ceiling as EIP-3009.
- The chat upto-only refusal says so when upto was sent and the gateway
  refused it, instead of listing only local reasons.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant