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:
| Field | Type | Description |
|---|
feeCalculator | 0x${string} | Contract that computes fee amounts |
protocolFeeConfig | 0x${string} | Protocol-level fee configuration |
feeRecipient | 0x${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