Skip to main content
The X402rMerchant class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration.

Payment Operations

Release Funds from Escrow

Use release() to transfer escrowed funds to the receiver (merchant). The amount parameter is required and specifies the exact amount to release in token units.
import { X402rMerchant } from '@x402r/merchant';

// Release 10 USDC (6 decimals) from escrow
const { txHash } = await merchant.release(paymentInfo, BigInt('10000000'));
console.log('Released:', txHash);
For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later.
// Release 3 USDC of a 10 USDC escrow
const { txHash } = await merchant.release(paymentInfo, BigInt('3000000'));
console.log('Partial release:', txHash);

// Check what remains
const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo);
console.log('Remaining in escrow:', capturableAmount); // 7000000n
The amount parameter is always required. There is no default “release all” behavior. Always query getPaymentAmounts() first to determine the available capturable amount.

Refund While in Escrow

Use refundInEscrow() to return escrowed funds to the payer before release. The amount parameter is required.
// Full refund of 10 USDC
const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000'));
console.log('Refunded from escrow:', txHash);
// Partial refund: return 2 USDC, keep 8 USDC in escrow
const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000'));
console.log('Partial refund:', txHash);

Charge Directly

Use charge() for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 transferWithAuthorization).
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';

const { txHash } = await merchant.charge(
  paymentInfo,
  BigInt('5000000'),       // 5 USDC
  tokenCollectorAddress,   // token collector contract
  collectorData            // authorization data (e.g., ERC-3009 signature)
);
console.log('Charged:', txHash);
The charge() method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer.

Refund After Release (Post-Escrow)

Use refundPostEscrow() to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant’s balance.
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';

const { txHash } = await merchant.refundPostEscrow(
  paymentInfo,
  BigInt('5000000'),       // 5 USDC to refund
  tokenCollectorAddress,   // token collector that sources the refund
  collectorData            // authorization data
);
console.log('Post-escrow refund:', txHash);
Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer.

Query Methods

Get Payment Amounts

Use getPaymentAmounts() to query the current capturable and refundable amounts for a payment. This method reads directly from the escrow contract and is fully functional (not stubbed).
const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);

console.log('Capturable:', capturableAmount);  // Funds available to release
console.log('Refundable:', refundableAmount);  // Funds available to refund

if (capturableAmount > 0n) {
  // Release available funds
  const { txHash } = await merchant.release(paymentInfo, capturableAmount);
  console.log('Released all capturable funds:', txHash);
}
getPaymentAmounts() requires the escrowAddress to be configured when creating the X402rMerchant instance.

Get Operator Configuration

Use getOperatorConfig() to retrieve all 14 immutable slot addresses from the PaymentOperator contract. This includes the escrow address, fee configuration, all 5 condition slots, and all 5 recorder slots.
const config = await merchant.getOperatorConfig();

// Core state
console.log('Escrow:', config.escrow);
console.log('Fee recipient:', config.feeRecipient);
console.log('Fee calculator:', config.feeCalculator);
console.log('Protocol fee config:', config.protocolFeeConfig);

// Condition slots (address(0) = always allow)
console.log('Authorize condition:', config.authorizeCondition);
console.log('Charge condition:', config.chargeCondition);
console.log('Release condition:', config.releaseCondition);
console.log('Refund in-escrow condition:', config.refundInEscrowCondition);
console.log('Refund post-escrow condition:', config.refundPostEscrowCondition);

// Recorder slots (address(0) = no-op)
console.log('Authorize recorder:', config.authorizeRecorder);
console.log('Charge recorder:', config.chargeRecorder);
console.log('Release recorder:', config.releaseRecorder);
console.log('Refund in-escrow recorder:', config.refundInEscrowRecorder);
console.log('Refund post-escrow recorder:', config.refundPostEscrowRecorder);

Get Fee Structure

Use getFeeStructure() to retrieve the fee-related addresses for the operator. This is a lighter alternative to getOperatorConfig() when you only need fee information.
const fees = await merchant.getFeeStructure();

console.log('Fee calculator:', fees.feeCalculator);
console.log('Protocol fee config:', fees.protocolFeeConfig);
console.log('Fee recipient:', fees.feeRecipient);
The returned FeeStructure contains three fields:
FieldTypeDescription
feeCalculator0x${string}Contract that computes fee amounts
protocolFeeConfig0x${string}Protocol-level fee configuration
feeRecipient0x${string}Address that receives the operator’s fee share

Get Release Conditions

Use getReleaseConditions() to check which condition contract governs release operations. A zero address means releases are always allowed.
const releaseCondition = await merchant.getReleaseConditions();

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
if (releaseCondition === ZERO_ADDRESS) {
  console.log('No release conditions configured - releases always allowed');
} else {
  console.log('Release condition contract:', releaseCondition);
}

Stubbed Methods

The following methods are defined but not yet implemented. They throw a NotImplementedError when called and will be available once subgraph/indexer integration is complete.
These methods throw NotImplementedError at runtime. Do not call them in production code until they are implemented.

getPaymentState

Returns the lifecycle state of a payment (NonExistent, InEscrow, Released, Settled, Expired). Requires subgraph integration.
import { PaymentState, NotImplementedError } from '@x402r/core';

try {
  const state = await merchant.getPaymentState(paymentInfo);
} catch (error) {
  if (error instanceof NotImplementedError) {
    console.log('getPaymentState requires subgraph integration');
    // Use getPaymentAmounts() as an alternative to infer state
    const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
    if (capturableAmount > 0n) {
      console.log('Payment has funds in escrow');
    }
  }
}

getReceiverPayments

Returns all payment hashes where the current wallet is the receiver. Requires subgraph integration.
try {
  const { hashes } = await merchant.getReceiverPayments();
} catch (error) {
  if (error instanceof NotImplementedError) {
    console.log('getReceiverPayments requires subgraph integration');
    // Use event subscriptions as an alternative to track payments
  }
}

Release vs Refund Decision Flow

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 } 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: '0xYourOperator...',
    escrowAddress: config.authCaptureEscrow,
    refundRequestAddress: config.refundRequest,
  });

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

  // 2. Release if funds are available
  if (capturableAmount > 0n) {
    const { txHash } = await merchant.release(paymentInfo, capturableAmount);
    console.log('Released:', txHash);
  }

  // 3. Inspect operator configuration
  const operatorConfig = await merchant.getOperatorConfig();
  console.log('Escrow contract:', operatorConfig.escrow);
  console.log('Release condition:', operatorConfig.releaseCondition);

  // 4. Check fee structure
  const fees = await merchant.getFeeStructure();
  console.log('Fee calculator:', fees.feeCalculator);
}

main().catch(console.error);

Next Steps