Skip to main content

Prerequisites

  • A wallet with ETH on Base Sepolia for gas (faucet)
  • Node.js 18+ and npm
  • An authorized payment on an x402r operator (see Merchant Guide)
There are pre-configured examples in the x402r-sdk repo, including payer examples and a full dispute resolution scenario.

1. Install Dependencies

npm install @x402r/sdk

2. Create a Payer Client

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

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

const payer = createPayerClient({
  publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
  walletClient: createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  }),
  operatorAddress: '0x...',           // your operator address
  refundRequestAddress: '0x...',      // from deploy result
  refundRequestEvidenceAddress: '0x...',
  escrowPeriodAddress: '0x...',
  freezeAddress: '0x...',
})
All payer actions (refund, freeze, evidence) must happen during the escrow period. Once escrow expires, the merchant can release.

3. Check Payment State

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

// paymentInfo is the same struct used during authorization
const paymentInfo: PaymentInfo = { /* ... */ }

const amounts = await payer.payment.getAmounts(paymentInfo)
console.log('Collected:', amounts.hasCollectedPayment)
console.log('Capturable:', amounts.capturableAmount)
console.log('Refundable:', amounts.refundableAmount)

const inEscrow = await payer.escrow?.isDuringEscrow(paymentInfo)
console.log('In escrow:', inEscrow)

4. Request a Refund

Request a refund while the payment is still in escrow:
// Check if a refund request already exists
const hasExisting = await payer.refund?.has(paymentInfo)
if (hasExisting) {
  console.log('Refund already requested')
} else {
  const tx = await payer.refund?.request(paymentInfo, 1_000_000n) // 1 USDC
  console.log('Refund requested:', tx)
}

// Check refund status
const status = await payer.refund?.getStatus(paymentInfo)
console.log('Status:', status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused

5. Freeze a Payment (Optional)

freeze() blocks the merchant from releasing until the arbiter unfreezes. Only use when you need to prevent a release during investigation.
const frozen = await payer.freeze?.isFrozen(paymentInfo)
if (!frozen) {
  const tx = await payer.freeze?.freeze(paymentInfo)
  console.log('Payment frozen:', tx)
}

6. Submit Evidence (Optional)

Attach evidence to a refund request. Evidence is stored on-chain as IPFS CIDs:
// Upload your evidence to IPFS first, then submit the CID
const tx = await payer.evidence?.submit(paymentInfo, 'QmYourEvidenceCID...')
console.log('Evidence submitted:', tx)

// Read back evidence
const count = await payer.evidence?.count(paymentInfo)
console.log('Evidence entries:', count)

for (let i = 0n; i < count!; i++) {
  const entry = await payer.evidence?.get(paymentInfo, i)
  console.log(`  [${i}] CID: ${entry?.cid} from ${entry?.submitter}`)
}

7. Cancel a Refund Request (Optional)

If the issue is resolved directly with the merchant:
const cancelTx = await payer.refund?.cancel(paymentInfo)
console.log('Refund cancelled:', cancelTx)

Next Steps

Arbiter Guide

What happens after you submit a refund request.

Protocol Overview

Escrow timing, capture, and the payment lifecycle.

Examples

Full dispute resolution scenario end-to-end.