Skip to main content
The Client SDK provides methods to subscribe to blockchain events in real-time using viem’s watchContractEvent under the hood. All subscription methods return an object with an unsubscribe function that you should call when you no longer need the watcher.

watchPaymentState

Watch for state changes on a specific payment. This subscribes to ReleaseExecuted, RefundInEscrowExecuted, and RefundPostEscrowExecuted events on the PaymentOperator contract.
const { unsubscribe } = client.watchPaymentState(
  paymentInfoHash,
  (event) => {
    console.log(`Payment event: ${event.eventName}`);

    switch (event.eventName) {
      case 'ReleaseExecuted':
        console.log('Funds released to merchant');
        console.log('Amount:', event.args.amount);
        break;
      case 'RefundInEscrowExecuted':
        console.log('Funds refunded from escrow');
        break;
      case 'RefundPostEscrowExecuted':
        console.log('Funds refunded after escrow period');
        break;
    }
  }
);

// Stop watching when done
unsubscribe();

Signature

watchPaymentState(
  paymentInfoHash: `0x${string}`,
  callback: (event: PaymentOperatorEventLog) => void
): { unsubscribe: () => void }

PaymentOperatorEventLog Type

interface PaymentOperatorEventLog {
  eventName:
    | 'ReleaseExecuted'
    | 'RefundInEscrowExecuted'
    | 'RefundPostEscrowExecuted'
    | 'AuthorizationCreated'
    | 'ChargeExecuted';
  args: {
    paymentInfoHash?: `0x${string}`;
    payer?: `0x${string}`;
    receiver?: `0x${string}`;
    amount?: bigint;
  };
  address: `0x${string}`;
  blockNumber: bigint;
  transactionHash: `0x${string}`;
  logIndex: number;
}

watchRefundRequests

Watch for refund request lifecycle events. This subscribes to RefundRequested, RefundRequestStatusUpdated, and RefundRequestCancelled events on the RefundRequest contract.
const { unsubscribe } = client.watchRefundRequests((event) => {
  switch (event.eventName) {
    case 'RefundRequested':
      console.log('New refund request submitted');
      console.log('Payment:', event.args.paymentInfoHash);
      console.log('Amount:', event.args.amount);
      break;
    case 'RefundRequestStatusUpdated':
      console.log('Refund status changed:', event.args.status);
      // 1 = Approved, 2 = Denied
      break;
    case 'RefundRequestCancelled':
      console.log('Refund request cancelled');
      break;
  }
});

// Stop watching when done
unsubscribe();

Signature

watchRefundRequests(
  callback: (event: RefundRequestEventLog) => void
): { unsubscribe: () => void }
Requires refundRequestAddress to be configured on the client. Throws an error if not set.

RefundRequestEventLog Type

interface RefundRequestEventLog {
  eventName:
    | 'RefundRequested'
    | 'RefundRequestStatusUpdated'
    | 'RefundRequestCancelled';
  args: {
    paymentInfoHash?: `0x${string}`;
    payer?: `0x${string}`;
    receiver?: `0x${string}`;
    amount?: bigint;
    nonce?: bigint;
    status?: number;
  };
  address: `0x${string}`;
  blockNumber: bigint;
  transactionHash: `0x${string}`;
  logIndex: number;
}

watchMyPayments

Watch for new payment authorizations where the connected wallet is the payer. This subscribes to AuthorizationCreated events on the PaymentOperator contract, filtered by the wallet’s address.
const { unsubscribe } = client.watchMyPayments((event) => {
  console.log('New payment authorized!');
  console.log('Event:', event.eventName);        // 'AuthorizationCreated'
  console.log('Hash:', event.args.paymentInfoHash);
  console.log('Receiver:', event.args.receiver);
  console.log('Amount:', event.args.amount);
});

// Stop watching when done
unsubscribe();

Signature

watchMyPayments(
  callback: (event: PaymentOperatorEventLog) => void
): { unsubscribe: () => void }
Requires a walletClient with an account to be configured, since the events are filtered by the payer address.

watchFreezeEvents

Watch for freeze and unfreeze events on a specific Freeze contract. This subscribes to PaymentFrozen and PaymentUnfrozen events.
const freezeAddress = '0x...'; // Freeze contract address

const { unsubscribe } = client.watchFreezeEvents(
  freezeAddress,
  (event) => {
    if (event.eventName === 'PaymentFrozen') {
      console.log('Payment frozen:', event.args.paymentInfoHash);
      console.log('Frozen by:', event.args.caller);
    } else if (event.eventName === 'PaymentUnfrozen') {
      console.log('Payment unfrozen:', event.args.paymentInfoHash);
      console.log('Unfrozen by:', event.args.caller);
    }
  }
);

// Stop watching when done
unsubscribe();

Signature

watchFreezeEvents(
  freezeAddress: `0x${string}`,
  callback: (event: FreezeEventLog) => void
): { unsubscribe: () => void }

FreezeEventLog Type

interface FreezeEventLog {
  eventName: 'PaymentFrozen' | 'PaymentUnfrozen';
  args: {
    paymentInfoHash?: `0x${string}`;
    caller?: `0x${string}`;
  };
  address: `0x${string}`;
  blockNumber: bigint;
  transactionHash: `0x${string}`;
  logIndex: number;
}

Combining Multiple Watchers

You can run multiple watchers simultaneously and clean them all up together. This is a common pattern for building dashboards or monitoring services.
import { X402rClient } from '@x402r/client';
import type { PaymentInfo } from '@x402r/core';

class PaymentMonitor {
  private unsubscribers: (() => void)[] = [];

  constructor(
    private client: X402rClient,
    private freezeAddress: `0x${string}`
  ) {}

  start() {
    // Watch for new payments
    const { unsubscribe: unwatchPayments } = this.client.watchMyPayments(
      (event) => {
        console.log('[Payment] New authorization:', event.args.paymentInfoHash);
      }
    );
    this.unsubscribers.push(unwatchPayments);

    // Watch for refund request updates
    const { unsubscribe: unwatchRefunds } = this.client.watchRefundRequests(
      (event) => {
        console.log(`[Refund] ${event.eventName}:`, event.args.paymentInfoHash);
      }
    );
    this.unsubscribers.push(unwatchRefunds);

    // Watch for freeze/unfreeze events
    const { unsubscribe: unwatchFreezes } = this.client.watchFreezeEvents(
      this.freezeAddress,
      (event) => {
        console.log(`[Freeze] ${event.eventName}:`, event.args.paymentInfoHash);
      }
    );
    this.unsubscribers.push(unwatchFreezes);

    console.log('Payment monitor started with 3 watchers');
  }

  /** Watch a specific payment for state changes */
  watchPayment(paymentInfoHash: `0x${string}`) {
    const { unsubscribe } = this.client.watchPaymentState(
      paymentInfoHash,
      (event) => {
        console.log(`[State] ${event.eventName} for ${paymentInfoHash}`);
      }
    );
    this.unsubscribers.push(unsubscribe);
  }

  stop() {
    for (const unsub of this.unsubscribers) {
      unsub();
    }
    this.unsubscribers = [];
    console.log('Payment monitor stopped - all watchers cleaned up');
  }
}

// Usage
const monitor = new PaymentMonitor(client, freezeAddress);
monitor.start();

// Optionally track a specific payment
monitor.watchPayment('0xabc123...');

// On application shutdown
process.on('SIGINT', () => {
  monitor.stop();
  process.exit();
});

Event Types Reference

MethodEvents WatchedContractUse Case
watchPaymentStateReleaseExecuted, RefundInEscrowExecuted, RefundPostEscrowExecutedPaymentOperatorTrack a single payment’s lifecycle
watchRefundRequestsRefundRequested, RefundRequestStatusUpdated, RefundRequestCancelledRefundRequestMonitor refund request workflow
watchMyPaymentsAuthorizationCreated (filtered by payer)PaymentOperatorTrack new payments for your wallet
watchFreezeEventsPaymentFrozen, PaymentUnfrozenFreezeMonitor dispute freeze activity

Best Practices

Store the unsubscribe function and call it when you no longer need the watcher. Failing to unsubscribe causes memory leaks and orphaned WebSocket connections.
const { unsubscribe } = client.watchPaymentState(hash, callback);

// In your cleanup / shutdown logic:
unsubscribe();
Event subscriptions use WebSocket connections under the hood. If you are running a long-lived process, consider implementing reconnection logic:
function watchWithReconnect(
  hash: `0x${string}`,
  callback: (event: PaymentOperatorEventLog) => void
) {
  let sub = client.watchPaymentState(hash, callback);

  // Reconnect on error (simplified example)
  const reconnect = () => {
    sub.unsubscribe();
    sub = client.watchPaymentState(hash, callback);
  };

  return {
    unsubscribe: () => sub.unsubscribe(),
    reconnect,
  };
}
For reliable real-time events, configure your viem PublicClient with a WebSocket transport instead of HTTP polling:
import { createPublicClient, webSocket } from 'viem';
import { baseSepolia } from 'viem/chains';

const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: webSocket('wss://base-sepolia.your-rpc-provider.com'),
});
Multiple events can fire in quick succession (e.g., batch releases). Consider debouncing your UI update handlers:
function debounce<T extends (...args: unknown[]) => void>(
  fn: T,
  ms: number
): T {
  let timer: NodeJS.Timeout;
  return ((...args: unknown[]) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  }) as T;
}

const debouncedHandler = debounce((event) => {
  console.log('Processing event:', event);
}, 500);

client.watchMyPayments(debouncedHandler);

Next Steps