User Data WebSocket Stream
Private WebSocket stream for real-time positions, open orders, and account balances on GaiaEx. Requires API key authentication.
Connection
WS wss://openapi.gaiaex.com/ws/user/{address}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
address | string | User's wallet address |
Authentication
The WebSocket accepts either an API-key HMAC signature (for bots and algo clients) or a session token (for mobile/web app sessions). HMAC is recommended for order-submission workloads because it amortises the TLS + HTTP-upgrade cost across every order sent on the same connection.
Option A: HMAC (API keys, recommended for trading)
Send the same three X-GAIAEX-* headers you use for REST during the WebSocket upgrade. Signing recipe on the handshake is ts + "GET" + path (body empty), where path is /ws/user/{address} exactly. The API key must carry the trade permission to submit order actions.
X-GAIAEX-APIKEY: <api_key>
X-GAIAEX-TIMESTAMP: <unix_ms>
X-GAIAEX-SIGNATURE: HMAC_SHA256(api_secret, <ts> + "GET" + "/ws/user/<address>")Option B: Session token (mobile/web app)
Subprotocol header (recommended):
Sec-WebSocket-Protocol: Bearer.<session_token>Authorization header (where supported by your WS client):
Authorization: Bearer <session_token>
Connection Example (Python)
import asyncio
import websockets
import json
SESSION_TOKEN = "your_session_token"
ADDRESS = "0xYourAddress"
async def listen():
uri = f"wss://openapi.gaiaex.com/ws/user/{ADDRESS}"
async with websockets.connect(
uri,
subprotocols=[f"Bearer.{SESSION_TOKEN}"]
) as ws:
async for message in ws:
data = json.loads(message)
if data["type"] == "update":
print(f"Balance: {data['balance']['available']}")
print(f"Positions: {len(data['positions'])}")
print(f"Open orders: {len(data['orders'])}")
asyncio.run(listen())Connection Example (JavaScript)
const ADDRESS = '0xYourAddress';
const SESSION_TOKEN = 'your_session_token';
const ws = new WebSocket(
`wss://openapi.gaiaex.com/ws/user/${ADDRESS}`,
[`Bearer.${SESSION_TOKEN}`]
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'update') {
console.log('Balance:', data.balance.available);
console.log('Positions:', data.positions.length);
console.log('Orders:', data.orders.length);
}
};Message Types
Update (type: "update")
Pushed whenever positions, orders, or balances change. Also sent periodically as a heartbeat even if no changes occurred.
{
"type": "update",
"positions": [
{
"coin": "ETH",
"szi": "0.1",
"entryPx": "3500.00",
"positionValue": "350.00",
"unrealizedPnl": "12.50",
"leverage": { "type": "cross", "value": 10 },
"liquidationPx": "3150.00"
}
],
"orders": [
{
"orderId": 987654321,
"symbol": "ETH",
"side": "BUY",
"type": "LIMIT",
"origQty": "0.1",
"price": "3400.00",
"status": "NEW"
}
],
"spot_orders": [],
"balance": {
"usdc_balance": "1000.00",
"hl_confirmed_bal": "1000.00",
"pending_deposit": "0.00",
"available": "850.00",
"unrealized_pnl": "12.50",
"margin_used": "150.00",
"maintenance_margin": "35.00",
"total_ntl_pos": "350.00",
"cross_margin_ratio": "0.041",
"cross_account_leverage": "0.35"
},
"spot_balances": [
{
"coin": "USDC",
"total": "500.00",
"hold": "0.00",
"available": "500.00"
}
],
"timestamp": 1743508800000,
"sequence": 42
}| Field | Type | Description |
|---|---|---|
positions | array | All open perpetual positions (empty array if none) |
orders | array | All open perpetual orders |
spot_orders | array | All open spot orders |
balance | object | Perpetual account balance summary |
spot_balances | array | Spot token balances |
timestamp | int | Server timestamp in ms |
sequence | int | Monotonically increasing sequence number per connection |
Heartbeat (type: "heartbeat")
Sent when the data feed is temporarily unavailable. Confirms the connection is alive.
{
"type": "heartbeat",
"address": "0xYourAddress",
"timestamp": 1743508800000,
"sequence": 43
}Error (type: "error")
Sent when a temporary data fetch error occurs. The connection remains open and will resume normal updates.
{
"type": "error",
"message": "Temporary data fetch error. Retrying..."
}Order Actions via WebSocket
You can also place and manage orders through the WebSocket connection, avoiding the overhead of establishing new HTTP connections.
Sending an Order Action
Send a JSON message with an action field:
{
"action": "place_order",
"client_id": "my-order-001",
"data": {
"user_address": "0xYourAddress",
"symbol": "ETH",
"is_buy": true,
"size": "0.1",
"price": "3500.00",
"order_type": "limit"
}
}Supported Actions
| Action | Description |
|---|---|
place_order | Place a new order |
cancel_order | Cancel an order by ID |
cancel_all | Cancel all orders (optionally by symbol) |
close_position | Close a position |
set_tpsl | Set take-profit / stop-loss |
modify_order | Modify an existing order |
Response Messages
Acknowledgement (immediate):
{
"type": "order_ack",
"client_id": "my-order-001",
"action": "place_order",
"timestamp": 1743508800000
}Result (after processing):
{
"type": "order_result",
"client_id": "my-order-001",
"success": true,
"data": {
"orderId": 987654322,
"status": "open",
"symbol": "ETH"
}
}Error result:
{
"type": "order_result",
"client_id": "my-order-001",
"success": false,
"error": {
"code": 400,
"detail": "Insufficient margin"
}
}Ping / Pong
To keep the connection alive, send a text message "ping". The server will respond with "pong".
→ "ping"
← "pong"TIP
Most WebSocket libraries handle ping/pong frames automatically. The text-based ping/pong is an additional application-level mechanism.
Terminal Walkthrough
Step 1: Connect with wscat
# Replace with your actual address and session token
$ wscat -c wss://openapi.gaiaex.com/ws/user/0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD \
-s "Bearer.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Connected (press CTRL+C to quit)
# Initial state snapshot arrives immediately:
< {"type":"update","positions":[{"coin":"ETH","szi":"0.5","entryPx":"3400.00",...}],"orders":[],"balance":{"available":"850.00",...},"timestamp":...}
# Place an order via WebSocket:
> {"action":"place_order","client_id":"test-001","data":{"user_address":"0xA6E3...","symbol":"ETH","is_buy":true,"size":"0.1","price":"3450.00","order_type":"limit"}}
< {"type":"order_ack","client_id":"test-001","action":"place_order","timestamp":...}
< {"type":"order_result","client_id":"test-001","success":true,"data":{"orderId":123456,"status":"open"}}
# Updated state with the new order:
< {"type":"update","positions":[...],"orders":[{"orderId":123456,"symbol":"ETH","side":"BUY","price":"3450.00",...}],...}
# Cancel the order:
> {"action":"cancel_order","client_id":"test-002","data":{"user_address":"0xA6E3...","symbol":"ETH","order_id":123456}}
< {"type":"order_result","client_id":"test-002","success":true,...}Step 2: Python — Full Event Loop with Order Management
import asyncio
import json
import websockets
SESSION_TOKEN = "your_session_token"
ADDRESS = "0xYourAddress"
async def trading_loop():
uri = f"wss://openapi.gaiaex.com/ws/user/{ADDRESS}"
async with websockets.connect(uri, subprotocols=[f"Bearer.{SESSION_TOKEN}"]) as ws:
# Send keepalive pings
async def ping_loop():
while True:
await ws.send("ping")
await asyncio.sleep(20)
ping_task = asyncio.create_task(ping_loop())
try:
# Place order via WebSocket
order_msg = json.dumps({
"action": "place_order",
"client_id": "bot-001",
"data": {
"user_address": ADDRESS,
"symbol": "ETH",
"is_buy": True,
"size": "0.05",
"price": "3400.00",
"order_type": "limit",
},
})
await ws.send(order_msg)
print("Order sent")
# Process all messages
async for raw in ws:
if raw == "pong":
continue
msg = json.loads(raw)
if msg["type"] == "order_ack":
print(f"ACK: {msg['client_id']}")
elif msg["type"] == "order_result":
if msg["success"]:
print(f"Order filled/placed: {msg['data']}")
else:
print(f"Order error: {msg['error']}")
elif msg["type"] == "update":
bal = msg.get("balance", {})
print(f"Balance: {bal.get('available', 'N/A')} | "
f"Positions: {len(msg.get('positions', []))} | "
f"Orders: {len(msg.get('orders', []))}")
finally:
ping_task.cancel()
asyncio.run(trading_loop())