Skip to main content
The Arbiter SDK provides three subscription methods for monitoring dispute activity in real-time. Each returns an object with an unsubscribe function you call to stop watching.

Watch New Cases

Subscribe to RefundRequested events — these are new refund requests that need your attention:
import type { RefundRequestEventLog } from '@x402r/core';

const { unsubscribe } = arbiter.watchNewCases((event: RefundRequestEventLog) => {
  console.log('New refund request!');
  console.log('Payment hash:', event.args.paymentInfoHash);
  console.log('Payer:', event.args.payer);
  console.log('Receiver:', event.args.receiver);
  console.log('Amount:', event.args.amount);
  console.log('Nonce:', event.args.nonce);
  console.log('Block:', event.blockNumber);
});

// Later: stop watching
unsubscribe();

Watch Decisions

Subscribe to RefundRequestStatusUpdated events — these fire when a refund request is approved or denied:
import type { RefundRequestEventLog } from '@x402r/core';

const { unsubscribe } = arbiter.watchDecisions((event: RefundRequestEventLog) => {
  console.log('Decision made!');
  console.log('Payment hash:', event.args.paymentInfoHash);
  console.log('New status:', event.args.status);
  console.log('Transaction:', event.transactionHash);
});

Watch Freeze Events

Subscribe to PaymentFrozen and PaymentUnfrozen events from a Freeze condition contract:
import type { FreezeEventLog } from '@x402r/core';

const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`;

const { unsubscribe } = arbiter.watchFreezeEvents(
  freezeAddress,
  (event: FreezeEventLog) => {
    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);
    }
  }
);

Event Type Reference

MethodContract EventFires When
watchNewCasesRefundRequestedA payer submits a new refund request
watchDecisionsRefundRequestStatusUpdatedAn arbiter or receiver approves/denies a request
watchFreezeEventsPaymentFrozen / PaymentUnfrozenA payment is frozen or unfrozen

Event Log Types

Both watchNewCases and watchDecisions emit RefundRequestEventLog events:
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;
}
The watchFreezeEvents method emits FreezeEventLog events:
interface FreezeEventLog {
  eventName: 'PaymentFrozen' | 'PaymentUnfrozen';
  args: {
    paymentInfoHash?: `0x${string}`;
    caller?: `0x${string}`;
  };
  address: `0x${string}`;
  blockNumber: bigint;
  transactionHash: `0x${string}`;
  logIndex: number;
}

Example: Real-Time Arbiter Service

Combine all three watchers into a single service that processes cases automatically:
import { X402rArbiter } from '@x402r/arbiter';
import { RequestStatus } from '@x402r/core';
import type { PaymentInfo, RefundRequestEventLog, FreezeEventLog } from '@x402r/core';

class ArbiterService {
  private arbiter: X402rArbiter;
  private unsubscribers: Array<() => void> = [];
  private lookupPaymentInfo: (hash: `0x${string}`) => Promise<PaymentInfo>;

  constructor(
    arbiter: X402rArbiter,
    lookupPaymentInfo: (hash: `0x${string}`) => Promise<PaymentInfo>
  ) {
    this.arbiter = arbiter;
    this.lookupPaymentInfo = lookupPaymentInfo;
  }

  start(freezeAddress: `0x${string}`) {
    // Watch for new refund requests
    const { unsubscribe: unwatchCases } = this.arbiter.watchNewCases(
      (event) => this.handleNewCase(event)
    );
    this.unsubscribers.push(unwatchCases);

    // Watch for decisions (for logging and metrics)
    const { unsubscribe: unwatchDecisions } = this.arbiter.watchDecisions(
      (event) => this.handleDecision(event)
    );
    this.unsubscribers.push(unwatchDecisions);

    // Watch for freeze events
    const { unsubscribe: unwatchFreezes } = this.arbiter.watchFreezeEvents(
      freezeAddress,
      (event) => this.handleFreezeEvent(event)
    );
    this.unsubscribers.push(unwatchFreezes);

    console.log('Arbiter service started with 3 active watchers');
  }

  stop() {
    for (const unsub of this.unsubscribers) {
      unsub();
    }
    this.unsubscribers = [];
    console.log('Arbiter service stopped');
  }

  private async handleNewCase(event: RefundRequestEventLog) {
    const paymentInfoHash = event.args.paymentInfoHash!;
    const nonce = event.args.nonce ?? 0n;

    console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`);

    try {
      const paymentInfo = await this.lookupPaymentInfo(paymentInfoHash);

      // Verify the request exists and is pending
      const hasRequest = await this.arbiter.hasRefundRequest(paymentInfo, nonce);
      if (!hasRequest) {
        console.log('[SKIP] Request not found on-chain');
        return;
      }

      const status = await this.arbiter.getRefundStatus(paymentInfo, nonce);
      if (status !== RequestStatus.Pending) {
        console.log(`[SKIP] Request already ${RequestStatus[status]}`);
        return;
      }

      // Apply your decision logic
      const shouldApprove = await this.evaluateCase(paymentInfo);

      if (shouldApprove) {
        await this.arbiter.approveRefundRequest(paymentInfo, nonce);
        await this.arbiter.executeRefundInEscrow(paymentInfo);
        console.log(`[APPROVED + REFUNDED] ${paymentInfoHash}`);
      } else {
        await this.arbiter.denyRefundRequest(paymentInfo, nonce);
        console.log(`[DENIED] ${paymentInfoHash}`);
      }
    } catch (error) {
      console.error(`[ERROR] Failed to process ${paymentInfoHash}:`, error);
    }
  }

  private handleDecision(event: RefundRequestEventLog) {
    console.log(`[DECISION] Payment: ${event.args.paymentInfoHash}`);
    console.log(`           Status: ${event.args.status}`);
    console.log(`           Tx: ${event.transactionHash}`);
  }

  private handleFreezeEvent(event: FreezeEventLog) {
    if (event.eventName === 'PaymentFrozen') {
      console.log(`[FROZEN] Payment ${event.args.paymentInfoHash} frozen by ${event.args.caller}`);
    } else {
      console.log(`[UNFROZEN] Payment ${event.args.paymentInfoHash}`);
    }
  }

  private async evaluateCase(paymentInfo: PaymentInfo): Promise<boolean> {
    // Your evaluation logic here
    return paymentInfo.maxAmount < BigInt('5000000'); // Auto-approve < 5 USDC
  }
}

// Usage
const service = new ArbiterService(arbiter, lookupPaymentInfo);
service.start('0xFreezeContractAddress...' as `0x${string}`);

// Graceful shutdown
process.on('SIGINT', () => {
  service.stop();
  process.exit();
});

Example: Real-Time Dashboard API

Build an Express API that tracks arbiter activity in real-time using all three watchers:
import express from 'express';
import { X402rArbiter } from '@x402r/arbiter';
import { RequestStatus } from '@x402r/core';
import type { RefundRequestEventLog, FreezeEventLog } from '@x402r/core';

const app = express();

// Dashboard state
const dashboard = {
  pendingCases: [] as Array<{
    paymentInfoHash: string;
    payer: string;
    amount: string;
    nonce: string;
    timestamp: number;
  }>,
  recentDecisions: [] as Array<{
    paymentInfoHash: string;
    status: number;
    txHash: string;
    timestamp: number;
  }>,
  frozenPayments: new Set<string>(),
};

function setupWatchers(arbiter: X402rArbiter, freezeAddress: `0x${string}`) {
  // Track new refund requests
  const { unsubscribe: unwatchCases } = arbiter.watchNewCases(
    (event: RefundRequestEventLog) => {
      dashboard.pendingCases.push({
        paymentInfoHash: event.args.paymentInfoHash ?? '',
        payer: event.args.payer ?? '',
        amount: (event.args.amount ?? 0n).toString(),
        nonce: (event.args.nonce ?? 0n).toString(),
        timestamp: Date.now(),
      });
    }
  );

  // Track decisions and remove from pending
  const { unsubscribe: unwatchDecisions } = arbiter.watchDecisions(
    (event: RefundRequestEventLog) => {
      const hash = event.args.paymentInfoHash ?? '';

      // Remove from pending
      dashboard.pendingCases = dashboard.pendingCases.filter(
        (c) => c.paymentInfoHash !== hash
      );

      // Add to recent decisions
      dashboard.recentDecisions.push({
        paymentInfoHash: hash,
        status: event.args.status ?? 0,
        txHash: event.transactionHash,
        timestamp: Date.now(),
      });

      // Keep only the last 100 decisions
      if (dashboard.recentDecisions.length > 100) {
        dashboard.recentDecisions = dashboard.recentDecisions.slice(-100);
      }
    }
  );

  // Track freeze/unfreeze events
  const { unsubscribe: unwatchFreezes } = arbiter.watchFreezeEvents(
    freezeAddress,
    (event: FreezeEventLog) => {
      const hash = event.args.paymentInfoHash ?? '';
      if (event.eventName === 'PaymentFrozen') {
        dashboard.frozenPayments.add(hash);
      } else {
        dashboard.frozenPayments.delete(hash);
      }
    }
  );

  return () => {
    unwatchCases();
    unwatchDecisions();
    unwatchFreezes();
  };
}

// API endpoints
app.get('/api/dashboard', (_req, res) => {
  res.json({
    pendingCount: dashboard.pendingCases.length,
    frozenCount: dashboard.frozenPayments.size,
    recentDecisions: dashboard.recentDecisions.slice(-10).map((d) => ({
      ...d,
      statusName: RequestStatus[d.status],
    })),
  });
});

app.get('/api/pending', (_req, res) => {
  res.json({ cases: dashboard.pendingCases });
});

app.get('/api/frozen', (_req, res) => {
  res.json({ payments: Array.from(dashboard.frozenPayments) });
});

// Initialize
const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`;
const cleanup = setupWatchers(arbiter, freezeAddress);

app.listen(3000, () => {
  console.log('Arbiter dashboard running on http://localhost:3000');
});

process.on('SIGINT', () => {
  cleanup();
  process.exit();
});

Best Practices

Events may be missed during network disconnections or RPC outages. Periodically poll getPendingRefundRequests() to catch up on any missed cases.
PracticeDetail
Idempotent handlersDesign event handlers so that processing the same event twice has no adverse effects.
Graceful shutdownAlways call unsubscribe() during shutdown to close WebSocket connections cleanly.
Error boundariesWrap handler logic in try/catch so one failed event does not stop the watcher.
LoggingLog every event with its block number and transaction hash for debugging and audit trails.
Polling fallbackCombine watchers with periodic polling via getPendingRefundRequests(offset, count) for reliability.

Next Steps