Skip to main content

Fast Order Response SBE

MMWS only

This channel is available only via your dedicated Market Maker WebSocket (MMWS) host. It is not accessible from standard WebSocket endpoints.

Overview

The Fast Order SBE channel provides ultra-low-latency order push updates for HFT clients through the Market Maker WebSocket (MMWS). It delivers binary-encoded SBE messages directly from the matching engine for fast order submission, amendment, and cancellation acknowledgements.

This channel is designed for speed and efficiency. For details on which events trigger a push, see the Push Logic section below.

Release Schedule

ProductTestnetMainnet
Spot
Futures (linear & inverse)
OptionsJune 16, 2026July 21, 2026

Connection

EnvironmentURL
Testnetwss://stream-testnet.bybits.org/v5/private-sbe
Mainnetwss://<your-dedicated-MMWS-host>.bybit-aws.com/v5/private-sbe
  • SBE messages are sent as binary frames (opcode = 2).
  • Control frames (auth, ping/pong, subscribe/unsubscribe) use the standard Bybit V5 API JSON format.

Authentication

Authentication is required immediately after establishing a connection.

Auth Request

{
"req_id": "10001",
"op": "auth",
"args": [
"api_key",
1662350400000,
"signature"
]
}
FieldDescription
req_idOptional client identifier
args[1]Timestamp — must be greater than current time
args[2]Generated using the Bybit API signing algorithm

Auth Success Response

{
"success": true,
"ret_msg": "",
"op": "auth",
"conn_id": "cejreaspqfh3sjdnldmg-p"
}

Heartbeat

Send Ping

{"req_id": "100001", "op": "ping"}

Receive Pong

{
"success": true,
"ret_msg": "pong",
"conn_id": "465772b1-7630-4fdc-a492-e003e6f0f260",
"req_id": "100001",
"op": "ping"
}

Subscription

Available Topics

TopicDescription
order.sbe.resp.spotSpot fast order response
order.sbe.resp.linearLinear (USDT/USDC) fast order response
order.sbe.resp.inverseInverse fast order response
order.sbe.resp.optionOptions fast order response

Subscribe Example

{
"op": "subscribe",
"args": ["order.sbe.resp.linear", "order.sbe.resp.spot", "order.sbe.resp.option"]
}

Subscription Acknowledgment

{
"success": true,
"ret_msg": "",
"conn_id": "d30fdpbboasp1pjbe7r0",
"req_id": "abc123",
"op": "subscribe"
}

Push Logic

fast.resp.order messages are actively pushed to clients when the order is initiated by the user (active trading actions: place, amend, cancel).

info

Upon channel restart or re-subscription, pushes start from the latest matching event — the focus is speed, not backfill.

Scenario / EventPush via Fast Order ChannelNotes
Maker order new (accepted / ack)✅ YesAll active actions initiated by client (place / amend / cancel / reject).
Maker order filled / partial filled✅ YesAll active actions initiated by client (place / amend / cancel / reject).
Taker order (active side)✅ YesAll active actions initiated by client (place / amend / cancel / reject).
COT (CloseOnTrigger) order✅ Yes (for triggered order)Triggered COT order acts like a new taker order; if it opens opposite side, orderLinkId="".
RO / ReduceOnly order✅ YesNormal push; if rejected due to cost or position box, rejectReason populated.
Condition / TP-SL triggered order✅ YesOnce condition triggers and order becomes active, it's pushed. orderLinkId="" (empty).
DCP (Disconnect All Protection)✅ YesPushes when DCP forcibly cancels orders on disconnect.
SMP cancel-taker / Cancel Both (Self Match Protection)✅ YesBoth taker/maker side cancellations will be pushed.
SMP cancel-maker✅ YesBoth taker/maker side cancellations will be pushed.
MMP (Market Maker Protection)✅ YesMMP trigger cancels also pushed in fast order channel.
Delist / Contract expiry / Option delivery❌ NoSystem-initiated close; no fast order push.
Order reject (matching / validation reject)✅ YesPushed immediately with rejectReason.
Amend success / reject✅ YesActive amend ack / reject are pushed.
Cancel success / reject✅ YesActive cancel ack / reject are pushed.

Differences from Standard WebSocket Order Channel

The table below highlights behavioral differences between the Fast Order SBE channel (order.sbe.resp.*) and the standard private WebSocket Order channel.

ScenarioFast Order ChannelStandard WS Order ChannelNotes
Conditional order — place / amend / cancel (before trigger)❌ No push✅ PushConditional orders are not sent to the matching engine before being triggered; the matching engine has no pre-trigger order state to push.
TP/SL order — place / amend / cancel (before trigger)❌ No push✅ PushSame reason as above.
Trailing stop order — place / amend / cancel (before trigger)❌ No push✅ PushSame reason as above.
Position liquidation order❌ No push✅ PushNote: liquidation-triggered cancellations are consistent between both channels.
Contract delist cancellation❌ No push✅ PushDelist cancellations are handled internally and not forwarded to the matching engine.
Amend / cancel an order that was fully filled before the request arrivedFast Order: orderStatus=RejectedStandard WS: orderStatus=Fillede.g. qty=10, amend to 12, but 10 lots already filled — Fast Order returns the amend/cancel as Rejected; Standard WS returns Filled.
Normal place / amend / cancel — leavesValue fieldNo value (0)Has valueFast Order always returns 0 for leavesValue on non-spot-market-order-by-value orders.
Pre-market call auction — cancel rejectedorderStatus=RejectedorderStatus=NewDuring the pre-market call auction phase, cancel requests are rejected; Fast Order reflects Rejected while Standard WS reflects New.

OrderLinkId Behavior by Version

Scenario2026 Testnet / MainnetNotes
Active new order (user-initiated)✅ PresentClient-initiated place includes user's orderLinkId.
Amend / Cancel (user-initiated)✅ PresentClient-initiated place includes user's orderLinkId.
Maker→Taker transition (e.g. price amend crosses book)✅ PresentClient-initiated place includes user's orderLinkId.
Active new conditional order (user-initiated)✅ PresentClient-initiated place includes user's orderLinkId.
Position set trading stop order❌ EmptySystem-created, no orderLinkId.

Message Structure (SBE)

templateId = 21000 (FastOrderResp)

Message Header (8 bytes)

FieldTypeSize (bytes)Description
blockLengthuint162Message body length
templateIduint162Fixed = 21000
schemaIduint162Fixed = 1
versionuint162Fixed = 2

Message Body

IDFieldTypeDescription
1categoryuint81=spot, 2=linear, 3=inverse, 4=option
2sideuint81=Buy, 2=Sell
3orderStatusuint8Order state enum. 0=Others, 4=PartiallyFilledAndCancelled, 5=Rejected, 6=New, 7=Cancelled, 8=PartiallyFilled, 9=Filled
4priceExponentint8Decimal places for price. price = mantissa / 10^priceExponent
5sizeExponentint8Decimal places for size
6valueExponentint8Decimal places for value
7rejectReasonuint160 if N/A. See rejectReason mapping
8priceint64Price mantissa (apply priceExponent)
9leavesQtyint64Remaining quantity mantissa (apply sizeExponent)
10leavesValueint64Active only when spot order type is market and marketUnit=quoteCoin; otherwise 0 (apply valueExponent)
11creationTimeint64Order creation timestamp in Fast Order channel (microseconds)
12updatedTimeint64Matching timestamp (microseconds)
13seqint64Cross sequence ID
14symbolIDint32Symbol ID
15liquidityint81=taker, 2=maker when trade, otherwise 0. Added in schema version 1
  • For pre-market perps, only applied to Continuous Trading stage
  • 16amendFlagint80=the feed is not triggered by amend action, 1=triggered by amend action. Added in schema version 2 (prod 21 July)
    17fillQtyint64Order filled quantity mantissa (apply sizeExponent). Added in schema version 2 (prod 21 July)
  • For pre-market perps, only applied to Continuous Trading stage
  • Provides the accumulated filled quantity across multiple executions within a single taker fill
  • 18fillPriceint64Order fill price mantissa (apply priceExponent). Added in schema version 2 (prod 21 July)
  • For pre-market perps, only applied to Continuous Trading stage
  • Provides the last filled price of execution across multiple executions within a single taker fill
  • 19originalQtyint64Order quantity mantissa (apply sizeExponent). Added in schema version 2 (prod 21 July)
    100orderIdvarString8Order ID (UUID)
    101orderLinkIdvarString8Optional; present for user-initiated orders

    rejectReason Mapping

    CodeName
    0EC_NoError
    1EC_Others
    2EC_UnknownMessageType
    3EC_MissingClOrdID
    4EC_MissingOrigClOrdID
    5EC_ClOrdIDOrigClOrdIDAreTheSame
    6EC_DuplicatedClOrdID
    7EC_OrigClOrdIDDoesNotExist
    8EC_TooLateToCancel
    9EC_UnknownOrderType
    10EC_UnknownSide
    11EC_UnknownTimeInForce
    12EC_WronglyRouted
    13EC_MarketOrderPriceIsNotZero
    14EC_LimitOrderInvalidPrice
    15EC_NoEnoughQtyToFill
    16EC_NoImmediateQtyToFill
    17EC_QtyCannotBeZero
    18EC_PerCancelRequest
    19EC_MarketOrderCannotBePostOnly
    20EC_PostOnlyWillTakeLiquidity
    21EC_CancelReplaceOrder
    22EC_InvalidSymbolStatus
    23EC_MarketOrderNoSupportTIF
    24EC_ReachMaxTradeNum
    25EC_InvalidPriceScale
    26EC_BitIndexInvalid
    27EC_StopBySelfMatch
    28EC_BySelfMatch
    29EC_InvalidSmpType
    30EC_CancelByMMP
    31EC_InCallAuctionStatus
    34EC_InvalidUserType
    35EC_InvalidMirrorOid
    36EC_InvalidMirrorUid
    37EC_SymbolNotExist
    38EC_CancelNoActiveOrders
    39EC_MissingUID
    100EC_EcInvalidQty
    101EC_InvalidAmount
    102EC_LoadOrderCancel
    103EC_CancelForNoFullFill
    104EC_MarketQuoteNoSuppSell
    105EC_DisorderOrderID
    106EC_InvalidBaseValue
    107EC_LoadOrderCanMatch
    108EC_SecurityStatusFail
    110EC_ReachRiskPriceLimit
    111EC_OrderNotExist
    112EC_CancelByOrderValueZero
    113EC_CancelByMatchValueZero
    200EC_ReachMarketPriceLimit

    SBE XML Template (Fast Order Response)

    <?xml version="1.0" encoding="UTF-8"?>
    <sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe" xmlns:mbx="https://bybit-exchange.github.io/docs/v5/intro" package="order.fast.sbe" id="1" version="2" semanticVersion="1.0.0" description="Bybit fast order response SBE schema" byteOrder="littleEndian" headerType="messageHeader">
    <types>
    <composite name="messageHeader" description="Template ID and length of message root">
    <type name="blockLength" primitiveType="uint16"/>
    <type name="templateId" primitiveType="uint16"/>
    <type name="schemaId" primitiveType="uint16"/>
    <type name="version" primitiveType="uint16"/>
    </composite>
    <composite name="varString8" description="Variable length UTF-8 string">
    <type name="length" primitiveType="uint8"/>
    <type name="varData" length="0" primitiveType="uint8" semanticType="String" characterEncoding="UTF-8"/>
    </composite>
    </types>
    <!-- Fast order response: active place/cancel/amend acknowledgements -->
    <sbe:message name="FastOrderResp" id="21000">
    <!-- Routing / classification -->
    <field id="1" name="category" type="uint8" description="1=spot, 2=linear, 3=inverse, 4=option"/>
    <!-- Side / status / rejection -->
    <field id="2" name="side" type="uint8" description="1=Buy, 2=Sell"/>
    <field id="3" name="orderStatus" type="uint8" description="Order state enum"/>
    <!-- Price / size (mantissas) with exponents -->
    <field id="4" name="priceExponent" type="int8" description="Decimal places for price"/>
    <field id="5" name="sizeExponent" type="int8" description="Decimal places for size"/>
    <field id="6" name="valueExponent" type="int8" description="Decimal places for value"/>
    <field id="7" name="rejectReason" type="uint16" description="0 if N/A"/>
    <field id="8" name="price" type="int64" mbx:exponent="priceExponent" description="Price mantissa"/>
    <field id="9" name="leavesQty" type="int64" mbx:exponent="sizeExponent" description="Remaining quantity mantissa"/>
    <field id="10" name="leavesValue" type="int64" mbx:exponent="valueExponent" description="Active only when spot order type is market and marketUnit=quoteCoin; otherwise 0."/>
    <!-- Timing -->
    <field id="11" name="creationTime" type="int64" description="Order creation timestamp in Fast order channel(microseconds)"/>
    <field id="12" name="updatedTime" type="int64" description="Matching timestamp (microseconds)"/>
    <field id="13" name="seq" type="int64" description="Cross sequence ID"/>
    <!-- SymbolID -->
    <field id="14" name="symbolID" type="int32" description="Symbol ID"/>
    <!-- Liquidity -->
    <field id="15" name="liquidity" type="int8" sinceVersion="1" description="1=taker, 2=maker when trade, otherwise 0"/>
    <!-- Version 2 add new fields-->
    <field id="16" name="amendFlag" type="int8" sinceVersion="2" description="0=not amended, 1=amended"/>
    <field id="17" name="fillQty" type="int64" sinceVersion="2" description="order Filled quantity mantissa"/>
    <field id="18" name="fillPrice" type="int64" sinceVersion="2" description="order fill price mantissa"/>
    <field id="19" name="originalQty" type="int64" sinceVersion="2" description="order originalorder quantity mantissa"/>
    <!-- Order identifiers -->
    <data id="100" name="orderId" type="varString8" description="Order ID"/>
    <data id="101" name="orderLinkId" type="varString8" description="Optional; present for user-initiated orders"/>
    </sbe:message>
    </sbe:messageSchema>

    Code Example

    package main

    import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/binary"
    "encoding/hex"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "math"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
    )

    // ---------- Config ----------

    const (
    MMWSURLTestnetBybits = "wss://stream-testnet.bybits.org/v5/private-sbe"
    MMWSURLTestnetBybit = "wss://stream-testnet.bybit.com/v5/private-sbe"
    MMWSURLMainnet = "wss://stream.bybit.com/v5/private-sbe"
    )

    // TODO: fill in your real keys
    const (
    APIKey = "YOUR_API_KEY"
    APISecret = "YOUR_API_SECRET"
    )

    var subTopics = []string{
    "order.sbe.resp.spot",
    }

    // ---------- SBE helpers ----------

    func readU8(buf []byte, off *int) (uint8, error) {
    if *off+1 > len(buf) {
    return 0, fmt.Errorf("readU8: out of range")
    }
    v := buf[*off]
    *off++
    return v, nil
    }

    func readI8(buf []byte, off *int) (int8, error) {
    if *off+1 > len(buf) {
    return 0, fmt.Errorf("readI8: out of range")
    }
    v := int8(buf[*off])
    *off++
    return v, nil
    }

    func readU16LE(buf []byte, off *int) (uint16, error) {
    if *off+2 > len(buf) {
    return 0, fmt.Errorf("readU16LE: out of range")
    }
    v := binary.LittleEndian.Uint16(buf[*off : *off+2])
    *off += 2
    return v, nil
    }

    func readI32LE(buf []byte, off *int) (int32, error) {
    if *off+4 > len(buf) {
    return 0, fmt.Errorf("readI32LE: out of range")
    }
    v := int32(binary.LittleEndian.Uint32(buf[*off : *off+4]))
    *off += 4
    return v, nil
    }

    func readI64LE(buf []byte, off *int) (int64, error) {
    if *off+8 > len(buf) {
    return 0, fmt.Errorf("readI64LE: out of range")
    }
    v := int64(binary.LittleEndian.Uint64(buf[*off : *off+8]))
    *off += 8
    return v, nil
    }

    func readVarString8(buf []byte, off *int) (string, error) {
    if *off+1 > len(buf) {
    return "", fmt.Errorf("readVarString8: no length byte")
    }
    ln := int(buf[*off])
    *off++
    if ln == 0 {
    return "", nil
    }
    if *off+ln > len(buf) {
    return "", fmt.Errorf("readVarString8: length out of range")
    }
    s := string(buf[*off : *off+ln])
    *off += ln
    return s, nil
    }

    func applyExp(mantissa int64, exp int8) float64 {
    e := int(exp)
    if e >= 0 {
    return float64(mantissa) / math.Pow10(e)
    }
    return float64(mantissa) * math.Pow10(-e)
    }

    // ---------- Fast Order SBE decode ----------

    type FastOrderSBEResp struct {
    SBEHeader struct {
    BlockLength uint16 `json:"blockLength"`
    TemplateID uint16 `json:"templateId"`
    SchemaID uint16 `json:"schemaId"`
    Version uint16 `json:"version"`
    } `json:"_sbe_header"`

    Category uint8 `json:"category"`
    Side uint8 `json:"side"`
    OrderStatus uint8 `json:"orderStatus"`
    PriceExponent int8 `json:"priceExponent"`
    SizeExponent int8 `json:"sizeExponent"`
    ValExponent int8 `json:"valueExponent"`
    RejectReason uint16 `json:"rejectReason"`

    PriceMantissa int64 `json:"priceMantissa"`
    LeavesQtyMantissa int64 `json:"leavesQtyMantissa"`
    LeavesValueMantissa int64 `json:"leavesValueMantissa"`

    CreationTime int64 `json:"creationTime"`
    UpdatedTime int64 `json:"updatedTime"`
    Seq int64 `json:"seq"`

    SymbolID int32 `json:"symbolID"`
    Liquidity int8 `json:"liquidity"`

    AmendFlag int8 `json:"amendFlag,omitempty"`
    FillQtyMantissa int64 `json:"fillQtyMantissa,omitempty"`
    FillPriceMantissa int64 `json:"fillPriceMantissa,omitempty"`
    OriginalQtyMantissa int64 `json:"originalQtyMantissa,omitempty"`

    OrderID string `json:"orderId"`
    OrderLinkID string `json:"orderLinkId"`

    Price float64 `json:"price"`
    LeavesQty float64 `json:"leavesQty"`
    LeavesValue float64 `json:"leavesValue"`
    FillQty float64 `json:"fillQty,omitempty"`
    FillPrice float64 `json:"fillPrice,omitempty"`
    OriginalQty float64 `json:"originalQty,omitempty"`

    RawOffsetEnd int `json:"_raw_offset_end"`
    }

    func decodeFastOrderResp(payload []byte, debug bool) (*FastOrderSBEResp, error) {
    if len(payload) < 8 {
    return nil, fmt.Errorf("payload too short for SBE header")
    }
    off := 0
    blockLen := binary.LittleEndian.Uint16(payload[off : off+2])
    templateID := binary.LittleEndian.Uint16(payload[off+2 : off+4])
    schemaID := binary.LittleEndian.Uint16(payload[off+4 : off+6])
    version := binary.LittleEndian.Uint16(payload[off+6 : off+8])
    off += 8

    if debug {
    log.Printf("HEADER: block_len=%d, template_id=%d, schema_id=%d, version=%d",
    blockLen, templateID, schemaID, version)
    }

    if templateID != 21000 {
    return nil, fmt.Errorf("unexpected templateId: %d", templateID)
    }

    var err error
    resp := &FastOrderSBEResp{}
    resp.SBEHeader.BlockLength = blockLen
    resp.SBEHeader.TemplateID = templateID
    resp.SBEHeader.SchemaID = schemaID
    resp.SBEHeader.Version = version

    bodyStart := off // fixed block begins here; var-length fields follow at bodyStart+blockLen

    if resp.Category, err = readU8(payload, &off); err != nil {
    return nil, err
    }
    if resp.Side, err = readU8(payload, &off); err != nil {
    return nil, err
    }
    if resp.OrderStatus, err = readU8(payload, &off); err != nil {
    return nil, err
    }
    if resp.PriceExponent, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    if resp.SizeExponent, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    if resp.ValExponent, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    if resp.RejectReason, err = readU16LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.PriceMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.LeavesQtyMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.LeavesValueMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.CreationTime, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.UpdatedTime, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.Seq, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    if resp.SymbolID, err = readI32LE(payload, &off); err != nil {
    return nil, err
    }
    // field 15 liquidity (sinceVersion=1) — present when blockLen covers it
    if off-bodyStart < int(blockLen) {
    if resp.Liquidity, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 16 amendFlag (sinceVersion=2)
    if off-bodyStart < int(blockLen) {
    if resp.AmendFlag, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 17 fillQty (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.FillQtyMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 18 fillPrice (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.FillPriceMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 19 originalQty (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.OriginalQtyMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // skip any future fixed fields we don't know about
    off = bodyStart + int(blockLen)
    if resp.OrderID, err = readVarString8(payload, &off); err != nil {
    return nil, err
    }
    if resp.OrderLinkID, err = readVarString8(payload, &off); err != nil {
    return nil, err
    }

    resp.Price = applyExp(resp.PriceMantissa, resp.PriceExponent)
    resp.LeavesQty = applyExp(resp.LeavesQtyMantissa, resp.SizeExponent)
    resp.LeavesValue = applyExp(resp.LeavesValueMantissa, resp.ValExponent)
    resp.FillQty = applyExp(resp.FillQtyMantissa, resp.SizeExponent)
    resp.FillPrice = applyExp(resp.FillPriceMantissa, resp.PriceExponent)
    resp.OriginalQty = applyExp(resp.OriginalQtyMantissa, resp.SizeExponent)
    resp.RawOffsetEnd = off

    return resp, nil
    }

    // ---------- WebSocket helpers ----------

    func sendJSON(conn *websocket.Conn, v any) error {
    data, err := json.Marshal(v)
    if err != nil {
    return err
    }
    return conn.WriteMessage(websocket.TextMessage, data)
    }

    func signAuth(secret, value string) string {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(value))
    return hex.EncodeToString(h.Sum(nil))
    }

    func heartbeat(ctx context.Context, conn *websocket.Conn) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    for {
    select {
    case <-ctx.Done():
    return
    case <-ticker.C:
    reqID := fmt.Sprintf("%d", time.Now().UnixMilli())
    err := sendJSON(conn, map[string]any{
    "req_id": reqID,
    "op": "ping",
    })
    if err != nil {
    log.Printf("[heartbeat] error sending ping: %v", err)
    return
    }
    }
    }
    }

    // ---------- Main run ----------

    func run(ctx context.Context, url string) error {
    dialer := websocket.Dialer{
    HandshakeTimeout: 10 * time.Second,
    EnableCompression: false,
    }

    conn, _, err := dialer.Dial(url, nil)
    if err != nil {
    return fmt.Errorf("dial error: %w", err)
    }
    defer conn.Close()
    log.Printf("Connected to %s", url)

    expires := (time.Now().Unix() + 10000) * 1000
    val := fmt.Sprintf("GET/realtime%d", expires)
    sig := signAuth(APISecret, val)

    authMsg := map[string]any{
    "req_id": "10001",
    "op": "auth",
    "args": []any{APIKey, expires, sig},
    }
    if err := sendJSON(conn, authMsg); err != nil {
    return fmt.Errorf("send auth error: %w", err)
    }

    if _, msg, err := conn.ReadMessage(); err != nil {
    return fmt.Errorf("read auth ack error: %w", err)
    } else {
    log.Printf("auth-ack: %s", string(msg))
    }

    subMsg := map[string]any{
    "op": "subscribe",
    "args": subTopics,
    }
    if err := sendJSON(conn, subMsg); err != nil {
    return fmt.Errorf("send subscribe error: %w", err)
    }

    hbCtx, hbCancel := context.WithCancel(ctx)
    defer hbCancel()
    go heartbeat(hbCtx, conn)

    for {
    select {
    case <-ctx.Done():
    log.Printf("context canceled, exit read loop")
    return nil
    default:
    }

    mt, data, err := conn.ReadMessage()
    if err != nil {
    return fmt.Errorf("read message error: %w", err)
    }

    switch mt {
    case websocket.BinaryMessage:
    resp, err := decodeFastOrderResp(data, false)
    if err != nil {
    log.Printf("binary decode error: %v", err)
    } else {
    j, _ := json.Marshal(resp)
    log.Printf("FAST_ORDER_SBE: %s", string(j))
    }
    case websocket.TextMessage:
    var obj map[string]any
    if err := json.Unmarshal(data, &obj); err != nil {
    log.Printf("text-nonjson: %s", string(data))
    continue
    }
    if op, ok := obj["op"].(string); ok && op == "pong" {
    continue
    }
    j, _ := json.Marshal(obj)
    log.Printf("control: %s", string(j))
    default:
    log.Printf("unknown message type %d", mt)
    }
    }
    }

    // ---------- Entry ----------

    func main() {
    url := flag.String("url", MMWSURLTestnetBybits, "WebSocket URL")
    flag.Parse()

    if APIKey == "YOUR_API_KEY" || APISecret == "YOUR_API_SECRET" {
    log.Println("⚠️ Please set APIKey and APISecret in the source before running.")
    }

    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    if err := run(ctx, *url); err != nil {
    log.Fatalf("run error: %v", err)
    }
    }