Skip to main content

Prerequisites

  • A wallet with ETH on Base Sepolia for gas (faucet)
  • Node.js 18+ and npm
  • A marketplace operator where your address is configured as the arbiter (see Deploy an Operator)
There are pre-configured arbiter examples and a full dispute resolution scenario in the SDK repo.

1. Install Dependencies

npm install @x402r/sdk

2. Create an Arbiter Client

import { createPublicClient, createWalletClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { createArbiterClient } from '@x402r/sdk'

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

const arbiter = createArbiterClient({
  publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
  walletClient: createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  }),
  operatorAddress: '0x...',              // from deploy result
  refundRequestAddress: '0x...',         // from deploy result
  refundRequestEvidenceAddress: '0x...', // from deploy result
  escrowPeriodAddress: '0x...',
  freezeAddress: '0x...',
})

3. Check for Pending Refund Requests

import type { PaymentInfo } from '@x402r/sdk'

const paymentInfo: PaymentInfo = { /* ... */ }

const hasRefund = await arbiter.refund?.has(paymentInfo)
if (hasRefund) {
  const request = await arbiter.refund?.get(paymentInfo)
  console.log('Amount:', request?.amount)
  console.log('Status:', request?.status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused
}
To list all refund requests for your operator:
const requests = await arbiter.refund?.getOperatorRequests(
  arbiter.config.operatorAddress,
  0n,   // offset
  10n,  // count
)
console.log('Pending requests:', requests)

4. Review Evidence

Both payers and merchants can submit evidence as IPFS CIDs. Read all entries before making a decision:
const count = await arbiter.evidence?.count(paymentInfo)
console.log('Evidence entries:', count)

const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, count!)

for (const entry of batch!.entries) {
  console.log('CID:', entry.cid)
  console.log('Submitter:', entry.submitter)
  console.log('Timestamp:', entry.timestamp)
}

5. Approve a Refund

refundInEscrow() auto-approves the pending RefundRequest. There is no undo.
const tx = await arbiter.payment.refundInEscrow(paymentInfo, request!.amount)
console.log('Refund approved:', tx)

// Verify
const approved = await arbiter.refund?.get(paymentInfo)
console.log('Approved amount:', approved?.approvedAmount)
console.log('Status:', approved?.status) // 1 = Approved

6. Deny a Refund Request

deny() = you reviewed and rejected the claim. refuse() = you decline to rule (e.g. conflict of interest).
const denyTx = await arbiter.refund?.deny(paymentInfo)
console.log('Refund denied:', denyTx)
Or decline to rule entirely:
const refuseTx = await arbiter.refund?.refuse(paymentInfo)
console.log('Declined to rule:', refuseTx)

7. Unfreeze a Payment

If the payer froze the payment during the dispute, unfreeze it after resolution:
const frozen = await arbiter.freeze?.isFrozen(paymentInfo)
if (frozen) {
  const tx = await arbiter.freeze?.unfreeze(paymentInfo)
  console.log('Unfrozen:', tx)
}

8. Distribute Accumulated Fees

Protocol fees accumulate on the operator when payments are released:
import { getChainConfig } from '@x402r/sdk'

const config = getChainConfig(84532)

const fees = await arbiter.operator.getAccumulatedProtocolFees(config.usdc)
console.log('Accumulated fees:', fees)

if (fees > 0n) {
  const tx = await arbiter.operator.distributeFees(config.usdc)
  console.log('Fees distributed:', tx)
}

Next Steps

Delivery Protection

Automated evaluation for every transaction.

Deploy Operator

How your address gets configured as arbiter on an operator.

Examples

Full dispute resolution scenario end-to-end.