Order Lifecycle

orders

Real-time order lifecycle events per address — order placements, cancellations, fills, and MMP triggers streamed to a single channel.

Channel: orders:{address} Example: orders:0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Reliability Model

Every event carries a per-address sequence number (seq). Clients use seq to detect gaps and recover missed events:

  1. On subscribe — the server returns a snapshot of all active (resting) orders
  2. Live events — each event has a monotonically increasing seq per address
  3. Gap detection — if client sees seq jump (e.g. 5 → 8), events 6 and 7 were missed
  4. Gap fill — call the REST endpoint to recover missed events by seq range

No server-side history or recovery is used — all recovery goes through the REST gap fill API.

Tag Filtering

Publications carry an event tag for server-side filtering. Without a filter, all event types are delivered.

TagValuesDescription
eventpost_order, cancel_order, update_order, trade, mmp_triggeredFilter by event type

Note: Clients using tagsFilter will see gaps in seq numbers — this is expected. Other event types (filtered out) occupy the missing sequence numbers. Use the gap fill REST API if you need to verify no events were lost.

// Only trade fills
const sub = client.newSubscription("orders:0xYOUR_ADDRESS", {
  tagsFilter: { key: "event", cmp: "eq", val: "trade" }
});

// Only fills + MMP (critical events)
const sub = client.newSubscription("orders:0xYOUR_ADDRESS", {
  tagsFilter: { key: "event", cmp: "in", vals: ["trade", "mmp_triggered"] }
});

Event Types

EventSourceWhen
post_orderNew limit order placedOrder enters the book
cancel_orderOrder cancelledUser cancel or expiry
update_orderOrder state changedPartial fill updates filled_amount and order_state
tradeExecution (fill)Taker or maker side of a trade
mmp_triggeredMarket maker protectionMMP circuit breaker trips

edit_order is not a wire event — edits emit cancel_order (old) + post_order (new order_id). Market orders have no post_order — they go straight to trade.

Payloads

Every message is a JSON object with a common envelope:

{
  "type": "<event_type>",
  "seq": 42,
  "timestamp": 1704067200000,
  "data": { ... }
}
FieldTypeDescription
typestringEvent type (see table above)
seqintegerPer-address sequence number, monotonically increasing. Use for gap detection.
timestampintegerEvent timestamp in epoch milliseconds
dataobject | arrayEvent payload (see below). Array when batch contains multiple orders.

Order Events (post_order, cancel_order, update_order)

{
  "type": "post_order",
  "seq": 1,
  "timestamp": 1704067200000,
  "data": {
    "order_id": "a1b2c3d4-...",
    "user_id": "my-algo-ref",
    "instrument": "ETH_USDC-PERPETUAL",
    "direction": "buy",
    "price": 2500.0,
    "size": 10.0,
    "order_type": "good_til_cancelled",
    "order_state": "open",
    "filled_amount": 0.0,
    "post_only": true,
    "mmp": false,
    "liquidation": false
  }
}
FieldTypeDescription
order_idstringUnique order identifier
user_idstring | absentOptional client-supplied reference (max 36 chars). Omitted if not set.
instrumentstringInstrument name (e.g. ETH_USDC-PERPETUAL, BTC_USDC-31OCT25-130000-C)
directionstring"buy" or "sell"
pricenumber | nullLimit price. May be null on cancel_order.
sizenumber | nullOrder size — unified from contracts (options) and amount (perps). May be null on cancel_order.
order_typestring | null"good_til_cancelled", "fill_or_kill", "immediate_or_cancel"
order_statestring | null"open", "filled", "cancelled", "partially_filled"
filled_amountnumber | nullCumulative filled size. May be null on cancel_order.
post_onlyboolean | nullPost-only flag
mmpboolean | nullWhether this order is covered by MMP
liquidationboolean | nullWhether this is a liquidation order

Fields marked null are present in the JSON but set to null when the upstream does not provide a value (common on cancel_order events). Always-present fields: order_id, instrument, direction.

Trade Events

{
  "type": "trade",
  "seq": 5,
  "timestamp": 1704067200000,
  "data": {
    "trade_id": "t1a2b3c4-...",
    "order_id": "a1b2c3d4-...",
    "instrument": "ETH_USDC-PERPETUAL",
    "direction": "buy",
    "liquidity_indicator": "taker",
    "price": 2501.50,
    "size": 1.5,
    "index_price": 2500.0,
    "liquidation": false,
    "taker_fee": 2.25,
    "maker_fee": 0.0,
    "systemic_risk_fee": 0.50,
    "liquidation_fee": 0.0,
    "realised_funding": 0.15
  }
}
FieldTypeDescription
trade_idstringUnique trade identifier
order_idstringThe order that was filled
instrumentstringInstrument name
directionstring"buy" or "sell"
liquidity_indicatorstring"taker" (aggressor) or "maker" (resting order)
pricenumberExecution price
sizenumberFill size
index_pricenumberIndex price at time of execution
liquidationbooleanWhether this fill is part of a liquidation
taker_feenumberTaker fee charged
maker_feenumberMaker fee/rebate (negative = rebate)
systemic_risk_feenumberSystemic risk fee
liquidation_feenumberLiquidation fee (0 if not a liquidation)
realised_fundingnumberRealised funding payment

Liquidity indicator: order_id == trade_id means taker (aggressor), otherwise maker (resting).

MMP Events

{
  "type": "mmp_triggered",
  "seq": 8,
  "timestamp": 1704067200000,
  "data": {
    "pair": "ETH",
    "frozen_until": 1704067500,
    "frozen_duration_seconds": 300
  }
}
FieldTypeDescription
pairstringPair symbol (e.g. "ETH", "BTC")
frozen_untilinteger | nullUnix timestamp (seconds) when MMP freeze ends
frozen_duration_secondsinteger | nullDuration of the freeze in seconds

Subscribe Snapshot

On subscribe, clients receive a snapshot of all active (resting) orders for the address. This is delivered as the subscription data payload:

{
  "type": "snapshot",
  "orders": [
    {
      "order_id": "f9cd15a1...",
      "instrument": "BTC_USDC-PERPETUAL",
      "direction": "buy",
      "price": 60000,
      "size": 5000,
      "fillable_amount": 5000,
      "order_state": "open",
      "post_only": false,
      "mmp": false,
      "liquidation": false,
      "maker": "0xAA836669CDBb4E6ae70e6B0B45417C98D19a432c"
    }
  ]
}

The snapshot fields are normalized to match the event format:

  • amount (perps) and contracts (options) are renamed to size
  • instrument_name is renamed to instrument
  • fillable_amount (remaining unfilled size) is injected from the exchange's order state

Use this as the initial state, then apply live events as deltas.

Gap Fill — REST API

Recover missed events by sequence number range. Events are persisted for 1 hour.

GET /api/v1/orders/events/{address}

The environment is determined automatically from the domain (sandbox.kyan.sh → sandbox, staging.kyan.sh → staging).

Path parameters:

ParamDescription
addressEthereum address (e.g. 0xAA836669...)

Query parameters:

ParamTypeDefaultDescription
after_seqintegerReturn events with seq > N (exclusive lower bound)
from_seqintegerReturn events with seq >= N (inclusive lower bound)
to_seqintegerReturn events with seq <= N (inclusive upper bound)
limitinteger100Max events to return (1–1000)

Response:

{
  "events": [
    {
      "type": "post_order",
      "seq": 6,
      "timestamp": 1704067200000,
      "data": { ... }
    },
    {
      "type": "cancel_order",
      "seq": 7,
      "timestamp": 1704067201000,
      "data": { ... }
    }
  ]
}

Example — recover after detecting gap (last seen seq=5):

curl "https://staging.kyan.sh/api/v1/orders/events/0xYOUR_ADDRESS?after_seq=5&limit=100"

Events are retained for 1 hour. Results are capped at 1000 events per request.

Subscribe — SDK Protobuf

Smallest frames, fastest deserialization. Recommended for HFT/algo clients.

import { Centrifuge } from "centrifuge/build/protobuf";

const client = new Centrifuge("wss://staging.kyan.sh/stream/websocket?format=protobuf");
const sub = client.newSubscription("orders:0xYOUR_ADDRESS");

let lastSeq = 0;

sub.on("subscribed", (ctx) => {
  // Initial snapshot of active orders (delivered on subscribe)
  const snapshot = ctx.data;
  if (snapshot?.type === "snapshot") {
    console.log("Active orders:", snapshot.orders);
  }
});

sub.on("publication", (ctx) => {
  const event = ctx.data instanceof Uint8Array
    ? JSON.parse(new TextDecoder().decode(ctx.data))
    : ctx.data;

  // Gap detection
  if (lastSeq > 0 && event.seq > lastSeq + 1) {
    console.warn(`Gap detected: ${lastSeq} → ${event.seq}`);
    // Call /api/v1/orders/events/{address}?after_seq={lastSeq}&to_seq={event.seq - 1}
  }
  lastSeq = event.seq;

  switch (event.type) {
    case "post_order":
    case "cancel_order":
    case "update_order":
      console.log(event.seq, event.data.order_id, event.data.order_state);
      break;
    case "trade":
      console.log(event.seq, event.data.trade_id, event.data.liquidity_indicator, event.data.price);
      break;
    case "mmp_triggered":
      console.log(event.seq, "MMP frozen:", event.data.pair, event.data.frozen_duration_seconds);
      break;
  }
});

sub.subscribe();
client.connect();

Subscribe — SDK JSON

import { Centrifuge } from "centrifuge";

const client = new Centrifuge("wss://staging.kyan.sh/stream/websocket");
const sub = client.newSubscription("orders:0xYOUR_ADDRESS");

let lastSeq = 0;

sub.on("subscribed", (ctx) => {
  if (ctx.data?.type === "snapshot") {
    console.log("Initial orders:", ctx.data.orders);
  }
});

sub.on("publication", (ctx) => {
  const { type, seq, timestamp, data } = ctx.data;

  if (lastSeq > 0 && seq > lastSeq + 1) {
    console.warn(`Gap: ${lastSeq} → ${seq}, call gap fill API`);
  }
  lastSeq = seq;

  if (type === "trade") {
    const { trade_id, price, size, liquidity_indicator } = data;
  }
});

sub.subscribe();
client.connect();

Notes

  • No authentication required — channels are public (addresses are on-chain)
  • Subscribe delivers snapshot — all active (resting) orders returned on subscribe
  • seq for gap detection — monotonically increasing per address, use to detect missed events
  • Gap fill REST API — recover missed events for up to 1 hour by seq range
  • Full-state events — each event contains all fields, not deltas
  • Multi-order batches — when an order event contains multiple orders, data is an array; single orders are unwrapped to a plain object

Related channels

Channels providing related data.

For perps trading

ChannelWhy
Perps BBOCurrent perps top-of-book.
Perps L2Full perps depth showing where resting orders sit in the book.
Funding RateFunding rate for the perps instrument.
Index PriceSpot index. Compare against index_price in trade payloads.
Trades (Perps)Public perps trade tape. Cross-reference with private trade events.

For options trading

ChannelWhy
Options BBOCurrent options top-of-book with mark price and Greeks.
Options L2Full options depth with per-level IV.
SVI SurfaceVol surface parameters for the same maturity.
Interest RateRisk-free rate. Options fills create rho exposure.
Trades (Options)Public options trade tape. Cross-reference with private trade events.