Skip to main content
This guide shows how to build real-time settlement tracking components - a step progress indicator, an event timeline, deposit status per leg, and a notification badge for settlements that need attention.

Prerequisites

  • @keystoneos/react installed
  • KeystoneProvider configured with settlements:read scope
  • A React project with TypeScript

What you will build

Four components that work together or independently:
  1. Progress stepper - visual indicator of where a settlement is in its lifecycle
  2. Event timeline - chronological list of every state transition
  3. Deposit tracker - per-leg deposit status for settlements awaiting deposits
  4. Notification badge - compact count of settlements needing action

Progress stepper

The settlement state machine follows a linear path (with branches for failure). Use getCurrentStepIndex() and SETTLEMENT_STEPS from @keystoneos/react to build a visual stepper.
// progress-stepper.tsx
import { useSettlement, getCurrentStepIndex, isTerminalState, SETTLEMENT_STEPS } from '@keystoneos/react';

interface ProgressStepperProps {
  settlementId: string;
}

export function ProgressStepper({ settlementId }: ProgressStepperProps) {
  const { settlement, isLoading } = useSettlement(settlementId);

  if (isLoading || !settlement) return <div>Loading...</div>;

  const currentIndex = getCurrentStepIndex(settlement.state);
  const isTerminal = isTerminalState(settlement.state);
  const isFailed = settlement.state === 'ROLLED_BACK' || settlement.state === 'TIMED_OUT';

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
      {SETTLEMENT_STEPS.map((step, index) => {
        const isCompleted = index < currentIndex;
        const isCurrent = index === currentIndex;
        const isPending = index > currentIndex;

        let dotColor = '#d1d5db'; // gray - pending
        if (isCompleted) dotColor = '#16a34a'; // green - done
        if (isCurrent && isFailed) dotColor = '#dc2626'; // red - failed
        if (isCurrent && !isFailed && !isTerminal) dotColor = '#2563eb'; // blue - active
        if (isCurrent && isTerminal && !isFailed) dotColor = '#16a34a'; // green - finalized

        let lineColor = '#e5e7eb'; // gray
        if (isCompleted) lineColor = '#16a34a'; // green

        return (
          <div key={step.state} style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
            {/* Dot and connector line */}
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '24px' }}>
              <div style={{
                width: isCurrent ? '14px' : '10px',
                height: isCurrent ? '14px' : '10px',
                borderRadius: '50%',
                backgroundColor: dotColor,
                border: isCurrent ? `3px solid ${dotColor}33` : 'none',
                flexShrink: 0,
              }} />
              {index < SETTLEMENT_STEPS.length - 1 && (
                <div style={{ width: '2px', height: '32px', backgroundColor: lineColor }} />
              )}
            </div>

            {/* Label and description */}
            <div style={{ paddingBottom: '16px' }}>
              <p style={{
                fontSize: '14px',
                fontWeight: isCurrent ? 700 : 500,
                color: isPending ? '#9ca3af' : '#111827',
              }}>
                {step.label}
              </p>
              {isCurrent && !isTerminal && (
                <p style={{ fontSize: '12px', color: '#6b7280', marginTop: '2px' }}>
                  {step.description}
                </p>
              )}
            </div>
          </div>
        );
      })}

      {/* Terminal state banner */}
      {isTerminal && (
        <div style={{
          marginTop: '8px',
          padding: '12px 16px',
          borderRadius: '8px',
          backgroundColor: isFailed ? '#fee2e2' : '#dcfce7',
          color: isFailed ? '#991b1b' : '#166534',
          fontSize: '14px',
          fontWeight: 600,
        }}>
          {settlement.state === 'FINALIZED' && 'Settlement complete. All assets have been transferred.'}
          {settlement.state === 'ROLLED_BACK' && 'Settlement rolled back. All deposits have been returned.'}
          {settlement.state === 'TIMED_OUT' && 'Settlement timed out. Deposits have been returned to original owners.'}
        </div>
      )}
    </div>
  );
}
The SETTLEMENT_STEPS array contains the ordered steps:
IndexStateLabelDescription
0INSTRUCTEDInstructedSettlement created from matched instructions
1COMPLIANCE_CHECKINGCompliance CheckScreening parties and wallets
2COMPLIANCE_CLEAREDCompliance ClearedAll checks passed
3AWAITING_DEPOSITSAwaiting DepositsWaiting for escrow deposits
4EXECUTING_SWAPExecuting SwapCoordinating atomic transfer
5FINALIZEDFinalizedSettlement complete

Event timeline

The useSettlementEvents hook returns every state transition that has occurred, in chronological order. This gives a full audit trail.
// event-timeline.tsx
import { useSettlementEvents, getStateInfo } from '@keystoneos/react';

interface EventTimelineProps {
  settlementId: string;
}

const TRIGGER_LABELS: Record<string, string> = {
  'system:engine': 'Settlement Engine',
  'system:contract': 'Smart Contract',
  'system:compliance': 'Compliance Check',
  'm2m:platform': 'Platform API',
  'session:user': 'User Action',
};

export function EventTimeline({ settlementId }: EventTimelineProps) {
  const { events, isLoading, error } = useSettlementEvents(settlementId);

  if (isLoading) return <div>Loading events...</div>;
  if (error) return <div style={{ color: '#dc2626' }}>Error: {error}</div>;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
      <h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '16px' }}>Event Timeline</h3>

      {events.length === 0 ? (
        <p style={{ color: '#6b7280', fontSize: '14px' }}>No events yet.</p>
      ) : (
        events.map((event, index) => {
          const stateInfo = getStateInfo(event.to_state);
          const isLast = index === events.length - 1;

          return (
            <div key={event.id} style={{ display: 'flex', gap: '12px' }}>
              {/* Timeline connector */}
              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '12px' }}>
                <div style={{
                  width: '8px', height: '8px', borderRadius: '50%',
                  backgroundColor: isLast ? '#2DD4A8' : '#9ca3af',
                  flexShrink: 0, marginTop: '6px',
                }} />
                {!isLast && (
                  <div style={{ width: '1px', height: '100%', minHeight: '40px', backgroundColor: '#e5e7eb' }} />
                )}
              </div>

              {/* Event content */}
              <div style={{ paddingBottom: '20px', flex: 1 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                  <span style={{ fontSize: '14px', fontWeight: 600 }}>{stateInfo.label}</span>
                  {event.from_state && (
                    <span style={{ fontSize: '12px', color: '#9ca3af' }}>
                      from {getStateInfo(event.from_state).label}
                    </span>
                  )}
                </div>

                <div style={{ display: 'flex', gap: '12px', marginTop: '4px', fontSize: '12px', color: '#6b7280' }}>
                  <span>{new Date(event.created_at).toLocaleString()}</span>
                  <span>{TRIGGER_LABELS[event.triggered_by] ?? event.triggered_by}</span>
                </div>

                {event.evidence_hash && (
                  <p style={{ marginTop: '4px', fontSize: '11px', fontFamily: 'monospace', color: '#9ca3af' }}>
                    Evidence: {event.evidence_hash.slice(0, 16)}...
                  </p>
                )}
              </div>
            </div>
          );
        })
      )}
    </div>
  );
}

Deposit tracker

When a settlement is in AWAITING_DEPOSITS, each leg needs a deposit to escrow. This component shows which legs are deposited and which are still pending.
// deposit-tracker.tsx
import { useSettlement, useDeposit } from '@keystoneos/react';

interface DepositTrackerProps {
  settlementId: string;
}

export function DepositTracker({ settlementId }: DepositTrackerProps) {
  const { settlement, isLoading } = useSettlement(settlementId);

  if (isLoading || !settlement) return <div>Loading...</div>;

  if (settlement.state !== 'AWAITING_DEPOSITS') {
    return null; // Only show during deposit phase
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
      <h3 style={{ fontSize: '16px', fontWeight: 700 }}>Deposit Status</h3>
      {settlement.legs.map((leg, index) => (
        <LegDepositRow
          key={index}
          settlementId={settlementId}
          legIndex={index}
          leg={leg}
        />
      ))}
    </div>
  );
}

interface LegDepositRowProps {
  settlementId: string;
  legIndex: number;
  leg: {
    instrument_id: string;
    quantity: string;
    leg_type: string;
    deposited?: boolean;
    deposit_tx_hash?: string;
  };
}

function LegDepositRow({ settlementId, legIndex, leg }: LegDepositRowProps) {
  const { status, txHash, error, deposit } = useDeposit(settlementId, legIndex);

  const isDeposited = leg.deposited || status === 'deposit_confirmed';
  const isProcessing = ['fetching_info', 'approving', 'approval_confirmed', 'depositing'].includes(status);
  const displayTxHash = txHash ?? leg.deposit_tx_hash;

  return (
    <div style={{
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '12px 16px', borderRadius: '8px',
      border: `1px solid ${isDeposited ? '#bbf7d0' : '#e5e7eb'}`,
      backgroundColor: isDeposited ? '#f0fdf4' : '#fff',
    }}>
      <div>
        <p style={{ fontSize: '14px', fontWeight: 600 }}>
          {leg.leg_type === 'asset_delivery' ? 'Asset Leg' : 'Payment Leg'}
        </p>
        <p style={{ fontSize: '13px', color: '#6b7280', fontFamily: 'monospace' }}>
          {Number(leg.quantity).toLocaleString()} {leg.instrument_id}
        </p>
        {displayTxHash && (
          <p style={{ fontSize: '11px', color: '#9ca3af', fontFamily: 'monospace', marginTop: '2px' }}>
            TX: {displayTxHash.slice(0, 10)}...{displayTxHash.slice(-8)}
          </p>
        )}
      </div>

      <div>
        {isDeposited && (
          <span style={{
            padding: '4px 10px', borderRadius: '9999px', fontSize: '12px',
            fontWeight: 600, backgroundColor: '#dcfce7', color: '#166534',
          }}>
            Deposited
          </span>
        )}

        {!isDeposited && status === 'idle' && (
          <button
            onClick={deposit}
            style={{
              padding: '6px 14px', backgroundColor: '#2DD4A8', color: '#fff',
              border: 'none', borderRadius: '6px', fontWeight: 600, cursor: 'pointer', fontSize: '13px',
            }}
          >
            Deposit
          </button>
        )}

        {isProcessing && (
          <span style={{ fontSize: '13px', color: '#2563eb' }}>Processing...</span>
        )}

        {status === 'error' && (
          <div style={{ textAlign: 'right' }}>
            <p style={{ fontSize: '12px', color: '#dc2626' }}>{error}</p>
            <button
              onClick={deposit}
              style={{
                marginTop: '4px', padding: '4px 10px', backgroundColor: '#fee2e2',
                color: '#991b1b', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px',
              }}
            >
              Retry
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

Notification badge

A compact badge showing how many settlements need attention (e.g., awaiting deposits). Useful in navigation bars or sidebars.
// settlement-badge.tsx
import { useSettlements } from '@keystoneos/react';

interface SettlementBadgeProps {
  state: string;
  label: string;
}

export function SettlementBadge({ state, label }: SettlementBadgeProps) {
  const { total, isLoading } = useSettlements({ state, limit: 1, offset: 0 });

  if (isLoading || total === 0) return null;

  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
      <span>{label}</span>
      <span style={{
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        minWidth: '20px', height: '20px', borderRadius: '9999px',
        backgroundColor: '#ef4444', color: '#fff',
        fontSize: '11px', fontWeight: 700, padding: '0 6px',
      }}>
        {total}
      </span>
    </div>
  );
}

// Usage in a navigation bar:
function Sidebar() {
  return (
    <nav>
      <a href="/settlements">
        <SettlementBadge state="AWAITING_DEPOSITS" label="Awaiting Deposits" />
      </a>
      <a href="/settlements?state=COMPLIANCE_CHECKING">
        <SettlementBadge state="COMPLIANCE_CHECKING" label="Compliance Review" />
      </a>
    </nav>
  );
}

Putting it all together

Here is a complete settlement detail page combining the stepper, timeline, and deposit tracker.
// settlement-detail-page.tsx
import { useSettlement, isTerminalState } from '@keystoneos/react';
import { ProgressStepper } from './progress-stepper';
import { EventTimeline } from './event-timeline';
import { DepositTracker } from './deposit-tracker';

interface SettlementDetailPageProps {
  settlementId: string;
}

export function SettlementDetailPage({ settlementId }: SettlementDetailPageProps) {
  const { settlement, isLoading, error } = useSettlement(settlementId);

  if (isLoading) return <div style={{ padding: '24px' }}>Loading settlement...</div>;
  if (error) return <div style={{ padding: '24px', color: '#dc2626' }}>Error: {error}</div>;
  if (!settlement) return <div style={{ padding: '24px' }}>Settlement not found.</div>;

  const isTerminal = isTerminalState(settlement.state);

  return (
    <div style={{ padding: '24px', maxWidth: '900px' }}>
      {/* Header */}
      <div style={{ marginBottom: '24px' }}>
        <h1 style={{ fontSize: '24px', fontWeight: 700 }}>
          {settlement.external_reference ?? `Settlement ${settlement.id.slice(0, 8)}`}
        </h1>
        <p style={{ fontSize: '14px', color: '#6b7280', marginTop: '4px' }}>
          Template: {settlement.template_slug} | Created: {new Date(settlement.created_at).toLocaleString()}
        </p>
      </div>

      {/* Two-column layout */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '32px' }}>
        {/* Left column: Progress + Deposits */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
          <ProgressStepper settlementId={settlementId} />
          <DepositTracker settlementId={settlementId} />

          {/* Parties summary */}
          <div>
            <h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '8px' }}>Parties</h3>
            {settlement.parties.map((party, i) => (
              <div key={i} style={{ padding: '8px 0', borderBottom: '1px solid #f3f4f6' }}>
                <p style={{ fontSize: '14px', fontWeight: 600 }}>
                  {party.name ?? party.external_reference}
                  <span style={{ fontWeight: 400, color: '#6b7280', marginLeft: '8px' }}>
                    ({party.role})
                  </span>
                </p>
                <p style={{ fontSize: '12px', fontFamily: 'monospace', color: '#9ca3af' }}>
                  {party.wallet_address}
                </p>
              </div>
            ))}
          </div>
        </div>

        {/* Right column: Event timeline */}
        <EventTimeline settlementId={settlementId} />
      </div>
    </div>
  );
}

Auto-refresh behavior

The useSettlement hook subscribes to real-time updates automatically:
  • While the settlement is in a non-terminal state, it polls for updates
  • When a state change is detected, the settlement object updates and all components re-render
  • Once the settlement reaches a terminal state (FINALIZED, ROLLED_BACK, TIMED_OUT), polling stops
Use isTerminalState(settlement.state) to conditionally render UI - for example, hiding the deposit tracker once the settlement finalizes.
import { isTerminalState } from '@keystoneos/react';

// Stop showing action buttons when settlement is done
if (isTerminalState(settlement.state)) {
  return <FinalizedView settlement={settlement} />;
}

Next steps

Settlement Dashboard

Build a list view to monitor all settlements.

Custody Integration

Route deposits through Fireblocks or BitGo.

useSettlement Reference

Full API reference for the useSettlement hook.

State Machine

Understand settlement states and transitions.