{s}omniamarkets

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 / withdrawMargin move it; trader.placePerpOrder (or plain createOrder on 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) and client.getMarginAccount(marginBank, account) — or the unified exchange.fetchPositions(). Live pricing/funding comes from client.getPerpState(pool) / exchange.fetchFundingRate(symbol).
  • Margin health is a chain read too. getMarginAccount now also returns the requirements + status (imReq / mmReq / cmReq / marginStatus, from MarginBank.getAccountHealth + getMarginStatus); client.getAccountHealth(marginBank, account) is the lighter standalone read when only health matters, and client.getLiquidationPrice(marginBank, pool, account) estimates the mark at which this pool's move alone would trip maintenance (null when flat). MarginStatus / MARGIN_STATUS / AccountHealth are exported; exchange.fetchPositions() now fills UnifiedPosition.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.