Skip to main content

Payment States

Every payment in X402r goes through a defined lifecycle represented by the PaymentState enum:
import { PaymentState } from '@x402r/core';

// PaymentState values:
PaymentState.NonExistent  // 0 - Payment doesn't exist
PaymentState.InEscrow     // 1 - Funds held in escrow
PaymentState.Released     // 2 - Funds released to merchant
PaymentState.Settled      // 3 - Payment fully settled
PaymentState.Expired      // 4 - Payment expired

Payment Info

The PaymentInfo struct uniquely identifies a payment and contains all its parameters:
interface PaymentInfo {
  operator: `0x${string}`;          // PaymentOperator contract address
  payer: `0x${string}`;             // Payer's address
  receiver: `0x${string}`;          // Merchant's address
  token: `0x${string}`;             // Payment token (e.g., USDC)
  maxAmount: bigint;                // Maximum payment amount
  preApprovalExpiry: bigint;        // Pre-approval expiry timestamp (0n if not used)
  authorizationExpiry: bigint;      // When payer can reclaim funds
  refundExpiry: bigint;             // Deadline for refund requests
  minFeeBps: number;                // Minimum fee in basis points
  maxFeeBps: number;                // Maximum fee in basis points
  feeReceiver: `0x${string}`;      // Address that receives fees
  salt: bigint;                     // Unique salt for this payment
}
Use computePaymentInfoHash() from @x402r/core to compute the unique hash of a payment. Use parsePaymentInfo() to deserialize a JSON PaymentInfo back into the typed struct.

Escrow Period

The EscrowPeriod contract tracks when a payment was authorized and enforces a configurable waiting period before funds can be released. During the escrow period:
  • Payers can request refunds or freeze the payment
  • Merchants can refund but cannot release
  • After the period — merchants can release funds to themselves
import { X402rClient } from '@x402r/client';

// Check when payment was authorized
const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress);

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

Refund Requests

When a payer wants a refund, they create a refund request that goes through approval:
import { RequestStatus } from '@x402r/core';

// RequestStatus values:
RequestStatus.Pending    // 0 - Awaiting decision
RequestStatus.Approved   // 1 - Approved by merchant/arbiter
RequestStatus.Denied     // 2 - Denied by merchant/arbiter
RequestStatus.Cancelled  // 3 - Cancelled by payer

Refund Flow

The Nonce Parameter

All refund methods require a nonce parameter. This is the record index from the PaymentIndexRecorder that identifies which charge the refund request applies to. For the first charge, use 0n.
// Request refund for the first charge
await client.requestRefund(paymentInfo, amount, 0n);

// Check status for the first charge
const status = await client.getRefundStatus(paymentInfo, 0n);

Freeze / Unfreeze

The Freeze contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted:
// Payer freezes payment (requires payer authorization)
await client.freezePayment(paymentInfo, freezeAddress);

// Merchant unfreezes payment (requires receiver authorization)
await merchant.unfreezePayment(paymentInfo, freezeAddress);

// Check frozen status
const frozen = await client.isFrozen(paymentInfo, freezeAddress);
Freezing a payment does not automatically escalate to an arbiter. It pauses the release condition to allow time for dispute resolution.

Roles and Permissions

RoleCan Do
PayerRequest refunds, freeze payments, cancel requests, query escrow state
MerchantRelease payments, charge, approve/deny refunds, unfreeze payments
ArbiterApprove/deny disputed refunds, execute refunds, batch operations, registry

Contract Architecture

Next Steps