Add a market
Four places to touch, in the order that avoids a market that routes but 400s.
Adding a perp touches the oracle, the engine, the BFF (implicitly), and the interface. Doing it in the wrong order produces a market users can navigate to but not trade.
Order
Oracle — get a price first
Add the asset with each venue's symbol for it. Venues disagree: BTCUSDT / BTC-USD /
BTC-USDT.
cd server/oracle
npm run onceAn asset printing "m":"na" means no venue delivered a usable frame. Stop here if so —
without an index price the engine cannot compute mark, funding, or liquidation.
Engine — markets.json
{
"AVAX-USDCX": {
"maxLeverage": 20,
"szDecimals": 2,
"minNotional": "10",
"maintMarginFraction": "0.05",
"fundingIntervalSeconds": 3600
}
}szDecimals is the size grid exponent and it also bounds price precision
(≤ 6 - szDecimals decimals). Pick it from the asset's realistic tick size, not arbitrarily.
Restart the engine.
Verify the BFF sees it
curl -s http://localhost:4000/v1/markets | jq '.[] | select(.id=="AVAX-USDCX")'Nothing to change here — the BFF reads the engine's config.
Interface — registry, then seed
Token registry (src/lib/tokens/registry.ts) — add the key, name, logo, decimals, plus any
alias:
CAVAX: { symbol: 'cAVAX', name: 'Canton AVAX', decimals: 8, logo: '/tokens/avax.svg' },
// alias layer: the engine calls it "AVAX"
TOKEN_ALIASES.AVAX = 'CAVAX';Market seed (src/lib/tokens/markets.ts):
{ id: 'AVAX-USDCX', base: 'CAVAX', quote: 'USDCX', maxLeverage: 20, szDecimals: 2 }Why the interface needs a seed at all
Route-loader slug validation is synchronous — it runs before any fetch can complete. So
MARKETS must exist statically for /trade/avax-usdcx to validate.
GET /v1/markets 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. That is deliberate: better an absent market than one that routes cleanly and then 400s on every order.
The corollary — if you add the seed but forget the engine config, the market simply will not appear, and nothing will tell you why.
Checklist
- Oracle prints a real price for the asset (
npm run once). -
markets.jsonhas the market; engine restarted. -
GET /v1/marketsreports it. - Token registry has the key, the logo file exists, and the engine alias resolves.
- Market seed added with matching
szDecimals. -
/trade/<slug>loads and the pair selector lists it. - A grid-valid order places; a deliberately off-grid one is rejected with
invalid_size.
Removing a market
Remove it from markets.json and restart the engine. The interface drops it from the pair
selector automatically via enabledMarkets — the seed entry can stay until the next cleanup
without harming anything.