Repository navigation
feat(solana): opt-in batch-settlement through @blockrun/llm's SolanaBatchPayer - #446
Conversation
Prepares Solana batch-settlement through @blockrun/llm's SolanaBatchPayer, which needs @x402/core and @x402/svm ~2.28. - @x402/core 2.28 turns on its own spend controls by default ($1 per payment, default assets only). ClawRouter enforces its own limits and BlockRun prices some calls above $1, so the proxy and doctor clients disable them, keeping pre-2.28 behavior exactly. Test clients are configured the same way. - @x402/svm 2.28 declares @solana/program-client-core ^6.1.0, which nests a second @solana/signers + transactions (6.10). An override points it at kit's 8.4.0 copy: svm uses only getAccountMetaFactory, whose signature is identical in 6.10 and 8.4. One @solana/* family stays in the tree. - viem pinned ~2.56.9: @blockrun/llm needs >=2.56.3, and the newest 2.57.x is days old. Lockfile re-resolved from the previous one with npm 11; the only changes are these packages, ox (viem's) and @anthropic-ai/sdk (@blockrun/llm's).
…atchPayer
Solana chat calls can now be paid with x402 batch-settlement: one deposit
opens a channel with BlockRun, each call carries an authorization for its
ceiling, and the gateway charges what the call actually cost. Off by default;
CLAWROUTER_SOLANA_BATCH=1 or startProxy({ solanaBatch }) turns it on.
ClawRouter does not implement the protocol. It drives the same payer
SolanaLLMClient ships (in-doubt payments never replaced, cross-process channel
lock, 429 cooldown, refused deposits park the wallet on exact), with BlockRun's
operator key pinned as the only trusted operator.
- Spend rules: the payer's client gets ClawRouter's own hooks via
configureClient (strict-cap check, SpendControl, signing recorders), so
batch payments are checked at their per-call ceiling exactly like exact
ones, and only when the user configured limits. The session cap then books
what each call was charged, not the ceiling: signatures a pay() proved not
charged are voided, the charged one is settled down to the receipt amount.
- payment-preauth routes non-streaming chat completions on the Solana rail to
the payer: paid -> returned; fallback -> exact against the freshest
challenge; served/failed -> returned unpaid; a throw (payment in doubt) ->
rethrown, never paid again.
- proxy: BatchPaymentUnresolvedError stops the model chain (payment_in_doubt)
instead of trying the next model, which would sign a new payment.
- tsup: @anthropic-ai/sdk (lazy-imported by @blockrun/llm, unused here) stays
external: -0.65MB and no child_process in dist.
Tests: routing unit tests for every pay() outcome, and an end-to-end test
through startProxy with the real payer and @x402/svm scheme against a local
gateway and RPC. Mutation-checked: booking the ceiling instead of the charge,
and dropping the in-doubt chain stop, each fail them.
Supersedes #435 (SkinGem), which started this.
…keeps the ceiling; notify onPayment Codex review of the batch integration: - [P1] payment_in_doubt is now a flag the fallback loop checks before the per-model timeout branch and after both internal retries (explicit-pin and rate-limit), so none of them can pay another model for the same request. A non-payer failure after a batch send (an abort, a transport error) is wrapped as PaymentInDoubtError and treated the same way. - [P1] A paid batch response without a receipt makes the payer report 0. That proves nothing, so the session cap keeps the signed ceiling instead of settling it down to zero. - [P2] Within one pay(), a re-sign follows a proven-unpaid 429: the earlier signatures are voided before the cap checks the new one. - [P2] onPayment fires for a confirmed batch charge, with the observer guarded as on the exact path.
…zero charges stay zero; preflight defers to the channel Codex re-review: - [P1] A receipt header is not proof of the charge: the payer reports 0 for a malformed receipt or one naming no amount. The charge is now parsed from the receipt itself (extra.chargedAmount, else amount, canonical integers only); anything else keeps the signed ceiling. - [P2] recordSuccess no longer books the local estimate after any signature, so a receipt that settled a call to zero stays zero. - [P2] With batch on, chat calls skip the liquid-balance preflight: right after a deposit the wallet can look empty while the channel holds the funds. A call with no escrow falls to exact, and a payment failure still takes the free-model fallback.
… budget Codex review round 3 (gate passed, two P2s): - A batch payment in doubt is handled before the global-deadline check, so the caller gets payment_in_doubt (and the dedup cache keeps it) instead of a generic timeout that drops the dedup entry and lets an identical retry pay again. A client that left still gets the disconnect cleanup. - The payer's in-doubt replay budget is 20s instead of the SDK's 5 min: the proxy request has a deadline, and once it is aborted each replay reuses the aborted signal, so a long budget only kept the channel busy.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to The reviewed documentation describes the opt-in settlement behavior and idle closure without an established contradiction; no actionable merge risk is identified in this change. Pre-merge checks |
|
…nused escrow sol.blockrun.ai's 402 advertises batchPolicy.idleCloseSeconds = 259200: an idle channel is closed at its settled watermark and the rest returns to the wallet. The doc said only a manual closeBatchChannel() would return it.
Opt-in Solana x402 batch-settlement for chat calls. One deposit opens a channel with BlockRun; after that, each call carries an authorization for its ceiling and the gateway charges what the call actually cost. It is off by default; turn it on with
CLAWROUTER_SOLANA_BATCH=1orstartProxy({ solanaBatch }).This replaces #435 (SkinGem). ClawRouter no longer implements the protocol itself: it drives
@blockrun/llm'sSolanaBatchPayer(3.23.1), the same payerSolanaLLMClientships. That payer never replaces an in-doubt payment, locks the channel file across processes, cools down after a 429, and parks a wallet on exact after a refused deposit. BlockRun's operator key is pinned as the only trusted operator.Commits
chore(deps): @x402/* 2.21 → 2.28 and @blockrun/llm 3.10 → 3.23.1.@solana/program-client-coreat kit 8.4. svm only usesgetAccountMetaFactory, whose signature is identical, and this keeps a single@solana/*family in the tree.~2.56.9.feat(solana): the integration.@anthropic-ai/sdk(lazy-imported by @blockrun/llm and unused here) stays external, which saves 0.65MB and removeschild_processfrom dist.onPaymentfires for batch charges.Tests (1197 pass; typecheck, lint, prettier and dist smoke are clean):
payment-preauth.batch.test.ts: everypay()outcome, receipt parsing (missing, malformed or zero), and an abort after the send counting as in doubt.proxy.solana-batch.test.ts: runs end to end throughstartProxywith the real payer and the real@x402/svmscheme against a local gateway and RPC. It covers:Review
Summary by CodeRabbit
New Features
Bug Fixes
Documentation