Skip to main content
The @x402r/merchant package provides everything merchants need to handle X402r payments: releasing funds, charging, processing refunds, and managing escrow.

Prerequisites

Before starting, ensure you have:
  • Node.js 20+
  • A funded wallet on Base Sepolia
  • A deployed PaymentOperator (see Deploy Operator)

Installation

npm install @x402r/merchant @x402r/helpers @x402r/core viem

Setup

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

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

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

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

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

Release a Payment

Release funds from escrow to the receiver:
// Release a specific amount
const { txHash } = await merchant.release(paymentInfo, BigInt('1000000'));
console.log('Payment released:', txHash);

Charge a Payment

For subscription or session-based flows, charge directly without prior escrow:
const { txHash } = await merchant.charge(
  paymentInfo,
  BigInt('500000'),         // amount to charge
  tokenCollectorAddress,    // ERC3009 token collector
  '0x...'                   // collector data (e.g., signature)
);
console.log('Payment charged:', txHash);

Handle Refund Requests

Process incoming refund requests:
import { RequestStatus } from '@x402r/core';

// Get pending refund requests (paginated)
const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n);
console.log(`${total} pending refund requests`);

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

// Approve a refund
const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, 0n);
console.log('Refund approved:', approveTx);

// Or deny a refund
const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, 0n);
console.log('Refund denied:', denyTx);

Execute Refunds

After approving, execute the actual refund:
// Refund while funds are still in escrow
const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('1000000'));
console.log('In-escrow refund:', txHash);

// Refund after funds have been released (post-escrow)
const { txHash: postTx } = await merchant.refundPostEscrow(
  paymentInfo,
  BigInt('500000'),
  tokenCollectorAddress,
  '0x...' // collector data
);
console.log('Post-escrow refund:', postTx);

Mark Payment Options as Refundable

Use the @x402r/helpers package to add escrow configuration to x402 payment options:
import { refundable } from '@x402r/helpers';

const option = refundable(
  {
    scheme: 'escrow',
    network: 'eip155:84532',
    payTo: '0xMerchant...',
    price: '$0.01',
  },
  '0xOperatorAddress...',
  { maxFeeBps: 500 } // optional: accept up to 5% fee
);

Watch for Events

Subscribe to incoming refund requests and releases:
// Watch for refund requests
const { unsubscribe } = merchant.watchRefundRequests((event) => {
  console.log('New refund request:', event);
});

// Watch for releases
const { unsubscribe: unsubReleases } = merchant.watchReleases((event) => {
  console.log('Release executed:', event);
});

Complete Example

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

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

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

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

  const merchant = new X402rMerchant({
    publicClient,
    walletClient,
    operatorAddress: '0x...',
    escrowAddress: config.authCaptureEscrow,
    refundRequestAddress: config.refundRequest,
  });

  // Check payment amounts
  const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
  console.log(`Capturable: ${capturableAmount}, Refundable: ${refundableAmount}`);

  // Release if escrow period has passed
  if (capturableAmount > 0n) {
    const { txHash } = await merchant.release(paymentInfo, capturableAmount);
    console.log('Released:', txHash);
  }

  // Watch for new refund requests
  merchant.watchRefundRequests(async (event) => {
    console.log('New refund request:', event);
  });
}

main().catch(console.error);

Next Steps