@@ -6,6 +6,7 @@ import { privateKeyToAccount } from "viem/accounts";
66import { base , baseSepolia } from "viem/chains" ;
77import { ExactEvmScheme , type ExactEvmSchemeOptions } from "@x402/evm/exact/client" ;
88import { UptoEvmScheme as UptoEvmClientScheme , type UptoEvmSchemeOptions } from "@x402/evm/upto/client" ;
9+ import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/client" ;
910import { ExactEvmSchemeV1 } from "@x402/evm/v1" ;
1011import { toClientEvmSigner } from "@x402/evm" ;
1112import { ExactSvmScheme } from "@x402/svm/exact/client" ;
@@ -49,6 +50,23 @@ const uptoSchemeOptions: UptoEvmSchemeOptions | undefined = process.env.EVM_RPC_
4950 ? { rpcUrl : process . env . EVM_RPC_URL }
5051 : undefined ;
5152
53+ // Batch-settlement scheme uses a per-scenario salt (CHANNEL_SALT) so concurrent
54+ // e2e runs don't collide on the same on-chain channel id. An optional voucher
55+ // signer (EVM_VOUCHER_SIGNER_PRIVATE_KEY) exercises the alt-EOA voucher branch
56+ // while deposits keep using the main client signer.
57+ const channelSalt = process . env . CHANNEL_SALT as `0x${string } ` | undefined ;
58+ const voucherSignerKey = process . env . EVM_VOUCHER_SIGNER_PRIVATE_KEY as
59+ | `0x${string } `
60+ | undefined ;
61+ const voucherSigner = voucherSignerKey
62+ ? toClientEvmSigner ( privateKeyToAccount ( voucherSignerKey ) , publicClient )
63+ : undefined ;
64+ const batchSettlementOptions =
65+ channelSalt || voucherSigner
66+ ? { ...( channelSalt ? { salt : channelSalt } : { } ) , ...( voucherSigner ? { voucherSigner } : { } ) }
67+ : undefined ;
68+ const batchSettlementScheme = new BatchSettlementEvmScheme ( evmSigner , batchSettlementOptions ) ;
69+
5270// Initialize Aptos signer if key is provided
5371let aptosAccount : Account | undefined ;
5472if ( process . env . APTOS_PRIVATE_KEY ) {
@@ -75,6 +93,7 @@ if (process.env.AVM_PRIVATE_KEY) {
7593const client = new x402Client ( )
7694 . register ( "eip155:*" , new ExactEvmScheme ( evmSigner , evmSchemeOptions ) )
7795 . register ( "eip155:*" , new UptoEvmClientScheme ( evmSigner , uptoSchemeOptions ) )
96+ . register ( "eip155:*" , batchSettlementScheme )
7897 . registerV1 ( "base-sepolia" , new ExactEvmSchemeV1 ( evmSigner ) )
7998 . registerV1 ( "base" , new ExactEvmSchemeV1 ( evmSigner ) )
8099 . register ( "solana:*" , new ExactSvmScheme ( svmSigner ) )
@@ -92,44 +111,75 @@ if (avmSigner) {
92111
93112const axiosWithPayment = wrapAxiosWithPayment ( axios . create ( ) , client ) ;
94113
95- axiosWithPayment
96- . get ( url )
97- . then ( async ( response ) => {
98- const data = response . data ;
99- // Check both v2 (PAYMENT-RESPONSE) and v1 (X-PAYMENT-RESPONSE) headers
100- const paymentResponse =
101- response . headers [ "payment-response" ] || response . headers [ "x-payment-response" ] ;
102-
103- if ( ! paymentResponse ) {
104- // No payment was required
105- const result = {
106- success : true ,
107- data : data ,
108- status_code : response . status ,
109- } ;
110- console . log ( JSON . stringify ( result ) ) ;
111- process . exit ( 0 ) ;
112- return ;
114+ // Multi-request scenarios (used by batch-settlement) issue several paid requests
115+ // against the same endpoint so the server can amortise on-chain claims, then
116+ // optionally signal a cooperative refund on the last request.
117+ const numberOfRequests = Number . parseInt ( process . env . MULTI_REQUEST_COUNT ?? "1" , 10 ) ;
118+ const refundOnLastRequest = process . env . REFUND_ON_LAST === "true" ;
119+
120+ /**
121+ * Issues a single paid request and returns the parsed result.
122+ *
123+ * @returns Structured result with response data and decoded payment-response.
124+ */
125+ async function issueRequest ( ) : Promise < {
126+ success : boolean ;
127+ data : unknown ;
128+ status_code : number ;
129+ payment_response ?: ReturnType < typeof decodePaymentResponseHeader > ;
130+ } > {
131+ const response = await axiosWithPayment . get ( url ) ;
132+ const paymentResponseHeader =
133+ response . headers [ "payment-response" ] || response . headers [ "x-payment-response" ] ;
134+
135+ if ( ! paymentResponseHeader ) {
136+ return { success : true , data : response . data , status_code : response . status } ;
137+ }
138+
139+ const decodedPaymentResponse = decodePaymentResponseHeader ( paymentResponseHeader ) ;
140+ return {
141+ success : decodedPaymentResponse . success ,
142+ data : response . data ,
143+ status_code : response . status ,
144+ payment_response : decodedPaymentResponse ,
145+ } ;
146+ }
147+
148+ try {
149+ const results : Awaited < ReturnType < typeof issueRequest > > [ ] = [ ] ;
150+ let lastChannelId : string | undefined ;
151+ for ( let i = 0 ; i < numberOfRequests ; i ++ ) {
152+ const isLast = i === numberOfRequests - 1 ;
153+
154+ if ( isLast && refundOnLastRequest && lastChannelId ) {
155+ batchSettlementScheme . requestRefund ( lastChannelId ) ;
113156 }
114157
115- const decodedPaymentResponse = decodePaymentResponseHeader ( paymentResponse ) ;
116-
117- const result = {
118- success : decodedPaymentResponse . success ,
119- data : data ,
120- status_code : response . status ,
121- payment_response : decodedPaymentResponse ,
122- } ;
123-
124- // Output structured result as JSON for proxy to parse
125- console . log ( JSON . stringify ( result ) ) ;
126- process . exit ( 0 ) ;
127- } )
128- . catch ( ( error ) => {
129- console . error ( JSON . stringify ( {
158+ const result = await issueRequest ( ) ;
159+ results . push ( result ) ;
160+
161+ const channelId = result . payment_response ?. extra ?. channelId ;
162+ if ( typeof channelId === "string" && channelId . length > 0 ) {
163+ lastChannelId = channelId ;
164+ }
165+ }
166+
167+ const last = results [ results . length - 1 ] ! ;
168+ const aggregate =
169+ numberOfRequests > 1
170+ ? { ...last , requests : results , request_count : numberOfRequests }
171+ : last ;
172+
173+ console . log ( JSON . stringify ( aggregate ) ) ;
174+ process . exit ( 0 ) ;
175+ } catch ( error : unknown ) {
176+ const err = error as { message ?: string ; response ?: { status ?: number } } ;
177+ console . error (
178+ JSON . stringify ( {
130179 success : false ,
131- error : error . message || "Request failed" ,
132- status_code : error . response ?. status || 500 ,
133- } ) ) ;
134- process . exit ( 1 ) ;
135- } ) ;
180+ error : err . message || "Request failed" ,
181+ status_code : err . response ?. status || 500 ,
182+ } ) ,
183+ ) ;
184+ process . exit ( 1 ) ;
185+ }
0 commit comments