Markets & tokens
One dialect for market ids, one alias layer for asset names, and the placement grid that mirrors the engine's rejection rules.
Markets
src/lib/tokens/markets.ts seeds the tradable perps:
| Market | Max leverage | szDecimals | Quote |
|---|---|---|---|
BTC-USDCX | 40× | 5 | USDCx |
ETH-USDCX | 25× | 4 | USDCx |
SOL-USDCX | 20× | 2 | USDCx |
MARKETS is a seed, not the truth. Route-loader slug validation is synchronous, so the list
must exist before any fetch can complete. GET /v1/markets (useMarketConfig) then overwrites
maxLeverage, szDecimals, minNotional, fees, maintMarginFraction and
fundingIntervalSeconds, and records enabledMarkets.
A seeded market the engine does not report disappears from the pair selector — better than routing a user into a market that 400s.
One dialect
id — "BTC-USDCX" — is the engine symbol, and it flows unchanged through:
/trade/:marketSlug(lowercased for the URL only)?market=on public readsmarkets=on the WebSocket filter- every
/v1/users/:party/*read
marketLabel(m); // "BTC / USDCx" — display only
slugOf(m); // "btc-usdcx" — URL only
findMarketBySlug(s); // case-insensitive, used by the route loader
findMarket(id); // by engine symbol
DEFAULT_MARKET;Token registry & the alias layer
src/lib/tokens/registry.ts owns display metadata. Keys form a literal union:
type TokenKey = 'CC' | 'CBTC' | 'CETH' | 'CSOL' | 'USDCX';The engine names its oracle assets BTC / ETH / SOL; the registry keys the Canton wrappers.
The quote arrives as USDCX or USDC. One alias layer reconciles all of it:
resolveTokenKey('BTC'); // → 'CBTC'
resolveTokenKey('USDC'); // → 'USDCX'
assetLogo('ETH'); // → logo path, cBTC fallback so no broken <img>Doing this in one place matters more than it looks. Ad-hoc symbol === 'BTC' ? … checks
scattered across components is how half the app ends up showing the wrong icon for a market the
other half renders correctly.
The placement grid
The engine rejects, never rounds — and rejections arrive as HTTP 200 with ok: false:
| Code | Meaning |
|---|---|
invalid_size | Size is not an exact multiple of 10^-szDecimals |
invalid_price | Price violates the significant-figure / decimal bound |
below_min_notional (0x0C) | price × size under minNotional on an opening leg |
So src/lib/market-grid.ts mirrors the rules client-side:
Size — exact multiple of 10^-szDecimals. Floor onto the grid before deriving any margin or
fee figure, or the preview disagrees with the fill.
Price — integer, or ≤5 significant figures and ≤6 - szDecimals decimals.
Notional — price × size ≥ minNotional, on the opening leg only. A reduce is exempt.
Never gate a Close on minNotional. Reduce-only orders are exempt engine-side; enforcing it
in the UI means a user with a small residual position cannot close it.
With szDecimals unknown there is no client-side grid at all. Guessing one number loosens
the size bound while tightening the price bound — worse than not validating, because it rejects
valid orders locally and lets invalid ones through.
UI helpers
<TokenIcon symbol="CBTC" />
<MarketIconPair market={market} /> // two overlapping icons, quote offset 0.62×