Skip to main content
The @x402r/client package provides everything payers need to interact with X402r payments: requesting refunds, freezing payments, and managing escrow.

Prerequisites

Before starting, ensure you have:
  • Node.js 20+
  • A funded wallet on Base Sepolia
  • Basic understanding of viem

Installation

npm install @x402r/client @x402r/core viem

Setup

Create the X402rClient instance:
import { createPublicClient, createWalletClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { X402rClient } from '@x402r/client';
import { getNetworkConfig } from '@x402r/core';

const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: http(),
});

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
  account,
  chain: baseSepolia,
  transport: http(),
});

const networkConfig = getNetworkConfig('eip155:84532')!;

const client = new X402rClient({
  publicClient,
  walletClient,
  operatorAddress: '0x...', // Your PaymentOperator address
  refundRequestAddress: networkConfig.refundRequest,
  escrowAddress: networkConfig.authCaptureEscrow,
});

Request a Refund

Submit a refund request for a payment that is in escrow:
// Request refund for the first charge (nonce = 0n)
const { txHash } = await client.requestRefund(
  paymentInfo,
  BigInt('1000000'), // amount to refund (e.g., 1 USDC)
  0n                 // nonce: identifies which charge
);
console.log(`Refund requested: ${txHash}`);

// Check refund status
const status = await client.getRefundStatus(paymentInfo, 0n);
console.log(`Refund status: ${status}`);
// 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled

Check Escrow Period

Query whether the payment is still within its escrow period:
const escrowPeriodAddress = '0x...'; // EscrowPeriod contract address

// Check if still within escrow period
const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress);
if (inEscrow) {
  console.log('Still in escrow period - can request refund');
} else {
  console.log('Escrow period passed - funds can be released');
}

// Get when the payment was authorized
const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress);
console.log(`Authorized at: ${new Date(Number(authTime) * 1000)}`);

Freeze a Payment

Freeze a payment to prevent release while a dispute is resolved:
const freezeAddress = '0x...'; // Freeze contract address

// Freeze the payment (only payer can do this)
const { txHash } = await client.freezePayment(paymentInfo, freezeAddress);
console.log(`Payment frozen: ${txHash}`);

// Check if payment is frozen
const frozen = await client.isFrozen(paymentInfo, freezeAddress);
console.log(`Frozen: ${frozen}`);

Watch for Updates

Subscribe to real-time events:
// Watch payment state changes (releases, refunds)
const { unsubscribe } = client.watchPaymentState(
  paymentInfoHash,
  (event) => {
    console.log('Payment event:', event.eventName);
  }
);

// Watch for refund request updates
const { unsubscribe: unsubRefunds } = client.watchRefundRequests((event) => {
  console.log('Refund event:', event.eventName);
});

// Later: stop watching
unsubscribe();
unsubRefunds();

Complete Example

import { createPublicClient, createWalletClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { X402rClient } from '@x402r/client';
import { getNetworkConfig, RequestStatus } from '@x402r/core';

async function main() {
  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(),
  });

  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
  const walletClient = createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  });

  const config = getNetworkConfig('eip155:84532')!;

  const client = new X402rClient({
    publicClient,
    walletClient,
    operatorAddress: '0x...',
    refundRequestAddress: config.refundRequest,
  });

  // Check if a refund request already exists
  const hasRequest = await client.hasRefundRequest(paymentInfo, 0n);

  if (!hasRequest) {
    // Request a refund
    const { txHash } = await client.requestRefund(
      paymentInfo,
      paymentInfo.maxAmount, // full refund
      0n
    );
    console.log(`Refund requested: ${txHash}`);
  }

  // Check status
  const status = await client.getRefundStatus(paymentInfo, 0n);
  if (status === RequestStatus.Approved) {
    console.log('Refund was approved!');
  }

  // List my refund requests (paginated)
  const { keys, total } = await client.getMyRefundRequests(0n, 10n);
  console.log(`${total} total refund requests`);

  for (const key of keys) {
    const request = await client.getRefundRequestByKey(key);
    console.log(`Amount: ${request.amount}, Status: ${request.status}`);
  }
}

main().catch(console.error);

Next Steps