diff --git a/app/(dashboard)/escrow/[id]/settlement/page.tsx b/app/(dashboard)/escrow/[id]/settlement/page.tsx
new file mode 100644
index 00000000..557975ed
--- /dev/null
+++ b/app/(dashboard)/escrow/[id]/settlement/page.tsx
@@ -0,0 +1,15 @@
+import { SettlementConfirmation } from '@/components/escrow/SettlementConfirmation';
+
+export default async function EscrowSettlementPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+
+ return (
+
+
+
+ );
+}
diff --git a/hooks/__tests__/fixtures/settlementApiResponses.ts b/hooks/__tests__/fixtures/settlementApiResponses.ts
new file mode 100644
index 00000000..e28312ef
--- /dev/null
+++ b/hooks/__tests__/fixtures/settlementApiResponses.ts
@@ -0,0 +1,59 @@
+import type { SettlementBreakdown } from '@/types/settlementBreakdown';
+
+/**
+ * Recorded response shapes from GET /api/escrow/{id}/settlement, used to
+ * stub the service layer in hook and component tests.
+ */
+
+export const ESCROW_ID = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC';
+export const SETTLEMENT_TX_HASH = '3f9a1c7e5b2d4a6f8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d9f2a4c6e8b0d8b20e4';
+
+export const confirmedSettlementResponse: SettlementBreakdown = {
+ escrowId: ESCROW_ID,
+ status: 'confirmed',
+ asset: 'XLM',
+ network: 'testnet',
+ totalEscrowAmount: 1250,
+ driverPayout: 1134.375,
+ platformFee: 31.25,
+ platformFeeRate: 0.025,
+ taxWithholdings: [
+ { code: 'vat', label: 'VAT on platform fee', rate: 0.075, amount: 2.34375 },
+ { code: 'wht', label: 'Withholding tax', rate: 0.05, amount: 57.03125 },
+ ],
+ heldAmount: 25,
+ heldReason: 'Dispute window open for 24 hours',
+ remainingBalance: 0,
+ transactionHash: SETTLEMENT_TX_HASH,
+ ledger: 51234567,
+ settledAt: '2026-09-28T14:32:10Z',
+ delivery: {
+ trackingNumber: 'SWC-2026-004821',
+ origin: 'Lagos, NG',
+ destination: 'Accra, GH',
+ deliveredAt: '2026-09-28T13:58:02Z',
+ },
+};
+
+export const finalizingSettlementResponse: SettlementBreakdown = {
+ ...confirmedSettlementResponse,
+ status: 'finalizing',
+ transactionHash: SETTLEMENT_TX_HASH,
+ ledger: null,
+ settledAt: null,
+};
+
+export const pendingSettlementResponse: SettlementBreakdown = {
+ ...confirmedSettlementResponse,
+ status: 'pending',
+ transactionHash: null,
+ ledger: null,
+ settledAt: null,
+};
+
+export const failedSettlementResponse: SettlementBreakdown = {
+ ...confirmedSettlementResponse,
+ status: 'failed',
+ ledger: null,
+ settledAt: null,
+};
diff --git a/lib/__tests__/settlementFormatters.test.ts b/lib/__tests__/settlementFormatters.test.ts
new file mode 100644
index 00000000..66b2e59f
--- /dev/null
+++ b/lib/__tests__/settlementFormatters.test.ts
@@ -0,0 +1,54 @@
+import {
+ formatAssetAmount,
+ formatRate,
+ formatSettlementDate,
+ getStellarExplorerTxUrl,
+ truncateHash,
+} from '@/lib/settlementFormatters';
+
+describe('settlementFormatters', () => {
+ describe('formatAssetAmount', () => {
+ it('shows at least two decimals with the asset code', () => {
+ expect(formatAssetAmount(1250, 'XLM')).toBe('1,250.00 XLM');
+ });
+
+ it('keeps Stellar 7-decimal precision', () => {
+ expect(formatAssetAmount(0.1234567, 'USDC')).toBe('0.1234567 USDC');
+ expect(formatAssetAmount(0.123456789, 'XLM')).toBe('0.1234568 XLM');
+ });
+ });
+
+ describe('formatRate', () => {
+ it('formats fractional rates as percentages', () => {
+ expect(formatRate(0.025)).toBe('2.5%');
+ expect(formatRate(0.1)).toBe('10%');
+ });
+ });
+
+ describe('getStellarExplorerTxUrl', () => {
+ it('links to the matching network on stellar.expert', () => {
+ expect(getStellarExplorerTxUrl('abc123', 'public')).toBe(
+ 'https://stellar.expert/explorer/public/tx/abc123',
+ );
+ expect(getStellarExplorerTxUrl('abc123', 'testnet')).toBe(
+ 'https://stellar.expert/explorer/testnet/tx/abc123',
+ );
+ });
+ });
+
+ describe('truncateHash', () => {
+ it('keeps the start and end of long hashes', () => {
+ expect(truncateHash('3f9a1c7e5b2d4a6f8c0e8b20e4')).toBe('3f9a1c…8b20e4');
+ });
+
+ it('returns short hashes unchanged', () => {
+ expect(truncateHash('abc123')).toBe('abc123');
+ });
+ });
+
+ describe('formatSettlementDate', () => {
+ it('formats ISO timestamps in UTC', () => {
+ expect(formatSettlementDate('2026-09-28T14:32:10Z')).toBe('Sep 28, 2026, 2:32 PM UTC');
+ });
+ });
+});
diff --git a/lib/settlementFormatters.ts b/lib/settlementFormatters.ts
new file mode 100644
index 00000000..e180f78a
--- /dev/null
+++ b/lib/settlementFormatters.ts
@@ -0,0 +1,45 @@
+import type { StellarNetwork } from '@/types/settlementBreakdown';
+
+const STELLAR_EXPERT_BASE_URL = 'https://stellar.expert/explorer';
+
+/**
+ * Formats an on-chain asset amount, keeping up to 7 decimal places (the
+ * precision of Stellar amounts) while always showing at least 2.
+ */
+export function formatAssetAmount(amount: number, asset: string): string {
+ const formatted = new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 7,
+ }).format(amount);
+ return `${formatted} ${asset}`;
+}
+
+/** Formats a fractional rate (0.025) as a percentage string ("2.5%"). */
+export function formatRate(rate: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'percent',
+ maximumFractionDigits: 2,
+ }).format(rate);
+}
+
+export function getStellarExplorerTxUrl(hash: string, network: StellarNetwork): string {
+ return `${STELLAR_EXPERT_BASE_URL}/${network}/tx/${encodeURIComponent(hash)}`;
+}
+
+/** Shortens a transaction hash for display, e.g. "3f9a1c…8b20e4". */
+export function truncateHash(hash: string, visible = 6): string {
+ if (hash.length <= visible * 2 + 1) return hash;
+ return `${hash.slice(0, visible)}…${hash.slice(-visible)}`;
+}
+
+export function formatSettlementDate(iso: string): string {
+ return new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ timeZone: 'UTC',
+ timeZoneName: 'short',
+ }).format(new Date(iso));
+}
diff --git a/services/__tests__/escrowSettlementService.test.ts b/services/__tests__/escrowSettlementService.test.ts
new file mode 100644
index 00000000..d4dd8e46
--- /dev/null
+++ b/services/__tests__/escrowSettlementService.test.ts
@@ -0,0 +1,105 @@
+import { AxiosError, AxiosHeaders } from 'axios';
+import { apiClient } from '@/services/api';
+import {
+ escrowSettlementService,
+ SettlementServiceError,
+} from '@/services/escrowSettlementService';
+import {
+ ESCROW_ID,
+ confirmedSettlementResponse,
+} from '@/hooks/__tests__/fixtures/settlementApiResponses';
+
+jest.mock('@/services/api', () => ({
+ apiClient: { get: jest.fn() },
+}));
+
+const mockGet = apiClient.get as jest.MockedFunction;
+
+function axiosErrorWith(status: number, data?: unknown): AxiosError {
+ const headers = new AxiosHeaders();
+ return new AxiosError('Request failed', 'ERR_BAD_RESPONSE', { headers }, null, {
+ status,
+ statusText: '',
+ headers: {},
+ config: { headers },
+ data,
+ });
+}
+
+describe('escrowSettlementService', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('getSettlement', () => {
+ it('requests the settlement endpoint for the escrow', async () => {
+ mockGet.mockResolvedValue({ data: confirmedSettlementResponse });
+ const controller = new AbortController();
+
+ const result = await escrowSettlementService.getSettlement(ESCROW_ID, controller.signal);
+
+ expect(mockGet).toHaveBeenCalledWith(`/escrow/${ESCROW_ID}/settlement`, {
+ signal: controller.signal,
+ });
+ expect(result).toEqual(confirmedSettlementResponse);
+ });
+
+ it('encodes the escrow id in the path', async () => {
+ mockGet.mockResolvedValue({ data: confirmedSettlementResponse });
+
+ await escrowSettlementService.getSettlement('esc/1 2');
+
+ expect(mockGet).toHaveBeenCalledWith('/escrow/esc%2F1%202/settlement', {
+ signal: undefined,
+ });
+ });
+
+ it('uses the backend error message when provided', async () => {
+ mockGet.mockRejectedValue(axiosErrorWith(422, { message: 'Escrow not yet released' }));
+
+ await expect(escrowSettlementService.getSettlement(ESCROW_ID)).rejects.toEqual(
+ expect.objectContaining({ message: 'Escrow not yet released', status: 422 }),
+ );
+ });
+
+ it('maps 404 responses to a not-found error', async () => {
+ mockGet.mockRejectedValue(axiosErrorWith(404));
+
+ const error = await escrowSettlementService.getSettlement(ESCROW_ID).catch((e) => e);
+
+ expect(error).toBeInstanceOf(SettlementServiceError);
+ expect(error.message).toBe('Settlement not found for this escrow.');
+ expect(error.status).toBe(404);
+ });
+
+ it('maps other failures to a generic error', async () => {
+ mockGet.mockRejectedValue(axiosErrorWith(500));
+
+ await expect(escrowSettlementService.getSettlement(ESCROW_ID)).rejects.toEqual(
+ expect.objectContaining({
+ message: 'Unable to load settlement details. Please try again.',
+ status: 500,
+ }),
+ );
+ });
+
+ it('wraps non-axios errors', async () => {
+ mockGet.mockRejectedValue(new Error('Network down'));
+
+ await expect(escrowSettlementService.getSettlement(ESCROW_ID)).rejects.toEqual(
+ expect.objectContaining({ message: 'Network down', status: null }),
+ );
+ });
+ });
+
+ describe('isTerminalStatus', () => {
+ it.each([
+ ['confirmed', true],
+ ['failed', true],
+ ['pending', false],
+ ['finalizing', false],
+ ] as const)('treats %s as terminal=%s', (status, expected) => {
+ expect(escrowSettlementService.isTerminalStatus(status)).toBe(expected);
+ });
+ });
+});
diff --git a/services/escrowSettlementService.ts b/services/escrowSettlementService.ts
new file mode 100644
index 00000000..6b0749ba
--- /dev/null
+++ b/services/escrowSettlementService.ts
@@ -0,0 +1,53 @@
+import { isAxiosError } from 'axios';
+import { apiClient } from '@/services/api';
+import type { SettlementBreakdown, SettlementStatus } from '@/types/settlementBreakdown';
+
+const TERMINAL_STATUSES: ReadonlySet = new Set(['confirmed', 'failed']);
+
+export class SettlementServiceError extends Error {
+ readonly status: number | null;
+
+ constructor(message: string, status: number | null) {
+ super(message);
+ this.name = 'SettlementServiceError';
+ this.status = status;
+ }
+}
+
+function toServiceError(error: unknown): SettlementServiceError {
+ if (isAxiosError<{ message?: string }>(error)) {
+ const status = error.response?.status ?? null;
+ const message =
+ error.response?.data?.message ??
+ (status === 404
+ ? 'Settlement not found for this escrow.'
+ : 'Unable to load settlement details. Please try again.');
+ return new SettlementServiceError(message, status);
+ }
+ if (error instanceof Error) {
+ return new SettlementServiceError(error.message, null);
+ }
+ return new SettlementServiceError('Unable to load settlement details. Please try again.', null);
+}
+
+/**
+ * escrowSettlementService — post-release settlement API communication.
+ * Hooks call this; components never call this directly.
+ */
+export const escrowSettlementService = {
+ async getSettlement(escrowId: string, signal?: AbortSignal): Promise {
+ try {
+ const { data } = await apiClient.get(
+ `/escrow/${encodeURIComponent(escrowId)}/settlement`,
+ { signal },
+ );
+ return data;
+ } catch (error) {
+ throw toServiceError(error);
+ }
+ },
+
+ isTerminalStatus(status: SettlementStatus): boolean {
+ return TERMINAL_STATUSES.has(status);
+ },
+};
diff --git a/types/settlementBreakdown.ts b/types/settlementBreakdown.ts
new file mode 100644
index 00000000..3c93f359
--- /dev/null
+++ b/types/settlementBreakdown.ts
@@ -0,0 +1,73 @@
+/**
+ * Types for the post-delivery settlement breakdown returned by
+ * GET /api/escrow/{id}/settlement.
+ *
+ * All monetary amounts are expressed in units of `asset` (e.g. XLM, USDC)
+ * and mirror the values emitted by the escrow contract's settlement event.
+ */
+
+/**
+ * Lifecycle of a settlement after escrow release.
+ * - `pending` — release requested, transaction not yet submitted
+ * - `finalizing` — transaction submitted, awaiting ledger confirmation
+ * - `confirmed` — transaction included in a closed ledger (terminal)
+ * - `failed` — transaction rejected or expired (terminal)
+ */
+export type SettlementStatus = 'pending' | 'finalizing' | 'confirmed' | 'failed';
+
+export type StellarNetwork = 'public' | 'testnet';
+
+export interface SettlementTaxWithholding {
+ /** Stable identifier for the withholding, e.g. `vat` or `wht`. */
+ code: string;
+ label: string;
+ /** Rate applied as a fraction, e.g. 0.075 for 7.5%. */
+ rate: number;
+ amount: number;
+}
+
+export interface SettlementDeliverySummary {
+ trackingNumber: string;
+ origin: string;
+ destination: string;
+ deliveredAt: string | null;
+}
+
+export interface SettlementBreakdown {
+ escrowId: string;
+ status: SettlementStatus;
+ asset: string;
+ network: StellarNetwork;
+ /** Total amount that was locked in escrow for the delivery. */
+ totalEscrowAmount: number;
+ /** Net amount released to the driver after fees and withholdings. */
+ driverPayout: number;
+ platformFee: number;
+ /** Platform commission as a fraction, e.g. 0.025 for 2.5%. */
+ platformFeeRate: number;
+ taxWithholdings: SettlementTaxWithholding[];
+ /** Amount still held (e.g. pending dispute window or compliance review). */
+ heldAmount: number;
+ heldReason: string | null;
+ /** Balance left in escrow or returned to the shipper after settlement. */
+ remainingBalance: number;
+ transactionHash: string | null;
+ ledger: number | null;
+ settledAt: string | null;
+ delivery: SettlementDeliverySummary;
+}
+
+export interface FormattedTaxWithholding extends SettlementTaxWithholding {
+ formattedAmount: string;
+ formattedRate: string;
+}
+
+export interface FormattedSettlementAmounts {
+ totalEscrowAmount: string;
+ driverPayout: string;
+ platformFee: string;
+ platformFeeRate: string;
+ totalTaxWithheld: string;
+ heldAmount: string;
+ remainingBalance: string;
+}