Skip to Content
Symmio Trading-SDK — the SDK surface for builders on Arbitrum
CoreCore ConceptsSolvers & Chains

Solvers & Chains

SYMMIO is a multi-solver, multi-chain protocol, and the SDK models both axes explicitly. Reach for this page before you wire market data, prices, notifications, or a trade flow — it explains how one config serves two very different products, and how the SDK normalizes the differences so your components rarely branch on them.

Two axes: solver kind and chain

A deployment is one chain running one or more solvers. Every solver has an id, and in this SDK the solver id is its kind — there is no separate version axis. Two kinds ship today:

KindProductChainMargin modelPrice providerNotifications protocol
enigmalowcap perpsArbitrum (42161)Virtual Accounts (per market/side)Enigma price serviceenigma — notification service
rasamajors perpsBase (8453)cross-margin on the sub-accountBinance USD-M Futuresrasa — solver position-state

The two are not tiers of the same thing — they are different counterparties with different endpoints, response shapes, and lifecycles. The SDK’s job is to hide that behind one stable surface.

SolverId ≡ solver kind. type SolverId = SymmioSolverKind ("enigma" | "rasa"), derived from SUPPORTED_SOLVER_KINDS. A chain’s registry entry keys its solvers by that id, so resolving a solver and dispatching by kind are the same operation.

Contracts version is a chain axis

Chains do not all run the same perps-core generation: Base runs v0.8.5, while Arbitrum runs v0.8.6. Each registry entry declares it as contractsVersion (a SymmioContractsVersion, override-able per chain through createConfig’s symmioConfig), and the SDK branches on it exactly where the generations diverge: which quote-send call the instant-open flow signs (sendQuote with solver-fee caps on v0.8.6, the legacy sendQuoteWithAffiliateAndData on v0.8.5), which selector set a session key needs (getInstantTradeRequiredSelectors / useInstantTradeRequiredSelectors), and which output shape the withdraw-request reads decode (advancedAmount is undefined on a v0.8.5 chain). It is a declared deployment fact — the diamond exposes no version view, so nothing is probed at runtime.

Targeting a solver

Every solver-aware action and hook takes an optional solverId. Omit it and the SDK resolves the chain’s default solver (defaultSolverId in the registry — enigma on Arbitrum, rasa on Base). Pass a literal kind to target a specific solver and, where the return is a normalized union, narrow the return type to that kind.

import { getMarkets } from "@symmio/trading-core"; // No solverId → the chain default; the Market union (narrow on `kind`). const markets = await getMarkets(config, {}); // Literal kind → the exact per-kind type. const rasa = await getMarkets(config, { solverId: "rasa" }); // RasaMarket[]

Read the chain’s default at runtime with config.getDefaultSolverId(chainId?):

const solverId = config.getDefaultSolverId(); // "enigma" on Arbitrum, "rasa" on Base

In React, pass solverId to the hook the same way — useMarkets({ solverId }), usePriceByName({ name, solverId }), and so on — or omit it to follow the connected chain’s default.

Per-solver divergence → normalized per-kind unions

The same logical endpoint returns different shapes per kind: different fields, optionality, and value types. Rather than leak those raw shapes, the SDK normalizes each divergent endpoint to a shared base type + one variant per kind, unioned on kind. You narrow on kind to reach a solver’s exclusive fields; a caller that already targets one solver gets that variant directly.

interface BaseMarket { /* fields both solvers return — camelCase, required */ } export interface EnigmaMarket extends BaseMarket { kind: "enigma"; state: number; // Enigma-only — narrow on kind to read it /* … */ } export interface RasaMarket extends BaseMarket { kind: "rasa"; /* Rasa-only fields */ } export type Market = EnigmaMarket | RasaMarket;

The same pattern applies across the SDK:

  • MarketsMarket = EnigmaMarket | RasaMarket (see getMarkets).
  • Mark pricesMarkPriceTick = EnigmaMarkPriceTick | BinanceMarkPriceTick; narrow on provider for Binance’s indexPrice. See Price Service.
  • Position notificationsRawPositionNotification is a base plus RawEnigmaPositionNotification / RawRasaPositionNotification. See Notifications.

A kind-exclusive field lives on that variant only, never as an optional field on a shared shape. That is how the types tell you a Rasa market has no state: accessing it without first narrowing kind === "enigma" is a compile error.

Margin models: Virtual Accounts vs cross-margin

The margin and execution model follows the sub-account’s isolation type, and the two products use different ones:

  • Enigma (lowcap) isolates positions into a Virtual Account (VA) scoped per market and side (long/short) — not one VA per position. TP/SL is signed against the VA, liquidation is per-VA, and the SDK predicts the next VA before an open lands (usePredictedNextVirtualAccount, keyed by the market symbolId and the side’s isolation type).
  • Rasa (majors) trades cross-margin directly on the sub-account (SubAccountIsolationType.CUSTOM) — there is no Virtual Account. Positions share the sub-account’s allocated margin, so a trade UI on Base must not select a VA, must not predict one, and reads liquidation at the account level.
import { SubAccountIsolationType } from "@symmio/trading-core"; // A rasa/majors sub-account is cross-margin — branch the UI on this, not on the solver. const isCrossMargin = subAccount.isolationType === SubAccountIsolationType.CUSTOM;

Branch on the sub-account isolation type, not on a hardcoded chain. Cross-margin (CUSTOM) means no VA leg: skip VA prediction and the TP/SL-against-VA step, and fund from the allocated balance. Assuming a VA on a rasa sub-account is the most common majors-integration bug.

Price providers

The active solver’s chain decides the price service, and the SDK’s mark-price reads are provider-agnostic — they serve whichever provider the target solver uses, so a component never branches on it:

  • Enigma — the Enigma price service (REST + WebSocket) on Arbitrum.
  • RasaBinance USD-M Futures: REST at fapi.binance.com and the mark-price stream wss://fstream.binance.com/market/ws/!markPrice@arr@1s.

usePriceByName / useMarkPrices return the same shape for both; narrow the tick on provider when you need a Binance-only field (indexPrice, funding). Provider-specific twins (useBinancePrices, useBinanceHealth, …) exist for callers that already know their source and want the Binance fields without narrowing — they surface UNSUPPORTED_BY_PRICE_SERVICE when the target solver is not priced by Binance. See Price Service.

Notifications protocols

Notifications are configured per solver — each solver carries a required notifications: SymmioNotificationsConfig, resolved via config.getSolver({ chainId, solverId }).notifications. Both products broadcast a report frame for every state transition, but over different transports, captured by the config’s protocol discriminant (SymmioNotificationsProtocol, "enigma" | "rasa"):

  • enigma — channel-scoped subscription, envelope-wrapped frames (EnigmaNotificationEnvelope), and history from a standalone notification service (searchUrl + POST /api/v1/search).
  • rasa — subscribe by an address list (buildRasaSubscribeMessage{"address":[…]}), a single multiplexed hub connection, and bare frames. History comes from the solver’s own position-state endpoint (POST /position-state/{start}/{size}); its notifications config carries no searchUrl.

Both the live watchNotifications stream and the REST searchNotifications take an optional solverId and dispatch on the resolved solver’s notifications.protocol, so callers subscribe and search the same way on either solver. searchNotifications is one interface over both kinds — the enigma notification service or the rasa position-state endpoint — returning a per-kind union you narrow on kind. See Notifications.

Rasa-only solver endpoints

The rasa solver exposes reads the enigma solver does not. Each has a core action (and a React hook); calling one against a non-rasa solver throws a typed UNSUPPORTED_BY_SOLVER error rather than dispatching:

EndpointCore actionReact hook
Solver readinessgetSolverReadinessuseSolverReadiness
Solver-side balancegetSolverBalanceInfouseSolverBalanceInfo
PartyA uPnLgetPartyAUpnlusePartyAUpnl
Global open interestgetSolverOpenInterestuseSolverOpenInterest
Symbol price rangegetSolverPriceRangeuseSolverPriceRange
Error-code messagegetErrorMessageuseErrorMessage

Notification history search is not rasa-only — it is the unified searchNotifications, which dispatches to the rasa solver’s position-state endpoint for a rasa solver. See the Rasa Solver hooks page for signatures and examples.

Solver capabilities

Some flows exist on one solver kind but not the other. Rather than branch on a hardcoded kind, each solver declares what it supports through an optional capabilities block on its registry entry (SolverCapabilitiesConfig). Every flag is opt-in — unset defaults to false, so a solver must declare a capability to enable it.

export interface SolverCapabilitiesConfig { /** Close a whole market + side group in one flow. Enigma (per-VA isolation) yes; rasa no. */ groupClose?: boolean; /** Place a LIMIT order — a pending open at a user-set price. Rasa (majors) yes; enigma no. */ limitOrder?: boolean; /** This solver's markets are the lowcap Pools. **Declarative only** — the listing functions do not check it (see below). Enigma yes; rasa no. */ listingService?: boolean; }

Today’s registry: enigma declares groupClose and listingService, rasa declares limitOrder. Unlike the other two, listingService is not gated on by any SDK function — it is metadata (readable via getSolverCapabilities); listing resolves at chain level (see Pools listing service).

TP/SL is not a capability flag. A solver supports conditional orders exactly when its config carries a tpsl block — inferred from presence, read with supportsTpSl, not from capabilities.

Resolve the flags with getSolverCapabilities (or the supportsGroupClose / supportsLimitOrder shorthands). All are non-throwing — an unknown chain/solver resolves to all-false, so gating never crashes:

import { getSolverCapabilities, supportsLimitOrder } from "@symmio/trading-core"; const { groupClose, limitOrder } = getSolverCapabilities(config, { chainId }); // or the shorthand — gate a flow / UI so an unsupported solver degrades gracefully: if (supportsLimitOrder(config, { solverId })) showLimitOrderTab();

Both take an optional chainId / solverId, defaulting to the chain’s default solver. The write actions self-guard too: limitOpenAuto on a solver without limitOrder throws UNSUPPORTED_BY_SOLVER. In React, gate with the useSolverCapabilities / useSupportsLimitOrder / useSupportsGroupClose hooks.

Pools listing service

The lowcap Pools flow (per-token liquidity markets — catalog, stats, LP stake/rewards, create/withdraw/claim) is served by a listing backend. Unlike the per-solver services above, listing is chain-level: its URL lives on the chain (SymmioChainConfig.listing, a SymmioListingConfig), because several solvers on one chain could share one listing deployment. A solver may still declare it does listing via the listingService capability, but that flag is metadata onlyresolveListingService does not check it. Resolution is purely chain-level: a chain either has a listing backend or it does not.

import { resolveListingService, supportsListingService } from "@symmio/trading-core"; // Non-throwing gate for `enabled` flags / UI: if (supportsListingService(config, { chainId })) showPoolsTab(); // Resolve the backend (throws LISTING_NOT_CONFIGURED where the chain has no listing backend): const { url } = resolveListingService(config, { chainId });

Today only Arbitrum has it (listing: { url: "https://listing85.enigma.bz" }). Point it at staging with a createConfig override — there is no separate environment axis.

The url is the host root, with no version segment and no trailing slash. The generated client’s own paths already begin with /v2, so a versioned base URL would request /v2/v2/market/search and 404.

In React, gate with useSupportsListingService, then read the catalog with useListingMarkets.

Configuring chains and solvers

Each registry entry carries a solvers map keyed by solver id plus a defaultSolverId. You override any of it per chain through createConfig’s symmioConfig — including a solver’s capabilities — see Config and Addresses.

import { createConfig, SymmioSupportedChainId } from "@symmio/trading-core"; import { zeroAddress } from "viem"; const config = createConfig({ getClient: () => publicClient, symmioConfig: { [SymmioSupportedChainId.ARBITRUM]: { addresses: { affiliatesAddress: zeroAddress } }, // enigma / lowcap [SymmioSupportedChainId.BASE]: { // Base requires a registered (non-zero) affiliate — the built-in default is used when you omit it. addresses: { affiliatesAddress: "0x45Eecd7B4f442388ACD90467E423A5CAAC3a9C3f" }, }, // rasa / majors }, });
  • Addresses — the per-chain registry, including Base and the rasa solver.
  • SolversgetMarkets and the solver read actions.
  • Price Service — Enigma vs Binance, and the provider-agnostic mark-price reads.
  • Notifications — the enigma and rasa protocols.
  • Build a Perps DEX — the end-to-end guide, for lowcap and majors.
Last updated on