Skip to main content
The X402rMerchant class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a nonce: bigint parameter that identifies which specific charge the refund targets.
The nonce parameter corresponds to the record index from the PaymentIndexRecorder. For the first charge against a payment, the nonce is 0n. Each subsequent charge increments the nonce.

Refund Request Queries

Check If a Refund Request Exists

Use hasRefundRequest() to check whether a payer has submitted a refund request for a specific payment and nonce.
const hasRequest = await merchant.hasRefundRequest(paymentInfo, 0n);

if (hasRequest) {
  console.log('Refund request exists for this payment');
} else {
  console.log('No refund request submitted');
}

Get Refund Request Status

Use getRefundStatus() to retrieve the current status of a refund request. Returns a RequestStatus enum value.
import { RequestStatus } from '@x402r/core';

const status = await merchant.getRefundStatus(paymentInfo, 0n);

switch (status) {
  case RequestStatus.Pending:
    console.log('Awaiting your decision');
    break;
  case RequestStatus.Approved:
    console.log('You approved this refund');
    break;
  case RequestStatus.Denied:
    console.log('You denied this refund');
    break;
  case RequestStatus.Cancelled:
    console.log('Payer cancelled the request');
    break;
}

Get Full Refund Request Data

Use getRefundRequest() to retrieve the complete refund request data, including the amount and status.
import type { RefundRequestData } from '@x402r/core';

const request: RefundRequestData = await merchant.getRefundRequest(paymentInfo, 0n);

console.log('Payment hash:', request.paymentInfoHash);
console.log('Nonce:', request.nonce);
console.log('Requested amount:', request.amount);
console.log('Status:', request.status);
The RefundRequestData type contains:
FieldTypeDescription
paymentInfoHash0x${string}Hash of the PaymentInfo struct
noncebigintRecord index this refund targets
amountbigintAmount requested for refund (uint120)
statusRequestStatusCurrent status (Pending, Approved, Denied, Cancelled)

Get Refund Request by Composite Key

Use getRefundRequestByKey() to look up a refund request directly by its composite key (the keccak256(paymentInfoHash, nonce) value returned from paginated queries).
const request = await merchant.getRefundRequestByKey(compositeKey);

console.log('Amount:', request.amount);
console.log('Status:', request.status);

Paginated Refund Request Listing

Get Pending Refund Requests

Use getPendingRefundRequests() to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your X402rMerchant instance.
// Get the first 10 refund request keys
const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n);

console.log(`Showing ${keys.length} of ${total} total refund requests`);

// Look up each request by its composite key
for (const key of keys) {
  const request = await merchant.getRefundRequestByKey(key);
  console.log(`Key: ${key}`);
  console.log(`  Amount: ${request.amount}`);
  console.log(`  Status: ${request.status}`);
}
For pagination, adjust the offset and count parameters:
// Page through all refund requests, 20 at a time
const pageSize = 20n;
let offset = 0n;
let hasMore = true;

while (hasMore) {
  const { keys, total } = await merchant.getPendingRefundRequests(offset, pageSize);

  for (const key of keys) {
    const request = await merchant.getRefundRequestByKey(key);
    // Process each request...
  }

  offset += pageSize;
  hasMore = offset < total;
}

Get Refund Request Count

Use getRefundRequestCount() to get the total number of refund requests targeting the current receiver.
const count = await merchant.getRefundRequestCount();
console.log(`Total refund requests: ${count}`);

if (count > 0n) {
  const { keys } = await merchant.getPendingRefundRequests(0n, count);
  console.log(`Retrieved all ${keys.length} request keys`);
}

Refund Request Actions

Approve a Refund Request

Use approveRefundRequest() to approve a pending refund request. This changes the request status to Approved.
const { txHash } = await merchant.approveRefundRequest(paymentInfo, 0n);
console.log('Refund approved:', txHash);
Approving a refund request changes its status but does not transfer funds. You must also call refundInEscrow() or refundPostEscrow() to execute the actual token transfer.

Deny a Refund Request

Use denyRefundRequest() to deny a pending refund request. This changes the request status to Denied.
const { txHash } = await merchant.denyRefundRequest(paymentInfo, 0n);
console.log('Refund denied:', txHash);
If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk.

Freeze Management

Check If a Payment Is Frozen

Use isFrozen() to check whether a payment has been frozen by the payer or an arbiter. Frozen payments cannot be released until unfrozen.
const freezeAddress: `0x${string}` = '0xFreezeContract...';

const frozen = await merchant.isFrozen(paymentInfo, freezeAddress);

if (frozen) {
  console.log('Payment is frozen - cannot release until unfrozen');
} else {
  console.log('Payment is not frozen');
}

Unfreeze a Payment

Use unfreezePayment() to remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze.
const freezeAddress: `0x${string}` = '0xFreezeContract...';

const { txHash } = await merchant.unfreezePayment(paymentInfo, freezeAddress);
console.log('Payment unfrozen:', txHash);

Complete Refund Workflow

Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved.
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 handleRefundWorkflow(
  merchant: X402rMerchant,
  paymentInfo: PaymentInfo,
  nonce: bigint
) {
  // Step 1: Check if a refund request exists
  const hasRequest = await merchant.hasRefundRequest(paymentInfo, nonce);
  if (!hasRequest) {
    console.log('No refund request for this payment/nonce');
    return;
  }

  // Step 2: Get the full request data
  const request = await merchant.getRefundRequest(paymentInfo, nonce);
  console.log(`Refund request: ${request.amount} tokens, status: ${request.status}`);

  // Step 3: Only process pending requests
  if (request.status !== RequestStatus.Pending) {
    console.log('Request already processed');
    return;
  }

  // Step 4: Check if the payment is frozen
  const freezeAddress: `0x${string}` = '0xFreezeContract...';
  const frozen = await merchant.isFrozen(paymentInfo, freezeAddress);
  if (frozen) {
    console.log('Payment is frozen - resolve dispute before processing refund');
    return;
  }

  // Step 5: Check available amounts
  const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
  console.log(`Available to refund: ${refundableAmount}`);

  // Step 6: Make a decision
  const shouldApprove = request.amount <= refundableAmount;

  if (shouldApprove) {
    // Approve the request
    const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, nonce);
    console.log('Approved:', approveTx);

    // Execute the refund from escrow
    const { txHash: refundTx } = await merchant.refundInEscrow(paymentInfo, request.amount);
    console.log('Refund executed:', refundTx);
  } else {
    // Deny the request
    const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, nonce);
    console.log('Denied:', denyTx);
  }
}

Automated Refund Processing

This example shows a policy-based system that automatically processes refund requests based on configurable rules.
import { X402rMerchant } from '@x402r/merchant';
import { RequestStatus } from '@x402r/core';

interface RefundPolicy {
  /** Auto-approve refunds under this amount (in token units) */
  autoApproveUnder: bigint;
  /** Maximum number of requests to process per batch */
  batchSize: bigint;
}

async function processRefundBatch(
  merchant: X402rMerchant,
  policy: RefundPolicy
) {
  // Get total pending count
  const totalCount = await merchant.getRefundRequestCount();
  console.log(`Total refund requests: ${totalCount}`);

  if (totalCount === 0n) {
    console.log('No refund requests to process');
    return;
  }

  // Fetch a batch of request keys
  const { keys, total } = await merchant.getPendingRefundRequests(0n, policy.batchSize);
  console.log(`Processing ${keys.length} of ${total} requests`);

  for (const key of keys) {
    const request = await merchant.getRefundRequestByKey(key);

    // Skip non-pending requests
    if (request.status !== RequestStatus.Pending) {
      continue;
    }

    if (request.amount < policy.autoApproveUnder) {
      // Auto-approve small refunds
      // Note: approveRefundRequest and refundInEscrow need
      // the original paymentInfo, which you would retrieve
      // from your payment database using request.paymentInfoHash
      console.log(`Auto-approve candidate: key=${key}, amount=${request.amount}`);
    } else {
      console.log(`Manual review required: key=${key}, amount=${request.amount}`);
    }
  }
}

// Usage
const policy: RefundPolicy = {
  autoApproveUnder: BigInt('5000000'), // Auto-approve under 5 USDC
  batchSize: 20n,
};

await processRefundBatch(merchant, policy);

Refund Request Lifecycle

Method Reference

MethodParametersReturnsDescription
hasRefundRequestpaymentInfo, nonce: bigintPromise<boolean>Check if refund request exists
getRefundStatuspaymentInfo, nonce: bigintPromise<RequestStatus>Get request status
getRefundRequestpaymentInfo, nonce: bigintPromise<RefundRequestData>Get full request data
approveRefundRequestpaymentInfo, nonce: bigintPromise<{ txHash }>Approve a pending request
denyRefundRequestpaymentInfo, nonce: bigintPromise<{ txHash }>Deny a pending request
getPendingRefundRequestsoffset: bigint, count: bigintPromise<{ keys, total }>Paginated request keys
getRefundRequestCount(none)Promise<bigint>Total requests for receiver
getRefundRequestByKeycompositeKey: hexPromise<RefundRequestData>Look up by composite key
unfreezePaymentpaymentInfo, freezeAddress: hexPromise<{ txHash }>Remove payment freeze
isFrozenpaymentInfo, freezeAddress: hexPromise<boolean>Check if payment is frozen

Next Steps