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:
- On subscribe — the server returns a snapshot of all active (resting) orders
- Live events — each event has a monotonically increasing
seqper address - Gap detection — if client sees
seqjump (e.g. 5 → 8), events 6 and 7 were missed - Gap fill — call the REST endpoint to recover missed events by
seqrange
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.
| Tag | Values | Description |
|---|---|---|
event | post_order, cancel_order, update_order, trade, mmp_triggered | Filter 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
| Event | Source | When |
|---|---|---|
post_order | New limit order placed | Order enters the book |
cancel_order | Order cancelled | User cancel or expiry |
update_order | Order state changed | Partial fill updates filled_amount and order_state |
trade | Execution (fill) | Taker or maker side of a trade |
mmp_triggered | Market maker protection | MMP 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": { ... }
}| Field | Type | Description |
|---|---|---|
type | string | Event type (see table above) |
seq | integer | Per-address sequence number, monotonically increasing. Use for gap detection. |
timestamp | integer | Event timestamp in epoch milliseconds |
data | object | array | Event payload (see below). Array when batch contains multiple orders. |
Order Events (post_order, cancel_order, update_order)
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
}
}| Field | Type | Description |
|---|---|---|
order_id | string | Unique order identifier |
user_id | string | absent | Optional client-supplied reference (max 36 chars). Omitted if not set. |
instrument | string | Instrument name (e.g. ETH_USDC-PERPETUAL, BTC_USDC-31OCT25-130000-C) |
direction | string | "buy" or "sell" |
price | number | null | Limit price. May be null on cancel_order. |
size | number | null | Order size — unified from contracts (options) and amount (perps). May be null on cancel_order. |
order_type | string | null | "good_til_cancelled", "fill_or_kill", "immediate_or_cancel" |
order_state | string | null | "open", "filled", "cancelled", "partially_filled" |
filled_amount | number | null | Cumulative filled size. May be null on cancel_order. |
post_only | boolean | null | Post-only flag |
mmp | boolean | null | Whether this order is covered by MMP |
liquidation | boolean | null | Whether 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
}
}| Field | Type | Description |
|---|---|---|
trade_id | string | Unique trade identifier |
order_id | string | The order that was filled |
instrument | string | Instrument name |
direction | string | "buy" or "sell" |
liquidity_indicator | string | "taker" (aggressor) or "maker" (resting order) |
price | number | Execution price |
size | number | Fill size |
index_price | number | Index price at time of execution |
liquidation | boolean | Whether this fill is part of a liquidation |
taker_fee | number | Taker fee charged |
maker_fee | number | Maker fee/rebate (negative = rebate) |
systemic_risk_fee | number | Systemic risk fee |
liquidation_fee | number | Liquidation fee (0 if not a liquidation) |
realised_funding | number | Realised 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
}
}| Field | Type | Description |
|---|---|---|
pair | string | Pair symbol (e.g. "ETH", "BTC") |
frozen_until | integer | null | Unix timestamp (seconds) when MMP freeze ends |
frozen_duration_seconds | integer | null | Duration 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) andcontracts(options) are renamed tosizeinstrument_nameis renamed toinstrumentfillable_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:
| Param | Description |
|---|---|
address | Ethereum address (e.g. 0xAA836669...) |
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
after_seq | integer | — | Return events with seq > N (exclusive lower bound) |
from_seq | integer | — | Return events with seq >= N (inclusive lower bound) |
to_seq | integer | — | Return events with seq <= N (inclusive upper bound) |
limit | integer | 100 | Max 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
seqfor gap detection — monotonically increasing per address, use to detect missed events- Gap fill REST API — recover missed events for up to 1 hour by
seqrange - Full-state events — each event contains all fields, not deltas
- Multi-order batches — when an order event contains multiple orders,
datais an array; single orders are unwrapped to a plain object
Related channels
Channels providing related data.
For perps trading
| Channel | Why |
|---|---|
| Perps BBO | Current perps top-of-book. |
| Perps L2 | Full perps depth showing where resting orders sit in the book. |
| Funding Rate | Funding rate for the perps instrument. |
| Index Price | Spot index. Compare against index_price in trade payloads. |
| Trades (Perps) | Public perps trade tape. Cross-reference with private trade events. |
For options trading
| Channel | Why |
|---|---|
| Options BBO | Current options top-of-book with mark price and Greeks. |
| Options L2 | Full options depth with per-level IV. |
| SVI Surface | Vol surface parameters for the same maturity. |
| Interest Rate | Risk-free rate. Options fills create rho exposure. |
| Trades (Options) | Public options trade tape. Cross-reference with private trade events. |
Updated about 2 months ago
