@@ -485,6 +549,15 @@ export default function TokenDetailPage() {
24h volume: {formatUsd(token.volume_24h)}
Market cap: {formatUsd(token.circulating_market_cap)}
Visible liquidity: {formatUsd(token.liquidity_usd)}
+
+ {aggregationMarketData?.supplyNostroMatch ? (
+
+ {formatTotalSupplyHoBackingLabel(
+ aggregationMarketData.totalSupplyUsd,
+ aggregationMarketData.supplyNostroMatch,
+ )}
+
+ ) : null}
Valuation source: {token.price_source === 'token-aggregation' ? 'live token aggregation' : token.price_source || 'unavailable'}
Market snapshot: {token.market_updated_at ? formatTimestamp(token.market_updated_at) : 'Unavailable'}
{formatUsd(market.priceUsd)}
-
Liquidity
-
{formatUsd(market.liquidityUsd)}
+
Cash & reserve
+
+
+
diff --git a/frontend/src/services/api/tokenAggregation.ts b/frontend/src/services/api/tokenAggregation.ts
index e025f25..ad587c4 100644
--- a/frontend/src/services/api/tokenAggregation.ts
+++ b/frontend/src/services/api/tokenAggregation.ts
@@ -1,14 +1,66 @@
import { resolveExplorerApiBase } from '../../../libs/frontend-api-client/api-base'
+export type TokenAggregationHoReserveCoverageStatus = 'sufficient' | 'insufficient' | 'pending' | 'unavailable'
+
+export interface TokenAggregationHoReserveCoverage {
+ status: TokenAggregationHoReserveCoverageStatus
+ multiplier: number
+ headOfficeId: number
+ reserveAssetGlCodes: string[]
+ reserveAssetsUsd: number
+ issuedCStarUsd: number
+ requiredReserveUsd: number
+ coverageRatio: number
+ formula: string
+ policyId: string
+ policyRef: string
+ generatedAt: string
+ tokenCount?: number
+ error?: string
+}
+
export interface TokenAggregationMarketSnapshot {
priceUsd?: number
volume24h?: number
liquidityUsd?: number
+ cashLiquidityUsd?: number
+ cashInBankUsd?: number
+ cashInBankAudit?: TokenAggregationCashInBankAudit
+ totalSupplyUsd?: number
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch
+ hoReserveCoverage?: TokenAggregationHoReserveCoverage
lastUpdated?: string | null
pricingKind?: 'spot' | 'lp-share'
isCanonicalClone?: boolean
}
+export type TokenAggregationSupplyNostroMatchStatus = 'matched' | 'break' | 'pending' | 'unavailable'
+
+export interface TokenAggregationSupplyNostroMatch {
+ status: TokenAggregationSupplyNostroMatchStatus
+ multiplier: number
+ toleranceBps: number
+ headOfficeId?: number
+ externalNostroGlCode?: string
+ formula?: string
+ totalSupplyUsd?: number
+ externalNostroCashUsd?: number
+ expectedSupplyUsd?: number
+ deltaUsd?: number
+ deltaBps?: number
+}
+
+export type TokenAggregationCashInBankAuditStatus = 'aligned' | 'breaks' | 'pending' | 'unavailable'
+
+export interface TokenAggregationCashInBankAudit {
+ status: TokenAggregationCashInBankAuditStatus
+ breaksCount: number
+ tripleStateAligned?: boolean
+ policyId: string
+ policyRef: string
+ error?: string
+}
+
export interface TokenAggregationTokenSnapshot {
chainId: number
address: string
@@ -79,6 +131,12 @@ interface RawTokenMarketBatchResponse {
decimals?: number | string | null
priceUsd?: number | string | null
liquidityUsd?: number | string | null
+ cashLiquidityUsd?: number | string | null
+ cashInBankUsd?: number | string | null
+ cashInBankAudit?: TokenAggregationCashInBankAudit | null
+ totalSupplyUsd?: number | string | null
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch | null
+ hoReserveCoverage?: TokenAggregationHoReserveCoverage | null
volume24h?: number | string | null
lastUpdated?: string | null
source?: string | null
@@ -99,6 +157,12 @@ interface RawTokenAggregationTokenResponse {
priceUsd?: number | string | null
volume24h?: number | string | null
liquidityUsd?: number | string | null
+ cashLiquidityUsd?: number | string | null
+ cashInBankUsd?: number | string | null
+ cashInBankAudit?: TokenAggregationCashInBankAudit | null
+ totalSupplyUsd?: number | string | null
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch | null
+ hoReserveCoverage?: TokenAggregationHoReserveCoverage | null
lastUpdated?: string | null
} | null
} | null
@@ -179,6 +243,9 @@ function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAgg
decimals,
priceUsd,
)
+ const cashLiquidityUsd = toNumber(token.market?.cashLiquidityUsd)
+ const cashInBankUsd = toNumber(token.market?.cashInBankUsd)
+ const totalSupplyUsd = toNumber(token.market?.totalSupplyUsd)
return {
chainId: toNumber(token.chainId) ?? 138,
@@ -192,6 +259,12 @@ function normalizeTokenSnapshot(raw: RawTokenAggregationTokenResponse): TokenAgg
priceUsd,
volume24h: toNumber(token.market.volume24h),
liquidityUsd,
+ cashLiquidityUsd,
+ cashInBankUsd,
+ cashInBankAudit: token.market.cashInBankAudit || undefined,
+ totalSupplyUsd,
+ supplyNostroMatch: token.market.supplyNostroMatch || undefined,
+ hoReserveCoverage: token.market.hoReserveCoverage || undefined,
lastUpdated: token.market.lastUpdated || null,
}
: null,
@@ -263,16 +336,25 @@ export const tokenAggregationApi = {
if (!snapshot.address) return []
const priceUsd = toNumber(snapshot.priceUsd)
const liquidityUsd = toNumber(snapshot.liquidityUsd)
+ const cashLiquidityUsd = toNumber(snapshot.cashLiquidityUsd)
+ const cashInBankUsd = toNumber(snapshot.cashInBankUsd)
+ const totalSupplyUsd = toNumber(snapshot.totalSupplyUsd)
const entry: TokenAggregationTokenSnapshot = {
chainId,
address: snapshot.address,
name: snapshot.name || undefined,
symbol: snapshot.symbol || undefined,
decimals: toNumber(snapshot.decimals),
- market: priceUsd != null || liquidityUsd != null || snapshot.pricingKind
+ market: priceUsd != null || liquidityUsd != null || cashLiquidityUsd != null || cashInBankUsd != null || totalSupplyUsd != null || snapshot.hoReserveCoverage || snapshot.pricingKind
? {
priceUsd,
liquidityUsd,
+ cashLiquidityUsd,
+ cashInBankUsd,
+ cashInBankAudit: snapshot.cashInBankAudit || undefined,
+ totalSupplyUsd,
+ supplyNostroMatch: snapshot.supplyNostroMatch || undefined,
+ hoReserveCoverage: snapshot.hoReserveCoverage || undefined,
volume24h: toNumber(snapshot.volume24h),
lastUpdated: snapshot.lastUpdated || null,
pricingKind: snapshot.pricingKind || undefined,
diff --git a/frontend/src/utils/tokenMarket.ts b/frontend/src/utils/tokenMarket.ts
index 45238cf..b7c4b4b 100644
--- a/frontend/src/utils/tokenMarket.ts
+++ b/frontend/src/utils/tokenMarket.ts
@@ -1,14 +1,36 @@
import type { AddressTokenBalance } from '@/services/api/addresses'
-import type { TokenAggregationMarketSnapshot, TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
+import type { TokenAggregationCashInBankAudit, TokenAggregationHoReserveCoverage, TokenAggregationMarketSnapshot, TokenAggregationSupplyNostroMatch, TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
export interface ResolvedTokenMarket {
priceUsd?: number
liquidityUsd?: number
+ cashLiquidityUsd?: number
+ cashInBankUsd?: number
+ cashInBankAudit?: TokenAggregationCashInBankAudit
+ totalSupplyUsd?: number
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch
+ hoReserveCoverage?: TokenAggregationHoReserveCoverage
+ lastUpdated?: string | null
source?: 'token-aggregation' | 'blockscout' | 'derived'
pricingKind?: 'spot' | 'lp-share'
isCanonicalClone?: boolean
}
+/** Wallet liquidity stack + HO audit freshness (API market or resolved balance). */
+export type TokenLiquidityMarketView = Pick<
+ TokenAggregationMarketSnapshot,
+ | 'cashLiquidityUsd'
+ | 'cashInBankUsd'
+ | 'cashInBankAudit'
+ | 'supplyNostroMatch'
+ | 'hoReserveCoverage'
+ | 'lastUpdated'
+>
+
+function normalizeLastUpdated(value: string | null | undefined): string | undefined {
+ return value ?? undefined
+}
+
export function isLikelyLpReceiptSymbol(symbol: string | undefined): boolean {
if (!symbol) return false
const upper = symbol.toUpperCase()
@@ -63,6 +85,13 @@ export function resolveTokenMarketForBalance(
return {
priceUsd: aggregationPrice,
liquidityUsd: aggregationMarket?.liquidityUsd,
+ cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd,
+ cashInBankUsd: aggregationMarket?.cashInBankUsd,
+ cashInBankAudit: aggregationMarket?.cashInBankAudit,
+ totalSupplyUsd: aggregationMarket?.totalSupplyUsd,
+ supplyNostroMatch: aggregationMarket?.supplyNostroMatch,
+ hoReserveCoverage: aggregationMarket?.hoReserveCoverage,
+ lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt),
source: 'token-aggregation',
pricingKind,
isCanonicalClone,
@@ -73,6 +102,13 @@ export function resolveTokenMarketForBalance(
return {
priceUsd: blockscoutPrice,
liquidityUsd: aggregationMarket?.liquidityUsd,
+ cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd,
+ cashInBankUsd: aggregationMarket?.cashInBankUsd,
+ cashInBankAudit: aggregationMarket?.cashInBankAudit,
+ totalSupplyUsd: aggregationMarket?.totalSupplyUsd,
+ supplyNostroMatch: aggregationMarket?.supplyNostroMatch,
+ hoReserveCoverage: aggregationMarket?.hoReserveCoverage,
+ lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt),
source: 'blockscout',
pricingKind,
isCanonicalClone,
@@ -82,12 +118,146 @@ export function resolveTokenMarketForBalance(
return {
priceUsd: undefined,
liquidityUsd: aggregationMarket?.liquidityUsd,
+ cashLiquidityUsd: aggregationMarket?.cashLiquidityUsd,
+ cashInBankUsd: aggregationMarket?.cashInBankUsd,
+ cashInBankAudit: aggregationMarket?.cashInBankAudit,
+ totalSupplyUsd: aggregationMarket?.totalSupplyUsd,
+ supplyNostroMatch: aggregationMarket?.supplyNostroMatch,
+ hoReserveCoverage: aggregationMarket?.hoReserveCoverage,
+ lastUpdated: normalizeLastUpdated(aggregationMarket?.lastUpdated ?? aggregationMarket?.hoReserveCoverage?.generatedAt),
source: 'derived',
pricingKind,
isCanonicalClone,
}
}
+/** Public-facing cash pool depth — never fall back to full DEX pool TVL. */
+export function resolveDisplayedCashLiquidityUsd(
+ market: Pick,
+): number | undefined {
+ const value = market.cashLiquidityUsd
+ return value != null && value > 0 ? value : undefined
+}
+
+export function formatCashLiquidityLabel(value: number | undefined): string {
+ if (value == null) return 'Cash liq. unavailable'
+ return formatUsd(value)
+}
+
+export function resolveDisplayedCashInBankUsd(
+ market: Pick,
+): number | undefined {
+ const value = market.cashInBankUsd
+ return value != null && value > 0 ? value : undefined
+}
+
+export function formatCashInBankAuditLabel(
+ audit: TokenAggregationCashInBankAudit | undefined,
+): string {
+ if (!audit) return 'audit pending'
+ switch (audit.status) {
+ case 'aligned':
+ return 'audit aligned'
+ case 'breaks':
+ return audit.breaksCount > 0 ? `audit breaks (${audit.breaksCount})` : 'audit breaks'
+ case 'pending':
+ return 'audit pending'
+ case 'unavailable':
+ return 'audit unavailable'
+ default:
+ return 'audit pending'
+ }
+}
+
+export function formatSupplyNostroMatchLabel(
+ match: TokenAggregationSupplyNostroMatch | undefined,
+): string {
+ if (!match) return 'supply match pending'
+ const ho = match.headOfficeId ?? 1
+ switch (match.status) {
+ case 'matched':
+ return `HO-${ho} supply matched (${match.multiplier}× nostro)`
+ case 'break':
+ return `HO-${ho} supply break (${match.multiplier}× rule)`
+ case 'pending':
+ return 'supply match pending'
+ case 'unavailable':
+ return 'supply match unavailable'
+ default:
+ return 'supply match pending'
+ }
+}
+
+/** Explicit Total supply vs 1.2× HO cash-in-bank backing line. */
+export function formatTotalSupplyHoBackingLabel(
+ totalSupplyUsd: number | undefined,
+ match: TokenAggregationSupplyNostroMatch | undefined,
+): string {
+ if (totalSupplyUsd == null) return 'Total supply USD unavailable'
+ if (!match || match.expectedSupplyUsd == null) {
+ return `Total supply ${formatUsd(totalSupplyUsd)} · HO backing check pending`
+ }
+ const ho = match.headOfficeId ?? 1
+ const mult = match.multiplier ?? 1.2
+ const status =
+ match.status === 'matched' ? 'matched' : match.status === 'break' ? 'break' : match.status
+ return `Total supply ${formatUsd(totalSupplyUsd)} · expected ${formatUsd(match.expectedSupplyUsd)} (${mult}× HO-${ho} nostro ${formatUsd(match.externalNostroCashUsd ?? 0)}) · ${status}`
+}
+
+export function formatHoReserveCoverageLabel(
+ coverage: TokenAggregationHoReserveCoverage | undefined,
+): string {
+ if (!coverage || !(coverage.coverageRatio > 0)) {
+ return coverage?.status === 'pending' ? 'pending' : 'unavailable'
+ }
+ const mult = coverage.multiplier ?? 1.2
+ const ratio = coverage.coverageRatio.toFixed(2)
+ const gl = coverage.reserveAssetGlCodes?.join('+') ?? '1000+1050'
+ const statusSuffix =
+ coverage.status === 'sufficient'
+ ? ''
+ : coverage.status === 'insufficient'
+ ? ' · insufficient'
+ : ` · ${coverage.status}`
+ return `${ratio}× (${mult}× rule · HO GL ${gl})${statusSuffix}`
+}
+
+/** Fineract audit freshness for HO nostro / reserve lines. */
+export function resolveHoLiquidityGeneratedAt(
+ market: Pick,
+): string | undefined {
+ const candidates = [
+ market.hoReserveCoverage?.generatedAt,
+ normalizeLastUpdated(market.lastUpdated),
+ ].filter(Boolean) as string[]
+ if (candidates.length === 0) return undefined
+ return candidates.sort().reverse()[0]
+}
+
+export function formatHoLiquidityAuditFreshness(
+ market: Pick,
+): string {
+ const at = resolveHoLiquidityGeneratedAt(market)
+ if (!at) return 'Fineract audit pending'
+ const ageMs = Date.now() - new Date(at).getTime()
+ if (!Number.isFinite(ageMs) || ageMs < 0) return 'Fineract audit pending'
+ const ageMin = Math.round(ageMs / 60_000)
+ if (ageMin < 1) return 'Fineract audit · just now'
+ if (ageMin < 60) return `Fineract audit · ${ageMin}m ago`
+ const ageH = Math.round(ageMin / 60)
+ return `Fineract audit · ${ageH}h ago`
+}
+
+export function formatCashInBankLabel(
+ value: number | undefined,
+ audit: TokenAggregationCashInBankAudit | undefined,
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch,
+): string {
+ if (value == null) return 'Cash-in-bank unavailable'
+ const supplyLabel = formatSupplyNostroMatchLabel(supplyNostroMatch)
+ return `${formatUsd(value)} (${formatCashInBankAuditLabel(audit)} · ${supplyLabel})`
+}
+
export function estimateTokenBalanceUsd(
rawAmount: string,
decimals: number,
diff --git a/frontend/src/utils/walletFundedTokenListing.ts b/frontend/src/utils/walletFundedTokenListing.ts
index 4c65186..b1bf3c6 100644
--- a/frontend/src/utils/walletFundedTokenListing.ts
+++ b/frontend/src/utils/walletFundedTokenListing.ts
@@ -1,5 +1,5 @@
import { getNativeAssetMarketSafe, estimateNativeUsdValue } from '@/services/api/nativeAssetPricing'
-import { tokenAggregationApi, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
+import { tokenAggregationApi, type TokenAggregationCashInBankAudit, type TokenAggregationHoReserveCoverage, type TokenAggregationSupplyNostroMatch, type TokenAggregationTokenSnapshot } from '@/services/api/tokenAggregation'
import { formatTokenAmount } from '@/utils/format'
import { estimateTokenBalanceUsd } from '@/utils/tokenMarket'
import {
@@ -33,6 +33,11 @@ export type FundedWalletTokenRow = {
priceUsd?: number
balanceUsd?: number
liquidityUsd?: number
+ cashLiquidityUsd?: number
+ cashInBankUsd?: number
+ cashInBankAudit?: TokenAggregationCashInBankAudit
+ supplyNostroMatch?: TokenAggregationSupplyNostroMatch
+ hoReserveCoverage?: TokenAggregationHoReserveCoverage
pricingKind?: string
marketSource?: string
metaMaskAddable: boolean
@@ -147,6 +152,11 @@ export async function enrichFundedWalletTokenRowsWithMarket(
priceUsd: nativePriceUsd,
balanceUsd: balanceUsdText != null ? Number(balanceUsdText) : undefined,
liquidityUsd: nativeMarket.data?.market?.liquidityUsd,
+ cashLiquidityUsd: nativeMarket.data?.market?.cashLiquidityUsd,
+ cashInBankUsd: nativeMarket.data?.market?.cashInBankUsd,
+ cashInBankAudit: nativeMarket.data?.market?.cashInBankAudit,
+ supplyNostroMatch: nativeMarket.data?.market?.supplyNostroMatch,
+ hoReserveCoverage: nativeMarket.data?.market?.hoReserveCoverage,
pricingKind: nativeMarket.data?.market?.pricingKind,
marketSource: nativeMarket.ok && nativeMarket.data?.market ? 'token-aggregation' : undefined,
}
@@ -161,6 +171,11 @@ export async function enrichFundedWalletTokenRowsWithMarket(
priceUsd,
balanceUsd,
liquidityUsd: market?.liquidityUsd,
+ cashLiquidityUsd: market?.cashLiquidityUsd,
+ cashInBankUsd: market?.cashInBankUsd,
+ cashInBankAudit: market?.cashInBankAudit,
+ supplyNostroMatch: market?.supplyNostroMatch,
+ hoReserveCoverage: market?.hoReserveCoverage,
pricingKind: market?.pricingKind,
marketSource: market ? 'token-aggregation' : undefined,
}