# BingX User Stream — VST Probe Notes (Phase 0) **Date:** 2026-06-01 **Scope:** VST only (no LIVE touch). **Result: Outcome A — VST has WebSocket. Full WS-on-both symmetry is achievable.** --- ## Gate G0 resolution | Check | Result | |---|---| | listenKey endpoint (`POST /openApi/user/auth/userDataStream`) | ✅ Returns `listenKey` (signed request, `signed_post_raw`) | | Signing method | ✅ Standard HMAC-SHA256 signed POST works — "header-only/unsigned" concern was unfounded | | WS URL | `wss://vst-open-api-ws.bingx.com/swap-market?listenKey=` | | Frames delivered | ✅ 667 SNAPSHOT frames in 20 s (idle session, no active orders) | | Gzip | Binary frames are gzip-compressed — `gzip.decompress(bytes(msg.data))` | | Ping/Pong | Server sends text `"Ping"` → client must respond with `"Pong"` | | listenKey keepalive | `PUT /openApi/user/auth/userDataStream {"listenKey": ...}` | | listenKey delete | `DELETE /openApi/user/auth/userDataStream {"listenKey": ...}` | --- ## Event schemas ### `SNAPSHOT` — position/leverage state (received continuously) ```json {"e":"SNAPSHOT","E":1780336019559,"ac":{"s":"MTL-USDT","l":1,"S":1,"mt":"isolated"}} ``` | Field | Meaning | |---|---| | `e` | `"SNAPSHOT"` | | `E` | Server timestamp ms | | `ac.s` | Symbol | | `ac.l` | Long leverage | | `ac.S` | Short leverage | | `ac.mt` | Margin type (`"isolated"`) | ### `ORDER_TRADE_UPDATE` — fill/order status (arrives on trade activity) Top-level envelope: `{"e":"ORDER_TRADE_UPDATE","E":,"o":{...}}` Inner `o` object: | Field | Meaning | |---|---| | `s` | Symbol | | `c` | clientOrderId | | `i` | orderId (venue) | | `X` | Order status (`NEW`, `PARTIALLY_FILLED`, `FILLED`, `CANCELED`) | | `x` | Execution type | | `p` | Order price | | `ap` | Average fill price | | `z` | Cumulative filled qty (total filled so far) | | `l` | **lastFilledQty — incremental fill for this event** | | `L` | Last fill price | | `n` | Commission amount | | `N` | Commission asset | **Critical:** `z` is cumulative; `l` is incremental per-event. `bingx_venue.py:582` reads `lastFilledQty` = `l`. The Rust kernel's `apply_fill` now accumulates (`slot.size += l`). ### `ACCOUNT_UPDATE` — balance/position push (arrives on trade activity) Top-level: `{"e":"ACCOUNT_UPDATE","E":,...}` Balance array (`B`): `[{"a":"USDT","wb":,"cw":}]` Position array (`P`): `[{"s":,"pa":,"ep":,"up":,"mt":,"ps":}]` ### `FUNDING_FEE` — funding charge (arrives on funding interval) Envelope: `{"e":"FUNDING_FEE","E":,"fs":{"s":,"fa":,"a":}}` Identified by `m == "FUNDING_FEE"` in some variants, or `e == "FUNDING_FEE"`. --- ## VST ↔ LIVE symmetry notes - Same `POST /openApi/user/auth/userDataStream` endpoint, same signing method - VST WS base: `wss://vst-open-api-ws.bingx.com/swap-market` - LIVE WS base: `wss://open-api-swap.bingx.com/swap-market` - Only difference: base hostname — **all frame schemas are identical** - `bingx_user_stream.py` must use `base_url_ws_private` from config (already in `BingxExecClientConfig`) --- ## listenKey lifecycle ``` POST /openApi/user/auth/userDataStream {} → {"listenKey": "..."} PUT /openApi/user/auth/userDataStream {"listenKey":..} → {} (keepalive, every 1800s) DELETE /openApi/user/auth/userDataStream {"listenKey":..} → {} (on close) ``` listenKey TTL: ~60 min. Keepalive extends it. Server signals expiry via `{"e":"listenKeyExpired"}`. --- ## Open items for Phase 2 - `executionReport` schema: confirmed from BLUE observer.py analysis; verify against live VST fill when first Phase 2 order is placed - `ACCOUNT_UPDATE` balance fields: `wb` (wallet balance), `cw` (cross wallet balance) - Funding fee `fs.fa` sign convention (positive = received, negative = paid) — to verify - 24h connection cap: BingX closes the socket after ~24h regardless of keepalive; overlap-rotation strategy required (open new connection before closing old)