Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mobula.io/llms.txt

Use this file to discover all available pages before exploring further.

Use this stream to learn that a submitted order filled, was liquidated, hit TP/SL, was canceled, or had its margin / leverage / TP / SL updated. The Perp Positions WSS gives you state (open positions + pending orders); this stream gives you the events that change that state.

Endpoint

  • URL: wss://stream-perps-prod-eu.mobula.io/
  • Event type: stream (in the subscribe envelope)
This stream is hosted on a different domain from the perp REST API and the Perp Positions WSS (which both live on hawk-api-prod-eu.mobula.io). Subscribing to perp events on hawk-api-prod-eu.mobula.io silently fails with CloseEvent { code: 4001, reason: "Idle timeout - no subscription received" } ~10 s later.

Subscribe

{
  "type": "stream",
  "authorization": "<MOBULA_SERVER_SIDE_KEY>",
  "payload": {
    "name": "perp-events-<your-uuid>",
    "chainIds": ["evm:42161", "evm:421614", "evm:8453", "lighter:304"],
    "events": ["order"],
    "subscriptionTracking": "true"
  }
}

Parameters

  • type (required) — must be "stream".
  • authorization (required) — your Mobula server-side API key.
  • payload.name (required) — client-provided subscription name. Echoed back on the ack frame and useful for log correlation.
  • payload.chainIds (required) — array of chain ids to subscribe to. See Chain coverage below.
  • payload.events (required) — array of event categories. "order" covers all perp trade-lifecycle events listed in Event types below.
  • payload.subscriptionTracking (optional) — string "true" (not a boolean). When set, the ack frame includes a subscriptionId you can use to demux multiple subscriptions on a single socket.
subscriptionTracking on this stream is the string "true". The sibling Perp Positions WSS uses a boolean true instead. The two envelope shapes are different — see the comparison table below.

Subscribe acknowledgement

{ "event": "subscribed", "subscriptionId": "sub_5cbd27158c9f0b7fb9bc66584d55d76a" }

Server push

Each event is one frame. The top-level envelope wraps the event-specific data:
{
  "data": { /* event-data — schema varies by event type, see below */ },
  "chainId": "evm:42161",
  "duplicateCount": 1,
  "subscriptionId": "sub_5cbd27158c9f0b7fb9bc66584d55d76a"
}
  • data.traderAddress is lowercased on the wire — match accordingly when filtering by wallet.
  • duplicateCount is informational; identical frames may still be delivered on reconnect or for Gains’ TRADE_STORED + MARKET_BUY pair on the same transaction. Deduplicate on the client with a key like (transactionHash, type, traderAddress, tradeId).

Event types

TypeEmitted byDescription
MARKET_BUY / MARKET_SELLGains, LighterMarket order fill
LIMIT_BUY / LIMIT_SELLGainsLimit order fill
LIQUIDATION_LONG / LIQUIDATION_SHORTGains, LighterPosition liquidated
TAKE_PROFIT / STOP_LOSSGains, LighterTP / SL trigger fired
TRADE_STOREDGainsEmitted in addition to MARKET_* when a Gains trade is committed to the Diamond — clients see two frames per tx
UPDATE_TP / UPDATE_SLGainsTP/SL edited. Carries newPriceQuote and quantityPercentage.
POSITION_SIZE_INCREASE_EXECUTED / POSITION_SIZE_DECREASE_EXECUTEDGainsMargin added / removed on an open position
LEVERAGE_UPDATE_EXECUTEDGainsLeverage changed on an open position
MARKET_OPEN_CANCELED / MARKET_CLOSE_CANCELEDGainsMarket order rejected by the Diamond. extra.cancelReason is a numeric Gains enum — see the Gains Trading docs for the canonical mapping.
For the field reference of each event category (PerpOrderExecuted, PerpTpSlUpdate, PerpUncategorizedEvent) including extra-field shapes, see the Perpetuals data model.

Gains “two frames per trade” caveat

For every Gains market action, you will receive two events back-to-back with overlapping but different field sets — these are not duplicates:
  • TRADE_STORED{ market, exchange, chainId, traderAddress, type, tradeId, transactionHash, extra: { tradeType, leverage, long, collateralAmount, index, openPrice, tp, sl }, date, ... }
  • MARKET_BUY (or MARKET_SELL) — { market, exchange, chainId, type, priceQuote, priceUSD, blockNumber, baseAmountRaw, tradeId, collateralAmountRaw, collateralAsset, leverage, traderAddress, takeProfitUSD, stopLossUSD, extra: { open, amountSentToTrader, trade: { long, index, tradeType, openPrice, collateralIndex } }, transactionHash, date, ... }
Common dedupe key: (transactionHash, type, traderAddress, tradeId) — distinguishes the two without dropping one of them.

Chain coverage

Chain idUsed for
evm:42161Gains on Arbitrum (mainnet)
evm:421614Gains on Arbitrum Sepolia (testnet)
evm:8453Gains on Base
lighter:304Lighter — used by this stream
Lighter on this stream is lighter:304. The sibling Perp Positions WSS uses lighter:301. Subscribing to events with lighter:301 returns no Lighter frames at all — it is silently dropped.

Envelope shape — events vs positions

The perp events stream and the perp positions WSS use different envelope shapes:
Perp Events (this page)Perp Positions
Hoststream-perps-prod-eu.mobula.iohawk-api-prod-eu.mobula.io
Subscribe top-level{ type: "stream", authorization, payload }{ event: "perp-positions-open" | "...-unfilled", data, authorization }
subscriptionTracking typestring "true"boolean true
Lighter chain idlighter:304lighter:301
Push semanticsone event per framefull snapshot per frame

Heartbeat & idle timeout

The server expects ping frames to keep the socket alive:
{ "event": "ping" }
The server replies with { "event": "pong" }. The idle timeout is short (~10 s) before a subscription is sent — make sure your first frame after open is a subscribe, not silence. Once subscribed, 30 s+ between pings has been observed to work.

Lighter “hawk” subscribers

Lighter MARKET_* / liquidation / TP / SL frames may include an activePosition / historicalPosition block embedded on the event with the full position state at the time of the event. Raw (non-hawk) subscribers receive a smaller payload without this block.

Implementation example

const ws = new WebSocket('wss://stream-perps-prod-eu.mobula.io/');

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({
    type: 'stream',
    authorization: process.env.MOBULA_API_KEY,
    payload: {
      name: `perp-events-${crypto.randomUUID()}`,
      chainIds: ['evm:42161', 'evm:8453', 'lighter:304'],
      events: ['order'],
      subscriptionTracking: 'true',     // STRING, not boolean
    },
  }));

  setInterval(() => ws.send(JSON.stringify({ event: 'ping' })), 30_000);
});

const seen = new Set<string>();

ws.addEventListener('message', (msg) => {
  const frame = JSON.parse(msg.data.toString());
  if (frame.event === 'pong') return;
  if (frame.event === 'subscribed') return;

  const e = frame.data;
  if (!e?.type) return;

  const dedupeKey = `${e.transactionHash}-${e.type}-${e.traderAddress}-${e.tradeId}`;
  if (seen.has(dedupeKey)) return;
  seen.add(dedupeKey);

  // Filter by your wallet — events are lowercased on the wire
  if (e.traderAddress !== WALLET_ADDRESS.toLowerCase()) return;

  handleEvent(e);
});

Perp Positions Stream

Live snapshot of open positions + unfilled orders.

Perpetuals Data Model

Per-event field reference (executed / TP-SL / uncategorized).

Execution Cookbook

Pair with this stream to learn that your order filled.

Funding Stream

Real-time funding rates across CeFi + DeFi exchanges.