The Arbiter SDK provides batch operations for processing multiple refund requests in a single call. Both batchApprove and batchDeny accept an array of { paymentInfo, nonce } objects.
Batch items are processed sequentially, not atomically. If one item fails mid-batch, all previously processed items will not be rolled back. Design your error handling accordingly.
Batch Approve
Approve multiple refund requests in one call:
const items = [
{ paymentInfo: paymentInfo1, nonce: 0n },
{ paymentInfo: paymentInfo2, nonce: 0n },
{ paymentInfo: paymentInfo3, nonce: 1n },
];
const results = await arbiter.batchApprove(items);
for (const { txHash } of results) {
console.log('Approved:', txHash);
}
Batch Deny
Deny multiple refund requests in one call:
const items = [
{ paymentInfo: paymentInfo4, nonce: 0n },
{ paymentInfo: paymentInfo5, nonce: 0n },
];
const results = await arbiter.batchDeny(items);
for (const { txHash } of results) {
console.log('Denied:', txHash);
}
Empty Batch Handling
Both batch methods safely handle empty arrays and return an empty results array:
const results = await arbiter.batchApprove([]);
console.log(results.length); // 0
Each item in the batch array must include both the paymentInfo struct and the nonce:
interface BatchItem {
/** The full PaymentInfo struct identifying the payment */
paymentInfo: PaymentInfo;
/** The record index (nonce) from PaymentIndexRecorder */
nonce: bigint;
}
The nonce identifies which specific charge record the refund request targets. For most single-charge payments, this is 0n.
Example: Triage and Batch Process Pending Cases
Fetch all pending cases, evaluate each one, then batch approve and deny:
import { X402rArbiter } from '@x402r/arbiter';
import { RequestStatus } from '@x402r/core';
import type { PaymentInfo, RefundRequestData } from '@x402r/core';
async function triageAndProcess(
arbiter: X402rArbiter,
receiverAddress: `0x${string}`,
lookupPaymentInfo: (hash: `0x${string}`) => Promise<PaymentInfo>
) {
// Step 1: Fetch pending cases
const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress);
console.log(`Processing ${total} pending cases`);
const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
// Step 2: Evaluate each case
for (const key of keys) {
const request = await arbiter.getRefundRequestByKey(key);
if (request.status !== RequestStatus.Pending) {
continue;
}
const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash);
const item = { paymentInfo, nonce: request.nonce };
if (shouldApprove(request)) {
toApprove.push(item);
} else {
toDeny.push(item);
}
}
// Step 3: Batch process decisions
const approveResults = await arbiter.batchApprove(toApprove);
const denyResults = await arbiter.batchDeny(toDeny);
console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`);
return { approved: approveResults, denied: denyResults };
}
function shouldApprove(request: RefundRequestData): boolean {
// Your decision logic here
return request.amount < BigInt('10000000'); // Auto-approve < 10 USDC
}
Example: Batch Approve with Refund Execution
After batch approving, execute refunds individually for each approved payment:
import { X402rArbiter } from '@x402r/arbiter';
import type { PaymentInfo } from '@x402r/core';
async function batchApproveAndExecute(
arbiter: X402rArbiter,
items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }>
) {
// Step 1: Batch approve all items
const approveResults = await arbiter.batchApprove(items);
console.log(`Approved ${approveResults.length} refund requests`);
// Step 2: Execute refunds individually
const executeResults: Array<{ txHash: `0x${string}` }> = [];
for (const { paymentInfo } of items) {
try {
const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo);
executeResults.push({ txHash });
console.log('Refund executed:', txHash);
} catch (error) {
console.error(`Failed to execute refund for ${paymentInfo.payer}:`, error);
}
}
return {
approved: approveResults,
executed: executeResults,
};
}
Example: Scheduled Batch Processing
Run batch processing on a recurring schedule:
import { X402rArbiter } from '@x402r/arbiter';
import { RequestStatus } from '@x402r/core';
import type { PaymentInfo } from '@x402r/core';
class BatchProcessor {
private arbiter: X402rArbiter;
private receiverAddress: `0x${string}`;
private intervalId?: NodeJS.Timeout;
private lookupPaymentInfo: (hash: `0x${string}`) => Promise<PaymentInfo>;
constructor(
arbiter: X402rArbiter,
receiverAddress: `0x${string}`,
lookupPaymentInfo: (hash: `0x${string}`) => Promise<PaymentInfo>
) {
this.arbiter = arbiter;
this.receiverAddress = receiverAddress;
this.lookupPaymentInfo = lookupPaymentInfo;
}
start(intervalMs: number = 60_000) {
this.intervalId = setInterval(() => this.processBatch(), intervalMs);
console.log(`Batch processor started (interval: ${intervalMs}ms)`);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
console.log('Batch processor stopped');
}
}
private async processBatch() {
try {
const { keys, total } = await this.arbiter.getPendingRefundRequests(
0n, 50n, this.receiverAddress
);
if (keys.length === 0) {
return;
}
console.log(`Processing ${keys.length} of ${total} pending cases`);
const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
for (const key of keys) {
const request = await this.arbiter.getRefundRequestByKey(key);
if (request.status !== RequestStatus.Pending) continue;
const paymentInfo = await this.lookupPaymentInfo(request.paymentInfoHash);
const item = { paymentInfo, nonce: request.nonce };
if (this.shouldApprove(request.amount)) {
toApprove.push(item);
} else {
toDeny.push(item);
}
}
if (toApprove.length > 0) {
await this.arbiter.batchApprove(toApprove);
console.log(`Batch approved: ${toApprove.length}`);
}
if (toDeny.length > 0) {
await this.arbiter.batchDeny(toDeny);
console.log(`Batch denied: ${toDeny.length}`);
}
} catch (error) {
console.error('Batch processing error:', error);
}
}
private shouldApprove(amount: bigint): boolean {
return amount < BigInt('10000000'); // Auto-approve < 10 USDC
}
}
// Usage
const processor = new BatchProcessor(arbiter, '0xReceiver...', lookupPaymentInfo);
processor.start(60_000); // Process every minute
// Graceful shutdown
process.on('SIGINT', () => {
processor.stop();
process.exit();
});
Each item in a batch results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider’s rate limits.
| Factor | Detail |
|---|
| Transaction ordering | Items are processed sequentially to ensure correct nonce ordering. |
| Gas costs | Each item is a separate transaction. Batch methods save on SDK overhead, not gas. |
| Partial failures | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. |
| Rate limiting | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. |
Next Steps