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)
1. Install Dependencies
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.