|
| 1 | +--- |
| 2 | +title: "Delivery Protection Arbiter" |
| 3 | +description: "Automatically evaluate every transaction and release or let funds expire." |
| 4 | +icon: "shield-check" |
| 5 | +--- |
| 6 | + |
| 7 | +In this model, the arbiter is the only address that can release funds. The merchant sets up a `forwardToArbiter()` hook that sends the HTTP response body to your arbiter service after every payment. Your service evaluates the response (AI, schema validation, heuristics) and calls `release()` if it passes. If you do not release, funds auto-refund to the payer after escrow expires. |
| 8 | + |
| 9 | +### Prerequisites |
| 10 | + |
| 11 | +* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia)) |
| 12 | +* Node.js 18+ and npm |
| 13 | + |
| 14 | +<Note> |
| 15 | +There is a full [AI garbage detector example](https://github.com/BackTrackCo/arbiter-examples) that implements this pattern with heuristic + LLM evaluation. |
| 16 | +</Note> |
| 17 | + |
| 18 | +### 1. Install Dependencies |
| 19 | + |
| 20 | +<CodeGroup> |
| 21 | +```bash npm |
| 22 | +npm install @x402r/sdk @x402r/core |
| 23 | +``` |
| 24 | +```bash pnpm |
| 25 | +pnpm add @x402r/sdk @x402r/core |
| 26 | +``` |
| 27 | +```bash bun |
| 28 | +bun add @x402r/sdk @x402r/core |
| 29 | +``` |
| 30 | +</CodeGroup> |
| 31 | + |
| 32 | +`@x402r/core` is needed for `deployDeliveryProtectionOperator()`. |
| 33 | + |
| 34 | +### 2. Deploy a Delivery Protection Operator |
| 35 | + |
| 36 | +```typescript |
| 37 | +import { deployDeliveryProtectionOperator } from '@x402r/core' |
| 38 | + |
| 39 | +const deployment = await deployDeliveryProtectionOperator( |
| 40 | + walletClient, |
| 41 | + publicClient, |
| 42 | + { |
| 43 | + chainId: 84532, |
| 44 | + arbiter: account.address, // your arbiter service wallet |
| 45 | + feeRecipient: account.address, // receives protocol fees |
| 46 | + escrowPeriodSeconds: 300n, // 5 minutes (short for automated flows) |
| 47 | + }, |
| 48 | +) |
| 49 | + |
| 50 | +console.log('Operator:', deployment.operatorAddress) |
| 51 | +console.log('EscrowPeriod:', deployment.escrowPeriodAddress) |
| 52 | +console.log('ArbiterCondition:', deployment.arbiterConditionAddress) |
| 53 | +``` |
| 54 | + |
| 55 | +This is simpler than the marketplace preset. No RefundRequest, Evidence, or Freeze contracts. The operator's `releaseCondition` is set to `StaticAddressCondition(arbiter)`, so only your arbiter address can call `release()`. |
| 56 | + |
| 57 | +### 3. Merchant Setup: Forward to Arbiter |
| 58 | + |
| 59 | +The merchant configures their x402 resource server to forward response data to your arbiter after every payment settlement: |
| 60 | + |
| 61 | +```typescript |
| 62 | +import { forwardToArbiter } from '@x402r/helpers' |
| 63 | + |
| 64 | +const resourceServer = new x402ResourceServer(facilitatorClient) |
| 65 | + .register(networkId, new CommerceEvmScheme(serverConfig)) |
| 66 | + .onAfterSettle( |
| 67 | + forwardToArbiter('http://your-arbiter:3001', { |
| 68 | + onError: (err) => console.error('Arbiter unreachable:', err), |
| 69 | + }) |
| 70 | + ) |
| 71 | +``` |
| 72 | + |
| 73 | +`forwardToArbiter()` POSTs to `{arbiterUrl}/verify` with: |
| 74 | + |
| 75 | +```json |
| 76 | +{ |
| 77 | + "responseBody": "the HTTP response body as a string", |
| 78 | + "transaction": "0xsettlement_tx_hash", |
| 79 | + "paymentPayload": { "x402Version": 1, "scheme": "commerce", "payload": "..." } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Fire-and-forget. Does not block the response to the client. Only fires for `commerce` scheme settlements. |
| 84 | + |
| 85 | +### 4. Build the Arbiter Service |
| 86 | + |
| 87 | +Your arbiter service receives the POST, evaluates the response, and calls `release()` if it passes: |
| 88 | + |
| 89 | +```typescript |
| 90 | +import { createPublicClient, createWalletClient, http } from 'viem' |
| 91 | +import { baseSepolia } from 'viem/chains' |
| 92 | +import { privateKeyToAccount } from 'viem/accounts' |
| 93 | +import { createX402r } from '@x402r/sdk' |
| 94 | + |
| 95 | +const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`) |
| 96 | + |
| 97 | +const arbiter = createX402r({ |
| 98 | + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), |
| 99 | + walletClient: createWalletClient({ |
| 100 | + account, |
| 101 | + chain: baseSepolia, |
| 102 | + transport: http(), |
| 103 | + }), |
| 104 | + operatorAddress: deployment.operatorAddress, |
| 105 | + escrowPeriodAddress: deployment.escrowPeriodAddress, |
| 106 | +}) |
| 107 | + |
| 108 | +// Express route handling the POST from forwardToArbiter() |
| 109 | +app.post('/verify', async (req, res) => { |
| 110 | + const { responseBody, transaction, paymentPayload } = req.body |
| 111 | + |
| 112 | + // Your evaluation logic here |
| 113 | + const passed = await evaluate(responseBody) |
| 114 | + |
| 115 | + if (passed) { |
| 116 | + // Extract paymentInfo from paymentPayload |
| 117 | + const paymentInfo = extractPaymentInfo(paymentPayload) |
| 118 | + const amounts = await arbiter.payment.getAmounts(paymentInfo) |
| 119 | + |
| 120 | + await arbiter.payment.release(paymentInfo, amounts.capturableAmount) |
| 121 | + res.json({ verdict: 'PASS' }) |
| 122 | + } else { |
| 123 | + // Do nothing. Funds auto-refund after escrow expires. |
| 124 | + res.json({ verdict: 'FAIL' }) |
| 125 | + } |
| 126 | +}) |
| 127 | +``` |
| 128 | + |
| 129 | +The `evaluate()` function is where your logic lives. It could be: |
| 130 | +- **Heuristic checks:** HTTP status code, response size, content-type validation |
| 131 | +- **AI evaluation:** send response body to an LLM and ask "is this a valid response?" |
| 132 | +- **Schema validation:** check if the response matches an expected JSON schema |
| 133 | + |
| 134 | +### 5. What Happens on Failure |
| 135 | + |
| 136 | +If the arbiter does not call `release()`, the payer can reclaim funds after the escrow period expires by calling `refundInEscrow()`. The escrow period acts as the verification window. |
| 137 | + |
| 138 | +```typescript |
| 139 | +// Anyone can call this after escrow expires (gated by EscrowPeriod condition) |
| 140 | +await payer.payment.refundInEscrow(paymentInfo, amount) |
| 141 | +``` |
| 142 | + |
| 143 | +No dispute process needed. The timeout is the safety net. |
| 144 | + |
| 145 | +## Next Steps |
| 146 | + |
| 147 | +<CardGroup cols={3}> |
| 148 | + <Card title="Dispute Resolution" icon="scale-balanced" href="/sdk/arbiter-dispute"> |
| 149 | + For human-reviewed disputes instead of automated evaluation. |
| 150 | + </Card> |
| 151 | + <Card title="Deploy Operator" icon="rocket" href="/sdk/deploy-operator"> |
| 152 | + Full deployment config and condition slot details. |
| 153 | + </Card> |
| 154 | + <Card title="Examples" icon="code" href="/sdk/examples"> |
| 155 | + Runnable examples for every SDK operation. |
| 156 | + </Card> |
| 157 | +</CardGroup> |
0 commit comments