@somnia-chain/markets-sdk
The TypeScript SDK for building on Somnia Markets — read live market data and place trades on the on-chain order book from your own app.
- Realtime data, no wallet required. Order books, trades, candles, and a user's positions and open orders stream into your UI the moment they happen on-chain — no polling loops to write or manage.
- Trading with a signer. Place and cancel orders, mint and redeem outcome shares, and more, through a typed trader bound to your wallet.
- Works anywhere, with first-class React. Use plain async functions in any environment, or drop in the hooks for components that update themselves.
Install
This package is published to GitHub Packages under the private @somnia-chain
scope, so npm needs to know where the scope lives and a token to read it.
-
Create a GitHub personal access token (classic) with the
read:packagesscope, and make sure your account has access to thesomnia-chainorganization. -
Add this to your project's
.npmrc(do not commit the token — use an env var or your user-level~/.npmrc):@somnia-chain:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} -
Install:
export NODE_AUTH_TOKEN=ghp_your_token_here pnpm add @somnia-chain/markets-sdk viem
Create an exchange
new SomniaMarkets(config) is the single entry point — the exchange owns everything:
symbols, market data, watches, and writes. No global setup, no hidden singleton;
each exchange is isolated.
import { SomniaMarkets } from "@somnia-chain/markets-sdk";
import { testnet } from "@somnia-chain/deployments"; // the deployments hub — the single source of contract addresses
import { somniaTestnet } from "viem/chains"; // or your own defineChain()
const exchange = new SomniaMarkets({
indexerUrl: "/v1/graphql",
chain: somniaTestnet,
wsRpcUrl: "wss://api.infra.testnet.somnia.network/ws",
addresses: testnet.addresses, // the hub map feeds in as-is — no hand-mapping
privateKey, // optional — needed for createOrder & friends
});
await exchange.loadMarkets();
const book = await exchange.watchOrderBook("BTC-95000-31DEC26/USDC#YES"); // live, zero RTT
const order = await exchange.createOrder("BTC-95000-31DEC26/USDC#YES", "limit", "buy", 10, 0.62);
The raw engine tier — bigint-exact, address-keyed — is reached through the
exchange (exchange.client, exchange.trader), never constructed separately.
The WebSocket opens lazily on first chain I/O, so an indexer-only exchange
(e.g. server-side GraphQL reads) never opens one. Nothing is shared between
instances: a bot per chain, per-request servers, parallel tests — just
construct another. Two exchanges never share watch state or sockets.
The guides, in reading order:
- The exchange API — the
SomniaMarketsclass, the SDK's primary surface: symbols (SOMI/USDC,BTC-95000-31DEC26/USDC#YES),fetch*/watch*/createOrder, human-unit structs. Exchange-bot muscle memory (ccxt included) transfers directly — start here. - Binary markets — the binary (YES/NO) CLOB: probability prices, the four sides, mint/burn/redeem, and a maker loop.
- Spot markets — base/quote books: ticks and lots, native-base escrow, market orders, and stop orders.
- Perps — live on testnet: cross-margin via the
MarginBank, funding, positions, and how perps slot into the
marketTypeunion. - Price feeds — realtime BTC/ETH index prices from the
on-chain EMA oracle:
watchPrice/getLivePrice, one-shot history + candles, and the React hooks. - The engine (advanced) — the raw tier behind the
exchange (
exchange.client/exchange.trader): bigint-exact reads, ref-counted watches, React hook wiring, raw writes. - Architecture guide — diagrams of the whole machine: the watch seam, event routing, the local order book, the reconnect lifecycle, and the one-round-trip write path.
Two ways to read
| How | What it is | Returns |
|---|---|---|
client.list* / client.get* | One-shot read (indexer GraphQL or on-chain) | a Promise |
client.getLive* + client.subscribeLive | Synchronous read off the live store (within a watchMarket scope) | a value, now |
use* hooks (/react) | React bindings over the live store (auto-watching) | re-render on change |
So client.getFills fetches once; client.getLiveFills reads the live tape;
useLiveFills re-renders a component as it updates. In React, provide the client
once with <SomniaMarketsProvider client={client}> (from @somnia-chain/markets-sdk/react)
and the hooks read it from context.
Markets come from one discriminated union — Market = SpotMarket | PerpMarket | BinaryMarket, keyed on marketType — via client.listMarkets / client.getMarket. Binary-only
callers can use client.listBinaryMarkets / client.getBinaryMarket, the same
query pre-narrowed to BinaryMarket. (Note: binary, not clob — a spot market
is an order book too, so "CLOB" was never the right label for the binary surface.)
Money crosses the API as raw integers (bigint on writes, decimal strings from the
indexer) scaled by token decimals. Convert at the edges with fromHuman (input)
and toHuman / toHumanString (display); for binary prices,
probabilityToPrice / priceToProbability map a YES price ↔ a 0–1 probability.
What's included
- Entry point —
new SomniaMarkets(config)→ the exchange (symbols,fetch*/watch*/createOrder, human-unit structs). Its engine tier —exchange.client(SomniaMarketsClient, bigint-exact reads + watches) andexchange.trader(raw writes) — is reached through it;ClientConfig - React —
SomniaMarketsProvider,useSomniaMarketsClient, and the hooksuseWatchMarket,useWatchUser,useLiveStatus,useIsTailing,useLiveFills,useLiveUserFills,useLiveMarketByPool,useLiveMarketByAddress,useLiveUserOrders,useLiveBinaryOrderBook,useLiveSpotOrderBook— the pool-keyed data hooks watch automatically while mounted - Client reads —
client.listMarkets/getMarket(theMarketunion),listBinaryMarkets/getBinaryMarket,getCandles,getBinaryOrderBook,getOpenOrders,getPortfolio,getSyncStatus,getMarketOnchain,getSystemInfo, … (indexer reads throw on failure — an empty result always means "no rows", never "request failed") - Live watches (no React) —
client.watchMarket(pool)/watchMarkets({ discover })/watchUser(account)→ ref-counted handles;getWatchStatus,subscribeLive,getLiveStatus,getLiveMarkets,getLiveMarketByPool/…ByAddress,getLiveFills,getLiveUserFills,getLiveUserOrders, and the locally materialized resting booksgetLiveBinaryOrderBook(binary, 4-sided) /getLiveSpotOrderBook— synchronous, zero round-trips, scoped to what you watch. Every market kind streams; a discovery watch picks up new markets from the creation events (the MarketCreator's rolling series AND directBinaryMarketsModule.createMarketmarkets); binary status/resolution stays current from chain events. - Trading —
client.createTrader(...)→placeOrder,cancelOrder,approveBuilder(opt a routing/builder frontend in for per-order builder fees),placeSpotOrder,placeSpotStopOrder,mintSet,burnSet,redeem,faucet,resolve,voidMarket. Each write awaits its receipt and resolves to{ hash, receipt }(placeOrderaddsorderId+fills). With aprivateKey/localaccountthe SDK signs locally with fixed fees and a locally-tracked nonce, and sends via Somnia'srealtime_sendRawTransaction— send + confirm in one round-trip, zero fee/nonce/gas estimation RPCs. In the browser, pass awalletClient(confirm rides the newHeads subscription). - Types & helpers —
Market/SpotMarket/BinaryMarket(+isSpotMarket/isBinaryMarket),LiveFill,LiveOrder,BinarySide,TailStatus,kindOf,fillKind,fromHuman/toHuman,DECIMALS, …
Every method, hook, and type is listed in the API reference.
How the live feed works
You get instant updates without running your own indexer — scoped to exactly the markets you watch. Opening a watch loads a consistent snapshot of that scope (the one and only indexer touch), then keeps it current by streaming its on-chain events over a WebSocket — so trades, orders, prices, and the resting order book itself update the moment they're final on-chain. There is no polling anywhere: the WebSocket is the only realtime transport, and if it drops the watches heal themselves by reconnecting with backoff and backfilling the missed blocks straight from chain.