Skip to content

Commit a4a6757

Browse files
vrasparclaude
andcommitted
Add two arbiter models: dispute resolution and delivery protection
- arbiter-quickstart: landing page explaining both models with comparison table, links to the two tutorials - arbiter-dispute: existing dispute flow (moved from arbiter-quickstart) - arbiter-delivery: new page for automated evaluation flow using deployDeliveryProtectionOperator() and forwardToArbiter() - overview: add "Two Operator Models" section - docs.json: add arbiter-dispute and arbiter-delivery to nav Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 05840cb commit a4a6757

5 files changed

Lines changed: 361 additions & 157 deletions

File tree

docs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@
9292
"sdk/overview",
9393
"sdk/merchant-quickstart",
9494
"sdk/payer-quickstart",
95-
"sdk/arbiter-quickstart"
95+
"sdk/arbiter-quickstart",
96+
"sdk/arbiter-dispute",
97+
"sdk/arbiter-delivery"
9698
]
9799
},
98100
{

sdk/arbiter-delivery.mdx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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>

sdk/arbiter-dispute.mdx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: "Dispute Resolution Arbiter"
3+
description: "Review refund requests, approve or deny refunds, and distribute fees."
4+
icon: "scale-balanced"
5+
---
6+
7+
### Prerequisites
8+
9+
* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia))
10+
* Node.js 18+ and npm
11+
* A marketplace operator where your address is configured as the arbiter (see [Deploy an Operator](/sdk/deploy-operator))
12+
13+
<Note>
14+
There are pre-configured [arbiter examples](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/arbiter) and a full [dispute resolution scenario](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/scenarios/dispute-resolution.ts) in the SDK repo.
15+
</Note>
16+
17+
### 1. Install Dependencies
18+
19+
<CodeGroup>
20+
```bash npm
21+
npm install @x402r/sdk
22+
```
23+
```bash pnpm
24+
pnpm add @x402r/sdk
25+
```
26+
```bash bun
27+
bun add @x402r/sdk
28+
```
29+
</CodeGroup>
30+
31+
### 2. Create an Arbiter Client
32+
33+
```typescript
34+
import { createPublicClient, createWalletClient, http } from 'viem'
35+
import { baseSepolia } from 'viem/chains'
36+
import { privateKeyToAccount } from 'viem/accounts'
37+
import { createArbiterClient } from '@x402r/sdk'
38+
39+
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
40+
41+
const arbiter = createArbiterClient({
42+
publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
43+
walletClient: createWalletClient({
44+
account,
45+
chain: baseSepolia,
46+
transport: http(),
47+
}),
48+
operatorAddress: '0x...', // from deploy result
49+
refundRequestAddress: '0x...', // from deploy result
50+
refundRequestEvidenceAddress: '0x...', // from deploy result
51+
escrowPeriodAddress: '0x...',
52+
freezeAddress: '0x...',
53+
})
54+
```
55+
56+
### 3. Check for Pending Refund Requests
57+
58+
```typescript
59+
import type { PaymentInfo } from '@x402r/sdk'
60+
61+
const paymentInfo: PaymentInfo = { /* ... */ }
62+
63+
const hasRefund = await arbiter.refund?.has(paymentInfo)
64+
if (hasRefund) {
65+
const request = await arbiter.refund?.get(paymentInfo)
66+
console.log('Amount:', request?.amount)
67+
console.log('Status:', request?.status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Refused
68+
}
69+
```
70+
71+
To list all refund requests for your operator:
72+
73+
```typescript
74+
const requests = await arbiter.refund?.getOperatorRequests(
75+
arbiter.config.operatorAddress,
76+
0n, // offset
77+
10n, // count
78+
)
79+
console.log('Pending requests:', requests)
80+
```
81+
82+
### 4. Review Evidence
83+
84+
Both payers and merchants can submit evidence as IPFS CIDs. Read all entries before making a decision:
85+
86+
```typescript
87+
const count = await arbiter.evidence?.count(paymentInfo)
88+
console.log('Evidence entries:', count)
89+
90+
const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, count!)
91+
92+
for (const entry of batch!.entries) {
93+
console.log('CID:', entry.cid)
94+
console.log('Submitter:', entry.submitter)
95+
console.log('Timestamp:', entry.timestamp)
96+
}
97+
```
98+
99+
### 5. Approve a Refund
100+
101+
Call `payment.refundInEscrow()` to approve. The RefundRequest recorder auto-approves the pending request, so there is no separate `approve()` call:
102+
103+
```typescript
104+
const tx = await arbiter.payment.refundInEscrow(paymentInfo, request!.amount)
105+
console.log('Refund approved:', tx)
106+
107+
// Verify
108+
const approved = await arbiter.refund?.get(paymentInfo)
109+
console.log('Approved amount:', approved?.approvedAmount)
110+
console.log('Status:', approved?.status) // 1 = Approved
111+
```
112+
113+
### 6. Deny a Refund Request
114+
115+
If the evidence does not support a refund:
116+
117+
```typescript
118+
const denyTx = await arbiter.refund?.deny(paymentInfo)
119+
console.log('Refund denied:', denyTx)
120+
```
121+
122+
Or decline to rule entirely:
123+
124+
```typescript
125+
const refuseTx = await arbiter.refund?.refuse(paymentInfo)
126+
console.log('Declined to rule:', refuseTx)
127+
```
128+
129+
### 7. Unfreeze a Payment
130+
131+
If the payer froze the payment during the dispute, unfreeze it after resolution:
132+
133+
```typescript
134+
const frozen = await arbiter.freeze?.isFrozen(paymentInfo)
135+
if (frozen) {
136+
const tx = await arbiter.freeze?.unfreeze(paymentInfo)
137+
console.log('Unfrozen:', tx)
138+
}
139+
```
140+
141+
### 8. Distribute Accumulated Fees
142+
143+
Protocol fees accumulate on the operator when payments are released:
144+
145+
```typescript
146+
import { getChainConfig } from '@x402r/sdk'
147+
148+
const config = getChainConfig(84532)
149+
150+
const fees = await arbiter.operator.getAccumulatedProtocolFees(config.usdc)
151+
console.log('Accumulated fees:', fees)
152+
153+
if (fees > 0n) {
154+
const tx = await arbiter.operator.distributeFees(config.usdc)
155+
console.log('Fees distributed:', tx)
156+
}
157+
```
158+
159+
## Next Steps
160+
161+
<CardGroup cols={3}>
162+
<Card title="Delivery Protection" icon="shield-check" href="/sdk/arbiter-delivery">
163+
Automated evaluation for every transaction.
164+
</Card>
165+
<Card title="Deploy Operator" icon="rocket" href="/sdk/deploy-operator">
166+
How your address gets configured as arbiter on an operator.
167+
</Card>
168+
<Card title="Examples" icon="code" href="/sdk/examples">
169+
Full dispute resolution scenario end-to-end.
170+
</Card>
171+
</CardGroup>

0 commit comments

Comments
 (0)