Perpetuals
Perps are live on testnet: BTC/USDSO:USDSO and ETH/USDSO:USDSO, backed by
PerpPool CLOBs riding the same shared OrderBook core as spot and binary.
The shape
Market is a three-way discriminated union — SpotMarket | PerpMarket | BinaryMarket, keyed on marketType (guards: isSpotMarket / isPerpMarket /
isBinaryMarket). A PerpMarket is a plain base/quote book plus perp state:
marginBank, initialMarginBps, fundingRate, cumulativeFundingPerUnit,
indexPrice, longOpenInterest, shortOpenInterest.
- Watches and live reads are unchanged. Perp pools emit the same order
events, so
watchMarket(pool),getLiveSpotOrderBook-style depth,getLiveFills,getLiveUserOrders, and the React hooks just work. Funding (FundingUpdated) and open interest (OpenInterestUpdated) stream into the perp market row live. - Margin, not escrow. Collateral (USDso) lives cross-margin in the
MarginBank:
trader.depositMargin/withdrawMarginmove it;trader.placePerpOrder(or plaincreateOrderon a perp symbol) locks margin from that balance — no per-order token approval. - Positions are on-chain reads, not indexed rows:
client.getPerpPosition(marginBank, account, pool)andclient.getMarginAccount(marginBank, account)— or the unifiedexchange.fetchPositions(). Live pricing/funding comes fromclient.getPerpState(pool)/exchange.fetchFundingRate(symbol). - Margin health is a chain read too.
getMarginAccountnow also returns the requirements + status (imReq/mmReq/cmReq/marginStatus, fromMarginBank.getAccountHealth+getMarginStatus);client.getAccountHealth(marginBank, account)is the lighter standalone read when only health matters, andclient.getLiquidationPrice(marginBank, pool, account)estimates the mark at which this pool's move alone would trip maintenance (null when flat).MarginStatus/MARGIN_STATUS/AccountHealthare exported;exchange.fetchPositions()now fillsUnifiedPosition.liquidationPrice.
History (indexer)
The CURRENT position/margin is the chain read above; the append-only history the chain doesn't expose is indexed (perp account plane), all one-shot indexer reads:
const funding = await client.getFundingPayments(account, { pool, limit: 50 }); // signed funding paid/received
const margin = await client.getMarginEvents(account, { limit: 50 }); // deposit/withdraw/lock/unlock
const liqs = await client.getLiquidations({ account, pool }); // liquidation events
const rates = await client.getFundingRateHistory(pool); // per-pool funding-rate series
const oi = await client.getOpenInterestHistory(pool); // per-pool open-interest series
getFundingRateHistory / getOpenInterestHistory are the append-only
counterparts to the overwrite-only fundingRate / longOpenInterest /
shortOpenInterest fields on the perp Market row (which only carry the latest
value). Liquidation history depends on the indexer's LiquidationEngine
subscription, which is deferred until that contract is deployed — until then
getLiquidations returns the MarginBank-sourced rows only.
Quick start
const exchange = new SomniaMarkets({ chain, wsRpcUrl, indexerUrl, privateKey });
await exchange.loadMarkets();
await exchange.depositMargin("BTC/USDSO:USDSO", 1_000); // USDso → MarginBank
await exchange.createOrder("BTC/USDSO:USDSO", "limit", "buy", 0.001, 62_000);
const [pos] = await exchange.fetchPositions(); // long/short + uPnL
const { fundingRate, markPrice } = await exchange.fetchFundingRate("BTC/USDSO:USDSO");
One-shot reads (no watch)
For a plain fetch without a live tail:
const perps = await client.listPerpMarkets({ baseSymbol: "WBTC", limit: 20 });
const one = await client.getPerpMarket(perps[0].id); // null if not a perp
const port = await client.getPerpPortfolio(account, { ordersLimit: 50, tradesLimit: 50, since });
const state = await client.getPerpState(one!.poolAddress); // mark/index/funding/OI
listPerpMarkets takes a PerpMarketFilter (baseSymbol / quoteSymbol, all
server-side); getPerpPortfolio takes the shared PortfolioOptions
(ordersLimit / tradesLimit / since). Positions + collateral stay on-chain
(getPerpPosition / getMarginAccount), not indexed rows.
Writing forward-compatible code
const m = client.getLiveMarketByPool(pool);
switch (m?.marketType) {
case "BINARY": /* YES/NO book */ break;
case "SPOT": /* base/quote book */ break;
case "PERP": /* base/quote book + funding/positions */ break;
}
Key on marketType (not on field presence), use the type guards, and read
decimals off the market row rather than assuming.