Skip to content

Batch-settlement TS sdk#2061

Merged
CarsonRoscoe merged 72 commits intomainfrom
batched-settlement
May 4, 2026
Merged

Batch-settlement TS sdk#2061
CarsonRoscoe merged 72 commits intomainfrom
batched-settlement

Conversation

@phdargen
Copy link
Copy Markdown
Collaborator

@phdargen phdargen commented Apr 17, 2026

Batch-Settlement EVM Scheme — TypeScript SDK

Implements the batch-settlement scheme for EVM in the TS SDK according to the specs outlined in #2051

Scheme implementation: key design decisions

Unlike exact / upto, this scheme is stateful (per-channel session, persisted), multi-payload (deposit, voucher, claim/refund/settle actions), and most paid requests do not trigger an onchain transaction at settle time. The implementation handles all of this transparently via lifecycle hooks the scheme registers on itself, so apps just register(network, scheme):

  • Server onBeforeVerify — detects stale client cumulative state, embeds the authoritative voucher snapshot in requirements.extra, and aborts with batch_settlement_stale_cumulative_amount so the middleware emits a corrective 402.
  • Server onAfterVerify — persists the verified channel snapshot from verifyResponse.extra (balance, totalClaimed, withdraw timer, refund nonce). Single point where onchain state is reconciled into the local session.
  • Server onBeforeSettle — the central piece. For voucher payloads it skips the facilitator entirely: CAS-bumps chargedCumulativeAmount, validates against the signed cap, and returns { skip: true, result } so the middleware responds immediately. If the client requested a cooperative refund, it rewrites the payload in place to a refundWithSignature settle action (claim of latest voucher + refund of unused balance), authorizes it locally with the receiver authorizer signer, and lets the facilitator submit it onchain.
  • Server onAfterSettle — writes back the post-tx snapshot (deposit → updated balance, refund → session deletion).
  • Client onPaymentResponse — on success, mirrors result.extra into local channel state; on a corrective 402 it copies the server's authoritative voucher into local storage and returns { recovered: true }, signaling the transport to retry transparently with a fresh voucher.

Other notable choices:

  • Pluggable storage with InMemory* and File* defaults. The server interface adds a compareAndSet(channelId, expected, next) primitive — the only correctness-critical piece, used by onBeforeSettle to serialize concurrent requests on the same channel.
  • Receiver authorizer is server-owned by default (recommended): claims/refunds remain valid across facilitator changes. Optional fallback to extra.receiverAuthorizer from the facilitator's /supported.
  • Channel manager is opt-in. scheme.createChannelManager(facilitator, network) runs a periodic tick that triggers claim / settle / refund based on configurable policies (interval, idle, threshold, pending withdrawal, shutdown). Reuses the same scheme storage and authorizer signer.
  • Facilitator is a thin router. Discriminates payload type and delegates to dedicated verify* / settle* modules per action; each returns an extra snapshot of post-tx channel state for the server to consume.

Changes outside mechanisms/evm/batch-settlement

These changes were necessary to host a stateful, multi-tx scheme cleanly. All are designed as general-purpose extension points that any current or future scheme can use; nothing is batch-settlement-specific.

core/types/mechanisms.tsSchemeClientHooks / SchemeServerHooks

Added optional schemeHooks? to SchemeNetworkClient and SchemeNetworkServer. When a scheme is register()-ed on x402Client / x402ResourceServer, its declared hooks are auto-wired into the existing per-instance hook system.

Why: any scheme with non-trivial protocol state needs to participate in the verify/settle lifecycle without forcing every consumer to manually call server.onBeforeVerify(scheme.handleX) etc. Makes scheme integration a single register() call and keeps the existing user-level hook API unchanged.

core/server/x402ResourceServer.tsBeforeSettleHook skip outcome

BeforeSettleHook can now return { skip: true, result: SettleResponse }. The middleware uses the provided response directly and runs onAfterSettle + extension enrichSettlementResponse hooks normally, but does not call facilitator.settle().

Why: any scheme where the resource server can confirm payment locally without a facilitator round-trip benefits — batch-settlement uses it for the common voucher case (0 gas, 0 RPC), but it equally supports off-chain receipt schemes, prepaid quota schemes, mock facilitators, etc.

core/client/x402Client.ts + core/http/x402HTTPClient.tsOnPaymentResponseHook

New client hook fired after every paid request:

type OnPaymentResponseHook = (
  ctx: PaymentResponseContext,
) => Promise<void | { recovered: true }>;

PaymentResponseContext is a discriminated bag with settleResponse (success/failure), paymentRequired (corrective 402), or error. x402HTTPClient.processPaymentResult(payload, getHeader, status) parses headers and surfaces a single recovered flag to the transport. Also added processResponse(response) returning a discriminated x402PaymentResult (success / settle_failed / payment_required / error / passthrough) for app-level consumers.

Why: schemes with client-side state need a deterministic place to react to what actually happened on the wire — either to update their model (success) or to repair it and request a retry (corrective 402). Generalizes to any scheme needing response-driven state (subscriptions, refresh tokens, off-chain receipt acks, etc.).

http/{fetch,axios}/src/index.ts — recovery path

Both transport wrappers now call processPaymentResult(...) after the paid request and, if a hook returns { recovered: true }, regenerate the payload and reissue once. Recovery is bounded to a single retry to prevent loops. No behavior change for schemes that don't register the hook.

core/types/facilitator.ts + core/http/httpFacilitatorClient.tsextra on VerifyResponse / SettleResponse

Added optional extra?: Record<string, unknown> (separate from extensions) and parsed it in the HTTP facilitator's zod schemas.

Why: extensions is reserved for the x402 extension framework. extra is the symmetric counterpart of PaymentRequirements.extra — scheme-specific protocol data flowing back from the facilitator. Batch-settlement uses it for post-tx channel snapshots; any future scheme needing scheme-level metadata in a verify/settle response gets it for free.

Also added the amount field to the SettleResponse zod schema (the field already existed on the type but wasn't being parsed), so dynamic-pricing schemes (upto, batch-settlement) round-trip correctly over HTTP.

Tests / examples

  • Unit + integration tests
  • E2E tests
  • Examples under examples/typescript/{servers,clients,facilitator}/batch-settlement[-streaming]/, including a streaming server with mid-stream voucher renewal

Links

Specs: #2051
x402BatchSettlement contract: #1950

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
x402 Ready Ready Preview, Comment May 4, 2026 1:41pm

Request Review

@jithinraj
Copy link
Copy Markdown
Contributor

Great work @phdargen. One thing worth pinning down before other SDKs copy it is the corrective-402 recovery contract.

Right now the important path is: stale cumulative detected -> server returns authoritative voucher snapshot -> client replaces local state -> one retry only.

A tiny vector set for that path would make the CAS/recovery behavior portable across SDKs without changing this PR.

@up2itnow0822
Copy link
Copy Markdown
Contributor

Great direction on batch-settlement. This is the right shape for repeat paid MCP calls where the client should not do a full settle path on every small request.

One operator note from the AgentPay MCP side: batch settlement changes the audit surface from one payment event to channel lifecycle state.

The checklist we are using for paid MCP integrations now includes:

  • deposit approval and per-channel deposit caps,
  • cumulative voucher cap checks before signing, not only per-request price checks,
  • server-owned receiverAuthorizer pinning or rotation approval,
  • compare-and-set storage per channelId so overlapping paid calls cannot sign from stale state,
  • corrective 402 recovery logs with the server cumulative amount, signed max claimable amount, recovery proof, and retry status,
  • refund and claim audit rows that tie off-chain vouchers back to on-chain settlement proof.

We added the AgentPay MCP compatibility recipe and docs test here:

This is not a blocker for PR #2061. It is a production-readiness note for teams that will wire batch-settlement into paid MCP tools or streaming paid calls.

@phdargen
Copy link
Copy Markdown
Collaborator Author

phdargen commented May 1, 2026

Updated to latest deployment @CarsonRoscoe

@CarsonRoscoe CarsonRoscoe merged commit 45d7d19 into main May 4, 2026
18 checks passed
@CarsonRoscoe CarsonRoscoe deleted the batched-settlement branch May 4, 2026 20:44
@shuhei0866
Copy link
Copy Markdown
Contributor

shuhei0866 commented May 5, 2026

Excited to see batch-settlement land. Planning to port to the Python SDK as a follow-up, following the same layout pattern as upto (#2023) and exact. Will also include e2e fixtures and a Python facilitator example to mirror the TS surface, and incorporate the recovery-path test vectors raised above by @jithinraj / @Bortlesboat where it makes sense for the Python wire layer.

Anyone already on this @phdargen? If not, I'll open a draft PR over the next 1-2 weeks. Happy to coordinate on the corrective-402 wire contract before locking in Python-side behavior. Particularly want to lock down EIP-712 byte equivalence and the null/undefined/absent boundaries early, since those are where Python ↔ TS interop usually breaks (#1762 was one example).

@shuhei0866
Copy link
Copy Markdown
Contributor

Hi all — opened #2199 as a draft PR1 of the planned Python port of @x402/evm/batch-settlement.

The PR description outlines:

  • foundation deliverable (constants / ABI / 18 wire-format models / 56 tests)
  • 12 wire decisions (D1-D12) locked down for the series
  • stacked series request: would feat/batch-settlement-python-sdk be a good base branch on upstream? (mirroring feat/python-v2-sdk and the sibling feat/batch-settlement-go-sdk)

Happy to adapt the shape if you'd prefer something else. No rush on review of #2199 itself — flagging now mainly for strategy alignment.

cc @phdargen @CarsonRoscoe

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

Labels

examples Changes to examples go python sdk Changes to core v2 packages specs Spec changes or additions typescript website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants