Skip to main content
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

Item Format

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();
});

Performance Considerations

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.
FactorDetail
Transaction orderingItems are processed sequentially to ensure correct nonce ordering.
Gas costsEach item is a separate transaction. Batch methods save on SDK overhead, not gas.
Partial failuresIf one transaction fails, previous ones remain on-chain. Handle partial failures in your logic.
Rate limitingLarge batches may hit RPC rate limits. Consider adding delays for 50+ item batches.

Next Steps