Skip to main content
This guide walks through building a fully functional settlement dashboard with state filtering, color-coded badges, and pagination using @keystoneos/react hooks.

Prerequisites

  • @keystoneos/react installed
  • KeystoneProvider configured with a valid session token (see Getting Started)
  • A React project with TypeScript

What you will build

A settlement list page with:
  • A table showing reference, state, parties, asset/payment amounts, and creation date
  • Color-coded state badges
  • A dropdown filter for settlement states
  • Previous/next pagination with total count
  • Click-to-detail navigation

Setting up the provider

Before building the dashboard component, make sure your app is wrapped in KeystoneProvider with a session token from your backend. The token needs settlements:read scope at minimum.
// app.tsx
import { useState, useEffect } from 'react';
import { KeystoneProvider } from '@keystoneos/react';
import { SettlementDashboard } from './settlement-dashboard';

function App() {
  const [token, setToken] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/keystone/session', { method: 'POST' })
      .then(r => r.json())
      .then(d => setToken(d.token));
  }, []);

  if (!token) return <div>Loading...</div>;

  return (
    <KeystoneProvider
      sessionToken={token}
      environment="production"
      onTokenExpired={async () => {
        const res = await fetch('/api/keystone/session', { method: 'POST' });
        const data = await res.json();
        return data.token;
      }}
    >
      <SettlementDashboard />
    </KeystoneProvider>
  );
}

Building the state badge

The getStateInfo utility returns display metadata for each settlement state, including a human-readable label and a semantic category you can map to colors.
// state-badge.tsx
import { getStateInfo } from '@keystoneos/react';

const STATE_COLORS: Record<string, { bg: string; text: string }> = {
  active: { bg: '#dbeafe', text: '#1e40af' },
  success: { bg: '#dcfce7', text: '#166534' },
  error: { bg: '#fee2e2', text: '#991b1b' },
  warning: { bg: '#fef3c7', text: '#92400e' },
  neutral: { bg: '#f3f4f6', text: '#374151' },
};

interface StateBadgeProps {
  state: string;
}

export function StateBadge({ state }: StateBadgeProps) {
  const info = getStateInfo(state);
  const colors = STATE_COLORS[info.category] ?? STATE_COLORS.neutral;

  return (
    <span
      style={{
        display: 'inline-block',
        padding: '2px 10px',
        borderRadius: '9999px',
        fontSize: '12px',
        fontWeight: 600,
        backgroundColor: colors.bg,
        color: colors.text,
      }}
    >
      {info.label}
    </span>
  );
}
The getStateInfo function maps states to categories:
StateCategorySuggested Color
INSTRUCTEDneutralGray
COMPLIANCE_CHECKINGactiveBlue
COMPLIANCE_CLEAREDactiveBlue
AWAITING_DEPOSITSwarningAmber
EXECUTING_SWAPactiveBlue
FINALIZEDsuccessGreen
ROLLED_BACKerrorRed
TIMED_OUTerrorRed

Building the state filter

Create a dropdown that filters settlements by state. Passing undefined fetches all states.
// state-filter.tsx
interface StateFilterProps {
  value: string | undefined;
  onChange: (state: string | undefined) => void;
}

const FILTER_OPTIONS = [
  { label: 'All States', value: undefined },
  { label: 'Instructed', value: 'INSTRUCTED' },
  { label: 'Compliance Checking', value: 'COMPLIANCE_CHECKING' },
  { label: 'Awaiting Deposits', value: 'AWAITING_DEPOSITS' },
  { label: 'Finalized', value: 'FINALIZED' },
  { label: 'Rolled Back', value: 'ROLLED_BACK' },
] as const;

export function StateFilter({ value, onChange }: StateFilterProps) {
  return (
    <select
      value={value ?? ''}
      onChange={e => onChange(e.target.value || undefined)}
      style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
    >
      {FILTER_OPTIONS.map(opt => (
        <option key={opt.label} value={opt.value ?? ''}>
          {opt.label}
        </option>
      ))}
    </select>
  );
}

Building pagination controls

// pagination.tsx
interface PaginationProps {
  page: number;
  totalPages: number;
  total: number;
  onPageChange: (page: number) => void;
}

export function Pagination({ page, totalPages, total, onPageChange }: PaginationProps) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginTop: '16px' }}>
      <button
        onClick={() => onPageChange(page - 1)}
        disabled={page === 0}
        style={{ padding: '6px 12px', borderRadius: '6px', cursor: page === 0 ? 'not-allowed' : 'pointer' }}
      >
        Previous
      </button>
      <span style={{ fontSize: '14px', color: '#6b7280' }}>
        Page {page + 1} of {totalPages} ({total} total)
      </span>
      <button
        onClick={() => onPageChange(page + 1)}
        disabled={page >= totalPages - 1}
        style={{ padding: '6px 12px', borderRadius: '6px', cursor: page >= totalPages - 1 ? 'not-allowed' : 'pointer' }}
      >
        Next
      </button>
    </div>
  );
}

The complete dashboard

This is the full working component that ties everything together.
// settlement-dashboard.tsx
import { useState } from 'react';
import { useSettlements, getStateInfo } from '@keystoneos/react';
import { StateBadge } from './state-badge';
import { StateFilter } from './state-filter';
import { Pagination } from './pagination';

const PAGE_SIZE = 20;

export function SettlementDashboard() {
  const [stateFilter, setStateFilter] = useState<string | undefined>(undefined);
  const [page, setPage] = useState(0);

  const { settlements, total, isLoading, error } = useSettlements({
    state: stateFilter,
    limit: PAGE_SIZE,
    offset: page * PAGE_SIZE,
  });

  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));

  // Reset to first page when filter changes
  const handleFilterChange = (state: string | undefined) => {
    setStateFilter(state);
    setPage(0);
  };

  if (error) {
    return <div style={{ color: '#dc2626', padding: '16px' }}>Error: {error}</div>;
  }

  return (
    <div style={{ padding: '24px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
        <h1 style={{ fontSize: '24px', fontWeight: 700 }}>Settlements</h1>
        <StateFilter value={stateFilter} onChange={handleFilterChange} />
      </div>

      {isLoading ? (
        <div style={{ padding: '40px', textAlign: 'center', color: '#6b7280' }}>Loading settlements...</div>
      ) : settlements.length === 0 ? (
        <div style={{ padding: '40px', textAlign: 'center', color: '#6b7280' }}>
          No settlements found{stateFilter ? ` with state "${stateFilter}"` : ''}.
        </div>
      ) : (
        <>
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead>
              <tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
                <th style={{ padding: '12px 8px' }}>Reference</th>
                <th style={{ padding: '12px 8px' }}>State</th>
                <th style={{ padding: '12px 8px' }}>Parties</th>
                <th style={{ padding: '12px 8px', textAlign: 'right' }}>Asset</th>
                <th style={{ padding: '12px 8px', textAlign: 'right' }}>Payment</th>
                <th style={{ padding: '12px 8px' }}>Created</th>
              </tr>
            </thead>
            <tbody>
              {settlements.map(settlement => {
                const assetLeg = settlement.legs.find(l => l.leg_type === 'asset_delivery');
                const paymentLeg = settlement.legs.find(l => l.leg_type === 'payment');

                return (
                  <tr
                    key={settlement.id}
                    onClick={() => window.location.href = `/settlements/${settlement.id}`}
                    style={{ borderBottom: '1px solid #f3f4f6', cursor: 'pointer' }}
                    onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#f9fafb')}
                    onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
                  >
                    <td style={{ padding: '12px 8px', fontFamily: 'monospace', fontSize: '13px' }}>
                      {settlement.external_reference ?? settlement.id.slice(0, 8)}
                    </td>
                    <td style={{ padding: '12px 8px' }}>
                      <StateBadge state={settlement.state} />
                    </td>
                    <td style={{ padding: '12px 8px', fontSize: '14px' }}>
                      {settlement.parties.map(p => p.name ?? p.external_reference).join(' / ')}
                    </td>
                    <td style={{ padding: '12px 8px', textAlign: 'right', fontFamily: 'monospace', fontSize: '13px' }}>
                      {assetLeg ? `${Number(assetLeg.quantity).toLocaleString()} ${assetLeg.instrument_id}` : '-'}
                    </td>
                    <td style={{ padding: '12px 8px', textAlign: 'right', fontFamily: 'monospace', fontSize: '13px' }}>
                      {paymentLeg ? `${Number(paymentLeg.quantity).toLocaleString()} ${paymentLeg.instrument_id}` : '-'}
                    </td>
                    <td style={{ padding: '12px 8px', fontSize: '14px', color: '#6b7280' }}>
                      {new Date(settlement.created_at).toLocaleDateString()}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>

          <Pagination
            page={page}
            totalPages={totalPages}
            total={total}
            onPageChange={setPage}
          />
        </>
      )}
    </div>
  );
}

What the data looks like

Here is an example of what a row renders with realistic data:
ReferenceStatePartiesAssetPaymentCreated
TRADE-2026-0042Awaiting DepositsSecuritize Fund I / Ondo Capital12,000,000 OUSG12,120,000 USDCApr 1, 2026
TRADE-2026-0038FinalizedSecuritize Fund I / Ondo Capital5,000,000 US09311A10075,050,000 USDCMar 28, 2026

Adding click-to-detail navigation

The table rows are already clickable (using window.location.href). In a real application, use your router’s navigation method:
// With React Router
import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();
// In the row onClick:
onClick={() => navigate(`/settlements/${settlement.id}`)}

// With Next.js
import { useRouter } from 'next/navigation';

const router = useRouter();
// In the row onClick:
onClick={() => router.push(`/settlements/${settlement.id}`)}

Next steps

Real-time Tracking

Build a detail view with live state updates and event timelines.

Bilateral Instructions

Create settlements by submitting bilateral instructions.

useSettlements Reference

Full API reference for the useSettlements hook.

State Machine

Understand the settlement state machine and transitions.