Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 12 additions & 26 deletions sdk/arbiter/ai-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ interface CaseEvaluationContext {
/** The payment information struct */
paymentInfo: PaymentInfo;

/** The record index (nonce) identifying which charge */
nonce: bigint;

/** Current payment state */
paymentState: PaymentState;

Expand Down Expand Up @@ -89,15 +86,15 @@ const handler = createWebhookHandler({
```

<Note>
Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRequest` on-chain automatically. This submits the decision only -- executing the actual refund transfer via `executeRefundInEscrow` is a separate step you handle after approval.
Setting `autoSubmitDecision: true` calls `refundInEscrow` (for approvals) or `denyRefundRequest` on-chain automatically. In v3, `refundInEscrow()` both executes the refund and auto-approves the pending request in one step.
</Note>

## Webhook Handler Configuration

```typescript
interface WebhookHandlerConfig {
/** X402rArbiter instance */
arbiter: X402rArbiter;
/** Arbiter client instance */
arbiter: ReturnType<typeof createArbiterClient>;

/** Your evaluation function */
evaluationHook: ArbiterHook;
Expand Down Expand Up @@ -127,8 +124,7 @@ interface WebhookResult extends DecisionResult {
The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests:

```typescript
import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter';
import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter';
import { createArbiterClient } from '@x402r/sdk';
import { PaymentState, RequestStatus } from '@x402r/core';
import type { PaymentInfo } from '@x402r/core';

Expand All @@ -143,23 +139,21 @@ const handler = createWebhookHandler({
});

// Step 2: Watch for new cases and feed them to the handler
const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLog) => {
const paymentInfoHash = event.args.paymentInfoHash!;
const nonce = event.args.nonce ?? 0n;
const unsubscribe = arbiter.watch.onRefundRequest(async (event: any) => {
const paymentInfoHash = event.args?.paymentInfoHash;
if (!paymentInfoHash) return;

console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`);
console.log(`[NEW CASE] ${paymentInfoHash}`);

// Build the evaluation context
// NOTE: You need to reconstruct the full PaymentInfo from your database or event logs
const paymentInfo = await lookupPaymentInfo(paymentInfoHash);
// Retrieve stored PaymentInfo from RefundRequest contract
const paymentInfo = await arbiter.refund.getStoredPaymentInfo(paymentInfoHash);

const context: CaseEvaluationContext = {
paymentInfo,
nonce,
paymentState: PaymentState.InEscrow,
refundStatus: RequestStatus.Pending,
paymentInfoHash,
refundAmount: event.args.amount,
refundAmount: event.args?.amount,
};

// Evaluate and optionally auto-submit
Expand All @@ -170,15 +164,7 @@ const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLo

if (result.executed) {
console.log(`[ON-CHAIN] Decision submitted: ${result.txHash}`);

// If approved, execute the refund transfer
if (result.decision === 'approve') {
const { txHash } = await arbiter.executeRefundInEscrow(
paymentInfo,
result.refundAmount // partial refund if specified
);
console.log(`[REFUND] Executed: ${txHash}`);
}
// In v3, refundInEscrow already executes the refund and auto-approves
} else {
console.log('[SKIPPED] Confidence below threshold, requires manual review');
}
Expand Down
156 changes: 45 additions & 111 deletions sdk/arbiter/batch-operations.mdx
Original file line number Diff line number Diff line change
@@ -1,122 +1,92 @@
---
title: "Batch Operations"
title: "Batch operations"
description: "Process multiple refund decisions efficiently with the Arbiter SDK"
icon: "layer-group"
---

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.
You can process multiple refund decisions by iterating over requests and calling individual methods. In v3, there is no separate approve step — calling `payment.refundInEscrow()` executes the refund and auto-approves the pending request in one transaction.

<Warning>
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.
Each decision is a separate on-chain transaction. If one fails mid-batch, previously processed items will **not** be rolled back. Design your error handling accordingly.
</Warning>

## Batch Approve
## Batch refund

Approve multiple refund requests in one call:
Execute refunds for multiple pending requests:

```typescript
const items = [
{ paymentInfo: paymentInfo1, nonce: 0n },
{ paymentInfo: paymentInfo2, nonce: 0n },
{ paymentInfo: paymentInfo3, nonce: 1n },
];
const items = [paymentInfo1, paymentInfo2, paymentInfo3];

const results = await arbiter.batchApprove(items);

for (const { txHash } of results) {
console.log('Approved:', txHash);
for (const paymentInfo of items) {
const request = await arbiter.refund.get(paymentInfo);
const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount);
console.log('Refund executed:', txHash);
}
```

## Batch Deny
## Batch deny

Deny multiple refund requests in one call:
Deny multiple refund requests:

```typescript
const items = [
{ paymentInfo: paymentInfo4, nonce: 0n },
{ paymentInfo: paymentInfo5, nonce: 0n },
];

const results = await arbiter.batchDeny(items);
const items = [paymentInfo4, paymentInfo5];

for (const { txHash } of results) {
for (const paymentInfo of items) {
const txHash = await arbiter.refund.deny(paymentInfo);
console.log('Denied:', txHash);
}
```

## Empty Batch Handling

Both batch methods safely handle empty arrays and return an empty results array:

```typescript
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`:

```typescript
interface BatchItem {
/** The full PaymentInfo struct identifying the payment */
paymentInfo: PaymentInfo;
/** The record index (nonce) from PaymentIndexRecorder */
nonce: bigint;
}
```

<Info>
The `nonce` identifies which specific charge record the refund request targets. For most single-charge payments, this is `0n`.
</Info>

## Example: Triage and Batch Process Pending Cases
## Example: triage and batch process pending cases

Fetch all pending cases, evaluate each one, then batch approve and deny:
Fetch all pending cases, evaluate each one, then process:

```typescript
import { X402rArbiter } from '@x402r/arbiter';
import { createArbiterClient } from '@x402r/sdk';
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>
arbiter: ReturnType<typeof createArbiterClient>,
operatorAddress: `0x${string}`
) {
// Step 1: Fetch pending cases
const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress);
console.log(`Processing ${total} pending cases`);
// Step 1: Fetch cases for this operator
const { keys, total } = await arbiter.refund.getOperatorRequests(
operatorAddress, 0n, 100n
);
console.log(`Processing ${total} cases`);

const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
const toRefund: PaymentInfo[] = [];
const toDeny: PaymentInfo[] = [];

// Step 2: Evaluate each case
for (const key of keys) {
const request = await arbiter.getRefundRequestByKey(key);
const request = await arbiter.refund.getByKey(key);

if (request.status !== RequestStatus.Pending) {
continue;
}

const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash);
const item = { paymentInfo, nonce: request.nonce };
const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash);

if (shouldApprove(request)) {
toApprove.push(item);
toRefund.push(paymentInfo);
} else {
toDeny.push(item);
toDeny.push(paymentInfo);
}
}

// Step 3: Batch process decisions
const approveResults = await arbiter.batchApprove(toApprove);
const denyResults = await arbiter.batchDeny(toDeny);
// Step 3: Process decisions
for (const paymentInfo of toRefund) {
const request = await arbiter.refund.get(paymentInfo);
await arbiter.payment.refundInEscrow(paymentInfo, request.amount);
}

console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`);
for (const paymentInfo of toDeny) {
await arbiter.refund.deny(paymentInfo);
}

return { approved: approveResults, denied: denyResults };
console.log(`Refunded: ${toRefund.length}, Denied: ${toDeny.length}`);
}

function shouldApprove(request: RefundRequestData): boolean {
Expand All @@ -125,56 +95,20 @@ function shouldApprove(request: RefundRequestData): boolean {
}
```

## Example: Batch Approve with Refund Execution

After batch approving, execute refunds individually for each approved payment:

```typescript
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,
};
}
```

## Performance Considerations
## Performance considerations

<Note>
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.
Each item 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.
</Note>

| 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. |
| **Gas costs** | Each item is a separate transaction. |
| **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
## Next steps

<CardGroup cols={2}>
<Card title="AI Integration" icon="robot" href="/sdk/arbiter/ai-integration">
Expand All @@ -184,7 +118,7 @@ Each item in a batch results in a separate on-chain transaction. Gas costs scale
Watch for new cases in real-time.
</Card>
<Card title="Decision Submission" icon="gavel" href="/sdk/arbiter/decision-submission">
Individual approve/deny methods and executeRefundInEscrow.
Individual deny methods and refundInEscrow.
</Card>
<Card title="Arbiter Quickstart" icon="rocket" href="/sdk/arbiter/quickstart">
Review the complete arbiter setup guide.
Expand Down
Loading
Loading