WebSocket
Environment
| Environment | Base URL |
|---|---|
| Production | wss://api-studio.r25.xyz |
Changelog
Dates use the YYYY-MM-DD format, listed in descending order. The date of a historical entry reflects when the document change was submitted, not necessarily when the interface went live.
| Date | Change |
|---|---|
| 2026-10-30 | Added: the market data subscription market.orderbook (Section 10, sources HYPERLIQUID / STOVE); server-initiated marginCall.alert margin-level warning push (Section 11). |
1. Document Overview & Capabilities
This document is for external partners integrating the OpenAPI trading WebSocket interface. It describes the trading endpoints exposed in the current version, market data subscriptions, the server-initiated margin-call warning push, the handshake signing method, the message protocol, and the callback structure. The Host, Client ID, and signature in the examples are placeholders; replace them with the values assigned by the platform or agreed upon by both parties.
The same AK/SK can be used for both WebSocket and REST: WebSocket handles trading write operations and real-time subscription pushes; for scenarios without a persistent connection, you can use the equivalent REST endpoints for order create/cancel and position close (see the REST documentation).
Current WebSocket capabilities include:
- Trading writes:
order.create,order.cancel,position.close; currently connected toHYPERLIQUIDandHABITTRADE. - Trading queries:
order.query,position.query. - Real-time subscriptions:
order.updatedandposition.updatedtrading topics, pushed per vault frame. - Market data subscriptions: the
market.orderbook(order book) topic on data sourcesHYPERLIQUID(perp/spot) andSTOVE(US equities etc.); see Section 10. - Server-initiated push:
marginCall.alertmargin-level warning; no subscription required, automatically pushed forHYPERLIQUIDvaults operable by the current AK once connected; see Section 11. - Account-level authorization:
data.vaultAddressof every business op must belong to a vault operable by the current AK, otherwiseVAULT_NOT_PERMITTED.
2. Base URL, Content-Type, Time/Number/Null Conventions
2.1 General Conventions
- Base URL: pick the corresponding
wssaddress from the environment table at the top; the trading endpoint path is fixed at/api/openapi/v1/ws. - Transport: UTF-8 JSON text frames; binary frames are closed. WebSocket does not use
Content-Typeor a request body. - Uplink frame limit: 5 KiB (exceeding returns
UPLINK_TOO_LARGE, connection not closed); exceeding 64 KiB closes the connection (code1009). - Authentication signs once during the handshake only; after the connection is established, no further auth is needed (see §4).
- Handshake URL query params (
clientId/timestamp/nonce/signature) should be UTF-8 percent-encoded. - This document only covers the WebSocket protocol available to partners (trading, market data subscriptions, and margin-call warning pushes).
2.2 Time Conventions
- Handshake
timestamp: Unix seconds (string); acceptsyyyy-MM-dd HH:mm:ssor a plain number (≥1e12 treated as milliseconds), valid window ±300 seconds. - Uplink request
ts: Unix millisecond timestamp, must be strictly increasing (ts≤ the cached previousts→ dropped withREQUEST_TS_STALE). - Downlink response/event
ts,acceptedAt, and order/positioncreatedAt/updatedAt/exchangeTime/serverTime: all epoch milliseconds.
2.3 Number & Null Conventions
- Trading numbers (quantity, price, leverage, slippage, USD value, fee, etc.) use decimal strings; do not parse them with lossy types such as JavaScript
Number.leverage.value/rawUsd,maxSlippageBps, etc. are also decimal strings. - Quantity must be a multiple of
sizeStepand limitpricea multiple ofpriceTick(see the overview table in §3); otherwiseINVALID_SIZE_STEP/INVALID_PRICE_TICKis returned. - Price fields are in USD per asset unless otherwise noted.
- Downlink serialization omits fields whose value is
nullby default (see §5 Message Protocol).
3. Overview
This document is for external partners integrating the OpenAPI trading WebSocket interface. The WebSocket interface supports order create/query/cancel, position query/close, real-time order/position push subscriptions, the order book market data subscription, and the server-initiated margin-call warning push. The same AK/SK can be used for both WebSocket and REST.
| Item | Value |
|---|---|
| WS endpoint | wss://<host>/api/openapi/v1/ws |
| Transport | UTF-8 JSON text frames (binary frames are closed) |
| Protocol version | v field fixed to "1.0.0" |
| Exchanges | HYPERLIQUID (HL, perpetual contracts) / HABITTRADE (HT, spot rebalancing vault) |
| Business ops | order.create / order.query / order.cancel / position.query / position.close |
| Trading subscription topics | order.updated / position.updated (type=subscribe, op is the topic) |
| Market data sources | HYPERLIQUID (perp/spot) / STOVE (US equities etc.) |
| Market data topic | market.orderbook (order book, see Section 10) |
| Server-initiated push | marginCall.alert margin-level warning (no subscription required, see Section 11) |
| Quantity precision | HL sizeStep=0.001; HT sizeStep=1 (must be a multiple, otherwise INVALID_SIZE_STEP) |
| Price precision | HL priceTick=0.1; HT priceTick=0.01 (limit price must be a multiple) |
Only one active connection is allowed per AK: opening a new connection with the same AK closes the old one (code 4001). The new connection does not inherit the old connection's subscriptions or in-flight requests.
4. Authentication & Handshake Signing
WebSocket reuses the HTTP OpenAPI AK/SK: clientId is the AK and the Ed25519 private key is the SK. Signing happens only at the handshake; no auth afterwards.
Handshake Signing
wss://<host>/api/openapi/v1/ws?clientId=<AK>×tamp=<ts>&nonce=<nonce>&signature=<sig>
| Param | Description |
|---|---|
clientId | AK (same Access Key as HTTP OpenAPI) |
timestamp | Unix seconds (string). Accepts yyyy-MM-dd HH:mm:ss or a plain number (≥1e12 treated as ms). Valid window ±300 seconds |
nonce | Random string (UUID recommended), one-time, must not repeat within 300 seconds |
signature | Ed25519 signature of the string to sign, lowercase hex (64 bytes ≡ 128 chars) |
String to sign (three lines separated by \n):
<clientId>\n<timestamp>\n<nonce>
i.e. the UTF-8 bytes of clientId + "\n" + timestamp + "\n" + nonce are signed with Ed25519 and hex-encoded.
Any check failure → the handshake returns HTTP 401 and does not upgrade to WS. After a successful handshake, no further auth is required for the connection.
5. Message Protocol
Serialization omits null fields. Allowed uplink type values: request / subscribe / unsubscribe.
5.1 Uplink Request
{
"v": "1.0.0",
"type": "request",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"exchange": "HYPERLIQUID",
"data": { }
}
| Field | Required | Description |
|---|---|---|
v | Yes | Protocol version, fixed 1.0.0 |
type | Yes | request / subscribe / unsubscribe |
id | Yes | Request ID, unique per connection, used to match responses. Reuse on the same connection → DUPLICATE_REQUEST_ID |
op | Yes | For request, the business op; for subscribe/unsubscribe, the topic/fixed string |
ts | Yes | Client time (ms), must be strictly increasing (ts ≤ cached previous ts → REQUEST_TS_STALE dropped) |
exchange | No | HYPERLIQUID / HABITTRADE; omit only for order.query/position.query cross-exchange merged query |
data | Depends on op | Business params of each op, see §7/§8 |
If a required protocol field (
v/type/id/op) is missing or invalid, anINVALID_REQUESTerror response is returned; the connection is not closed.The uplink frame limit is 5 KiB; exceeding returns
UPLINK_TOO_LARGE(connection not closed); exceeding 64 KiB closes the connection (code1009).
5.2 Downlink Response
{
"v": "1.0.0",
"type": "response",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"code": "SUCCESS",
"message": "accepted",
"data": { }
}
code is an error code name (see §12); code=SUCCESS means success.
Error responses (code ≠ SUCCESS) echo the request's id and op; when not provided in the request they are null — for request-response matching and troubleshooting:
{
"v": "1.0.0",
"type": "response",
"id": "req-1",
"op": "order.create",
"ts": 1787623690000,
"code": "INVALID_REQUEST",
"message": "unsupported type: subscribe2",
"data": { }
}
5.3 Downlink Event (Subscription Push)
{
"v": "1.0.0",
"type": "event",
"topic": "position.updated",
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"seq": 41,
"exchange": "HYPERLIQUID",
"ts": 1787638868358,
"data": { }
}
seq is monotonically increasing within a connection, used by the client for dedup/alignment, reset to zero on reconnect.
6. Connection, Rate Limit, Heartbeat & Close Codes
| Convention | Default | Description |
|---|---|---|
| Max uplink frame | 5 KiB | Exceeds → UPLINK_TOO_LARGE; over 64 KiB → 1009 |
| Max downlink frame | 64 KiB | Exceeds → dropped |
| In-flight requests per connection | 10 | Duplicate id → DUPLICATE_REQUEST_ID; full → TOO_MANY_INFLIGHT_REQUESTS |
| Max trading subscriptions per connection | 4 | order.updated / position.updated; re-subscribing the same exchange+topic overwrites the old (does not add quota) |
| Max market data subscriptions per connection | 200 | Total market.orderbook subscriptions; max 50 tickers per subscribe request, over-limit rejects the whole batch (see §10) |
| Heartbeat Ping period | 30s | Server sends Ping periodically |
| Pong timeout | 60s | No Pong for 60s → close code 4002 |
Rate limit (clientId + IP dimensions): IP 60/60s; clientId 120/1h. Either hit → RATE_LIMIT.
Close codes:
| Code | Meaning |
|---|---|
| 1000 | Normal close |
| 1008 | Auth/config policy rejection; binary frame; transport error |
| 1009 | Uplink frame over 64 KiB |
| 4001 | Replaced by a new connection on the same AK |
| 4002 | Pong timeout (no Pong for 60s) |
7. HYPERLIQUID (HL) Business Endpoints
General:
data.vaultAddressis required for every business op; the server checks whether it is operable by the current AK. No permission →VAULT_NOT_PERMITTED;data.deniedVaultAddresseslists the denied addresses.vaultAddressis case-insensitive.
7.1 order.create
HL supports MARKET and LIMIT order types.
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Yes | Coin |
side | Yes | BUY / SELL |
orderType | Yes | MARKET / LIMIT (other → INVALID_REQUEST) |
quantity | Yes | Coin quantity, must be a multiple of sizeStep (0.001) |
leverage | No | Leverage |
isCross | No | Cross-margin flag: true = cross / false = isolated; omitted → upstream default |
price | LIMIT required | Limit price, must be a multiple of priceTick (0.1) |
maxSlippageBps | MARKET required | Market slippage, 1–1000 |
timeInForce | No | e.g. GTC / FrontendMarket |
reduceOnly | No | defaults to false |
Request example (market buy):
{
"v": "1.0.0",
"type": "request",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol": "ETH",
"side": "BUY",
"orderType": "MARKET",
"quantity": "0.01",
"leverage": 1,
"isCross": true,
"maxSlippageBps": "500",
"timeInForce": "FrontendMarket",
"reduceOnly": false
}
}
Response data:
| Field | Description |
|---|---|
orderId | HYPERLIQUID:<exchangeOrderId> |
exchangeOrderId | Upstream order id (same value used for cancel/query) |
status | Always PENDING on success |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | Always null for HL (omitted) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HYPERLIQUID:525764570803",
"exchangeOrderId": "525764570803",
"status": "PENDING",
"acceptedAt": 1787623689690
}
}
7.2 order.cancel
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | Yes | exchangeOrderId returned by create/query |
symbol | Yes | Same coin as the order |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.cancel-3",
"op": "order.cancel",
"ts": 1787625671772,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"exchangeOrderId": "525796683535",
"symbol": "ETH"
}
}
Response data:
| Field | Description |
|---|---|
exchangeOrderId | Same as request |
status | PENDING (cancel accepted) |
Cancel takes effect asynchronously: after acceptance, HL executes the cancellation asynchronously (converges to
CANCELEDwithin about 1 minute). Thestatusof the cancel response beingPENDINGonly means the instruction was accepted, not that the order is already canceled; treatorder.queryororder.updatedpushes as the source of truth for the final state.
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.cancel-3",
"op": "order.cancel",
"ts": 1787625672636,
"code": "SUCCESS",
"message": "success",
"data": {
"exchangeOrderId": "525796683535",
"status": "PENDING"
}
}
7.3 position.close
HL close supports full close only: the close request carries no quantity parameter and always closes the entire position of the given symbol.
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Yes | Coin |
maxSlippageBps | Yes | 1–1000 |
Request example (full close):
{
"v": "1.0.0",
"type": "request",
"id": "position.close-2",
"op": "position.close",
"ts": 1787625645550,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol": "ETH",
"maxSlippageBps": "50"
}
}
Response data:
| Field | Description |
|---|---|
orderId | HYPERLIQUID:<exchangeOrderId> |
exchangeOrderId | Upstream order id |
status | Always PENDING on success (accepted; the final state is determined by order.query / order.updated) |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | Always null for HL (omitted) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.close-2",
"op": "position.close",
"ts": 1787625646517,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HYPERLIQUID:525796683535",
"exchangeOrderId": "525796683535",
"status": "PENDING",
"acceptedAt": 1787625646517
}
}
7.4 order.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | No | Exchange order id |
limit | No | 1–100; default 10; out-of-range or non-integer → INVALID_REQUEST (no auto-truncation) |
startDate / endDate | No | Time range: Unix seconds/milliseconds or yyyy-MM-dd HH:mm:ss; range over 7 days or start > end → INVALID_TIME_RANGE; invalid format → INVALID_REQUEST; omitted = no filter. Effective at day granularity on HL (the server converts to yyyy-MM-dd for the upstream) |
symbol | No | Coin |
status | No | Unified status: PENDING/PARTIALLY_FILLED/FILLED/CANCELED/REJECTED |
exchange | No | Omit to merge HL/HT queries, take limit sorted by createdAt descending |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.query-2",
"op": "order.query",
"ts": 1787629031208,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"limit": 20
}
}
Response data.orders array, element fields:
| Field | Description |
|---|---|
exchange | HYPERLIQUID |
symbol | Coin |
tokenAddress | Always null for HL |
exchangeOrderId | Exchange id |
side | BUY / SELL |
orderType | MARKET / LIMIT / CANCEL, etc. |
quantity / filledQuantity / price / averagePrice / fee | Decimal strings |
status | PENDING / PARTIALLY_FILLED / FILLED / CANCELED / REJECTED |
reduceOnly | bool |
createdAt / updatedAt | epoch ms |
vaultAddress | Vault address |
Response example (data.orders** array, two shown):**
{
"v": "1.0.0",
"type": "response",
"id": "order.query-2",
"op": "order.query",
"ts": 1787629031344,
"code": "SUCCESS",
"message": "success",
"data": {
"orders": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": null,
"side": null,
"orderType": "CANCEL",
"quantity": null,
"filledQuantity": null,
"price": null,
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787625672000,
"updatedAt": 1787625672000,
"fee": null,
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
},
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": "525796683535",
"side": "SELL",
"orderType": "MARKET",
"quantity": "0.018100000000000000",
"filledQuantity": null,
"price": "2507.800000000000000000",
"averagePrice": null,
"status": "FILLED",
"reduceOnly": true,
"createdAt": 1787625646000,
"updatedAt": 1787625650000,
"fee": "0.020528000000000000",
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
]
}
}
7.5 position.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchange | No | Omit to merge both exchanges |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "position.query-1",
"op": "position.query",
"ts": 1787637839577,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
}
Response data.positions array, element fields (contract position):
| Field | Description |
|---|---|
exchange | HYPERLIQUID |
symbol | Coin |
tokenAddress | Always null for HL |
side | LONG / SHORT (net position direction; SHORT = short) |
quantity | Position size (decimal string) |
entryPrice | Entry price |
markPrice | Always null for HL |
unrealizedPnl | Unrealized PnL |
leverage | { type, value, rawUsd } (e.g. { "type": "cross", "value": "20", "rawUsd": null }) |
marginMode | CROSS / ISOLATED |
positionValue / marginUsed / returnOnEquity / maxLeverage | Decimal strings |
exchangeTime / serverTime | Upstream/server time (ms) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.query-1",
"op": "position.query",
"ts": 1787637839823,
"code": "SUCCESS",
"message": "success",
"data": {
"positions": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"side": "LONG",
"quantity": "0.01",
"entryPrice": "2496.6",
"markPrice": null,
"unrealizedPnl": "0.131",
"leverage": { "type": "cross", "value": "20", "rawUsd": null },
"marginMode": "CROSS",
"positionValue": "25.097",
"marginUsed": "1.25485",
"returnOnEquity": "0.1049427221",
"maxLeverage": "25",
"exchangeTime": null,
"serverTime": 1787637839822
}
]
}
}
8. HABITTRADE Business Endpoints
HT (HabitTrade, spot rebalancing vault) semantics differ from HL:
orderType(MARKET/LIMIT) is an order type, not a direction; direction is determined byside(BUY/SELL).- Identify the traded asset with either
symbolortokenAddress(consistent for create/close; when both are provided, both are forwarded upstream; both missing →INVALID_REQUEST). - HT does not support cancel →
order.cancelreturnsFEATURE_UNSUPPORTED. - Create/close ack carries
encodedData(ABI payload to sign; the client signs and submits on-chain); HL always omits it. - Precision:
sizeStep = 1,priceTick = 0.01.
8.1 order.create
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Either symbol or tokenAddress | Asset symbol |
tokenAddress | Either symbol or tokenAddress | Asset contract address; both missing → INVALID_REQUEST |
side | Yes | BUY / SELL, mapped to upstream action |
orderType | Yes | MARKET / LIMIT (type, not direction) |
quantity | Yes | Share quantity, must be a multiple of sizeStep (1) |
maxSlippageBps | Yes | Slippage 1–1000 |
price | LIMIT required | Limit price, must be a multiple of priceTick (0.01) |
Request example (market buy):
{
"v": "1.0.0",
"type": "request",
"id": "order.create-ht-1",
"op": "order.create",
"ts": 1787751481326,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"symbol": "SPCHs",
"side": "BUY",
"quantity": "1",
"orderType": "MARKET",
"maxSlippageBps": "3000"
}
}
Response data:
| Field | Description |
|---|---|
orderId | HABITTRADE:<exchangeOrderId> |
exchangeOrderId | Upstream order id |
status | Fixed BUILT on success |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | ABI payload to sign (0x… hex); client signs and submits on-chain |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.create-ht-1",
"op": "order.create",
"ts": 1787751486434,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HABITTRADE:62f917df0a7f483e9783d8fd4b7b8115",
"exchangeOrderId": "62f917df0a7f483e9783d8fd4b7b8115",
"status": "BUILT",
"acceptedAt": 1787751486433,
"encodedData": "0xfca9bd9e0000000000000000000000...(ABI to sign, actual length is longer)"
}
}
8.2 order.cancel — Unsupported
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.cancel-ht-1",
"op": "order.cancel",
"ts": 1787626010000,
"code": "FEATURE_UNSUPPORTED",
"message": "HabitTrade does not support order cancel",
"data": null
}
8.3 position.close
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Either symbol or tokenAddress | Asset to close |
tokenAddress | Either symbol or tokenAddress | Contract address of the asset to close; both missing → INVALID_REQUEST |
maxSlippageBps | Yes | Slippage 1–1000 |
quantity | No | "-1" or omitted = full close (only valid for SELL); positive = partial close (must be a multiple of sizeStep) |
Request example (full close, quantity omitted):
{
"v": "1.0.0",
"type": "request",
"id": "position.close-ht-1",
"op": "position.close",
"ts": 1787736302848,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"symbol": "SPCHs",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"maxSlippageBps": "50"
}
}
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.close-ht-1",
"op": "position.close",
"ts": 1787736303000,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HABITTRADE:62f917df0a7f483e9783d8fd4b7b8115",
"exchangeOrderId": "62f917df0a7f483e9783d8fd4b7b8115",
"status": "BUILT",
"acceptedAt": 1787736302999,
"encodedData": "0xfca9bd9e0000000000000000000000...(ABI to sign)"
}
}
8.4 order.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | No | Exchange order id (for HT, the transaction hash) |
limit | No | 1–100; default 10; out-of-range or non-integer → INVALID_REQUEST (no auto-truncation) |
startDate / endDate | No | Time range: Unix seconds/milliseconds or yyyy-MM-dd HH:mm:ss; range over 7 days or start > end → INVALID_TIME_RANGE; invalid format → INVALID_REQUEST; omitted = no filter. Precise to the second on HT |
symbol | No | Asset contract address |
tokenAddress | No | Asset contract address |
status | No | Unified status |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.query-ht-1",
"op": "order.query",
"ts": 1787664502488,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"limit": 20
}
}
Response data.orders array, element fields:
| Field | Description |
|---|---|
exchange | HABITTRADE |
symbol | Asset contract address |
tokenAddress | Asset contract address |
exchangeOrderId | Transaction hash |
side | BUY / SELL |
orderType | Passed through from the upstream (uppercase, e.g. MARKET / LIMIT); null if the upstream does not return it |
quantity / filledQuantity | 0 |
price / averagePrice | Fill price (same value); decimal strings (no scientific notation) |
status | FILLED / PENDING / ERROR / PARTIALLY_FILLED |
reduceOnly | true for SELL, false for BUY |
createdAt / updatedAt | epoch ms |
fee | Always null |
vaultAddress | Vault address |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.query-ht-1",
"op": "order.query",
"ts": 1787664502653,
"code": "SUCCESS",
"message": "success",
"data": {
"orders": [
{
"exchange": "HABITTRADE",
"symbol": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"exchangeOrderId": "0x66560a584e68f8f9385d35acb950bfc0285fc3e739b708b8d71d6da628e54e33",
"side": "BUY",
"orderType": null,
"quantity": "0",
"filledQuantity": "0",
"price": null,
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787385339000,
"updatedAt": 1787385399000,
"fee": null,
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df"
}
]
}
}
8.5 position.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "position.query-ht-1",
"op": "position.query",
"ts": 1787662054736,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df"
}
}
Response data.positions array, element fields (vault portfolio):
| Field | Description |
|---|---|
exchange | HABITTRADE |
symbol | Asset name |
tokenAddress | Asset contract address |
side | Always LONG (held by the vault) |
quantity | Current quantity |
markPrice | Current quote |
entryPrice | Entry price (from upstream) |
unrealizedPnl / returnOnEquity | null (spot has none) |
leverage | { "type": "cross", "value": "1", "rawUsd": "0" } |
marginMode | CROSS |
positionValue | Current USD value |
marginUsed | "0" (spot has no margin) |
maxLeverage | "1" |
exchangeTime / serverTime | ms |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.query-ht-1",
"op": "position.query",
"ts": 1787667041781,
"code": "SUCCESS",
"message": "success",
"data": {
"positions": [
{
"exchange": "HABITTRADE",
"symbol": "USDC",
"tokenAddress": "0xc879c018db60520f4355c26ed1a6d572cdac1815",
"side": "LONG",
"quantity": "16.435225",
"entryPrice": null,
"markPrice": "1",
"unrealizedPnl": null,
"leverage": { "type": "cross", "value": "1", "rawUsd": "0" },
"marginMode": "CROSS",
"positionValue": "16.435225",
"marginUsed": "0",
"returnOnEquity": null,
"maxLeverage": "1",
"exchangeTime": null,
"serverTime": 1787667041780
}
]
}
}
9. Trading Subscription Endpoints
type=subscribe, op is the topic (order.updated / position.updated), data.vaultAddresses is an array; exchange is HYPERLIQUID or HABITTRADE. Market data subscriptions (market.orderbook) take a different request shape; see Section 10.
9.1 Subscribe Request & ack
Request example (position subscription):
{
"v": "1.0.0",
"type": "subscribe",
"id": "position.updated-6",
"op": "position.updated",
"ts": 1787638868184,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddresses": ["0x7097732c73b74e94d6e8cc9e77c287757a17afdc"]
}
}
ack (note the ack op is "subscribe", not the topic):
{
"v": "1.0.0",
"type": "subscribe",
"id": "position.updated-6",
"op": "subscribe",
"ts": 1787638868357,
"code": "SUCCESS",
"message": "subscribed",
"data": {
"subscription": {
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"exchange": "HYPERLIQUID",
"topic": "position.updated"
}
}
}
data.vaultAddressesis required; each address must be operable by the AK. Any without permission →VAULT_NOT_PERMITTED;data.deniedVaultAddresseslists the denied addresses (does not affect existing subscriptions).- Re-subscribing the same
exchange+topicoverwrites the old subscription; only brand-new subscriptions check the limit (≤ 4). - For HT subscriptions, change
exchangetoHABITTRADE; handshake/heartbeat are the same as HL.
9.2 Event Frames
Position event (one push immediately after subscribing with reason=INITIAL, then every 60s with reason=PERIODIC, one frame per vault, including empty snapshots):
{
"v": "1.0.0",
"type": "event",
"topic": "position.updated",
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"seq": 41,
"exchange": "HYPERLIQUID",
"ts": 1787638868358,
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"snapshot": true,
"reason": "INITIAL",
"stale": false,
"asOf": 1787638868357,
"positions": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"side": "LONG",
"quantity": "0.01",
"entryPrice": "2496.6",
"markPrice": null,
"unrealizedPnl": "0.131",
"leverage": { "type": "cross", "value": "20", "rawUsd": null },
"marginMode": "CROSS",
"positionValue": "25.097",
"marginUsed": "1.25485",
"returnOnEquity": "0.1049427221",
"maxLeverage": "25",
"exchangeTime": null,
"serverTime": 1787638868357
}
]
}
}
Order event (one push immediately after subscribing, then every 60s, only in-flight PENDING/PARTIALLY_FILLED, one frame per vault):
{
"v": "1.0.0",
"type": "event",
"topic": "order.updated",
"subscriptionId": "sub_ac6ab7be-3581-4bb1-83bd-125a9a29ca07",
"seq": 45,
"exchange": "HYPERLIQUID",
"ts": 1787639052680,
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"orders": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": "525796683535",
"side": "BUY",
"orderType": "MARKET",
"quantity": "0.01",
"filledQuantity": null,
"price": "2500.1",
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787623689692,
"updatedAt": 1787623689692,
"fee": null,
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
]
}
}
Position/order event element fields are the same as the corresponding exchange query endpoint: HL as §7.5/§7.4; HT as §8.5/§8.4.
9.3 Unsubscribe
Request example:
{
"v": "1.0.0",
"type": "unsubscribe",
"id": "unsubscribe-7",
"op": "unsubscribe",
"ts": 1787638961103,
"data": {
"subscriptionIds": ["sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd"]
}
}
ack:
{
"v": "1.0.0",
"type": "unsubscribe",
"id": "unsubscribe-7",
"op": "unsubscribe",
"ts": 1787638961187,
"code": "SUCCESS",
"message": "unsubscribed",
"data": null
}
10. Market Data Subscription Endpoints (market.orderbook)
Market data subscriptions run on the same WebSocket endpoint as trading; handshake auth, heartbeat, connection dedup, and the message envelope (§5) are identical — no extra credentials needed. The current market data topic is market.orderbook** (order book)**: data is the upstream order book JSON frame, passed through verbatim with zero processing. Use it for order book / depth scenarios.
- Market data
exchangeisHYPERLIQUID(perp/spot) orSTOVE(US equities etc.); oneexchangeper message — cross-source requires separate messages. - The two data sources have different upstream order book structures (see §10.2); the subscription request shape is identical, only
exchangediffers. dataallows the singletickersfield only — any extra field rejects the whole batch (INVALID_REQUEST).
10.1 Subscribe Request & ack
Request example:
{
"v": "1.0.0",
"type": "subscribe",
"id": "m-1",
"op": "market.orderbook",
"ts": 1792549000000,
"exchange": "HYPERLIQUID",
"data": {
"tickers": ["BTC@perp", "KNETIQ:kHYPE@perp", "RMD/USDC@spot"]
}
}
Ticker notation:
| Source | Form | Example | Rules |
|---|---|---|---|
| HL perp | coin@perp | BTC@perp | coin without : → the main market; HIP-3 form dex:coin@perp (e.g. KNETIQ:kHYPE@perp) routes by the prefix before : |
| HL spot | symbol@spot | RMD/USDC@spot | symbol is the spot pair name (e.g. RMD/USDC); the legacy @N index form is deprecated |
| STOVE | symbol@market | AAPLs@usex | market ∈ usex / hkex / krex (case-insensitive) |
- Neither side of
@may be empty; suffixes are case-insensitive (@PERP/@Spotboth accepted). - An invalid ticker rejects the whole batch (
INVALID_REQUEST, message names the offending item). A valid ticker with no data yet (not launched / not covered) is accepted without error and waits silently. - Note the ack's
opis the topic itself (market.orderbook) and carries asnapshotsarray:
{
"v": "1.0.0",
"type": "subscribe",
"id": "m-1",
"op": "market.orderbook",
"ts": 1792549000012,
"code": "SUCCESS",
"message": "subscribed",
"data": {
"subscription": {
"subscriptionId": "sub_…",
"exchange": "HYPERLIQUID",
"topic": "market.orderbook",
"tickers": ["BTC@perp", "KNETIQ:kHYPE@perp", "RMD/USDC@spot"]
},
"snapshots": [
{"ticker": "BTC@perp", "state": "pushed"},
{"ticker": "KNETIQ:kHYPE@perp", "state": "pending"},
{"ticker": "RMD/USDC@spot", "state": "pending"}
]
}
}
snapshots[].state:
| state | Meaning |
|---|---|
pushed | Data already available at acceptance; the first frame was delivered with the ack |
pending | Accepted but no data yet (new symbol / not covered upstream); frames arrive later, no error and no alert |
already | Idempotent re-subscribe (same exchange+ticker+topic already live on this connection); echoes the existing subscription, no new quota |
10.2 Market Data Event Frames
Same event envelope as §5.3 (seq increases within the connection, resets on reconnect; envelope ts is the server write time). data is the upstream order book JSON frame, verbatim with zero processing; the two data sources have different structures.
HYPERLIQUID (upstream l2Book, full snapshot):
{
"v": "1.0.0",
"type": "event",
"topic": "market.orderbook",
"subscriptionId": "sub_…",
"seq": 9,
"exchange": "HYPERLIQUID",
"ts": 1793334791900,
"data": {
"channel": "l2Book",
"data": {
"coin": "BSV",
"time": 1793334790833,
"levels": [
[
{"px": "17.017", "sz": "0.75", "n": 1},
{"px": "17.008", "sz": "22.63", "n": 2},
{"px": "16.994", "sz": "81.68", "n": 2},
{"px": "16.983", "sz": "5.0", "n": 1}
],
[
{"px": "17.033", "sz": "0.75", "n": 1},
{"px": "17.040", "sz": "594.3", "n": 1},
{"px": "17.048", "sz": "451.11", "n": 2},
{"px": "17.051", "sz": "367.6", "n": 1}
]
]
}
}
}
| Field | Description |
|---|---|
data.channel | Always l2Book (upstream channel name) |
data.data.coin | Coin: for perp, the subscribed coin (HIP-3 with DEX prefix, e.g. xyz:RELIANCE); for spot, the spot pair name (e.g. META/USDC) |
data.data.time | Upstream frame time (epoch ms) |
data.data.levels[0] | Bids, sorted by price descending |
data.data.levels[1] | Asks, sorted by price ascending |
Level element px / sz | Price / size, decimal strings |
Level element n | Number of aggregated orders at the price level (integer) |
- The HL order book is a full snapshot (not a delta): each push is the entire book. The example shows an excerpt; real frames usually carry up to 20 levels per side. Spot books may be sparse with fewer levels. Frames can be large — mind client processing performance.
- The nested
data.datastructure is exactly the upstream l2Book frame; the server does not parse or rewrite it.
STOVE:
{
"v": "1.0.0",
"type": "event",
"topic": "market.orderbook",
"subscriptionId": "sub_…",
"seq": 7,
"exchange": "STOVE",
"ts": 1793330910200,
"data": {
"type": "orderbook",
"data": {
"symbol": "DELTON",
"market": "hkex",
"bid": [{"price": 61.25, "volume": 200}],
"ask": [{"price": 61.4, "volume": 1600}],
"time": "2026-10-30T03:28:30Z"
}
}
}
| Field | Description |
|---|---|
data.type | Always orderbook (upstream message type; distinct from the subscription-side op name) |
data.data.symbol / market | Symbol / market code (usex / hkex / krex) |
data.data.bid | Bids |
data.data.ask | Asks |
Element price / volume | Price / size as JSON numbers (note HL uses strings — the two sources parse differently) |
data.data.time | ISO-8601 UTC timestamp string (second granularity) |
- The STOVE book is an object array; the structure is exactly the message from the upstream
orderbookschannel, passed through with zero processing.
10.3 Push Semantics & Frame Size
- Push on change only: the server compares cached data versions on a ~100ms cadence and pushes only when the data changed; the same version is never pushed twice. Frame cadence follows upstream updates; no fixed frequency is guaranteed.
- Downlink frame limit defaults to 64 KiB; oversized frames are dropped silently (no disconnect, no alert to the client). If an order book stream stops for a symbol, check single-frame overflow first (a larger per-client limit can be requested from the platform).
- Slow-consumer protection: when the server write times out (default 10s) or the send buffer overflows (default 512 KiB), slow clients are disconnected proactively.
- Unsubscribe on disconnect: all market subscriptions are revoked when the connection drops; resubscribe after reconnect (idempotent re-subscription is safe — you may simply replay the full subscription list).
10.4 Unsubscribe
Same shape as subscribe with type=unsubscribe and the topic as op; revoked per ticker; repeated unsubscribes are idempotent:
{
"v": "1.0.0",
"type": "unsubscribe",
"id": "m-2",
"op": "market.orderbook",
"ts": 1792549001000,
"exchange": "HYPERLIQUID",
"data": {"tickers": ["BTC@perp"]}
}
After unsubscription the ticker no longer produces events. Market data does not use the subscriptionIds form from §9.3.
10.5 Limits & Idempotency
| Item | Value |
|---|---|
| Max tickers per subscribe | 50 (over-limit rejects the whole batch, no partial acceptance) |
| Market subscriptions per connection | 200 (configurable per client) |
| Duplicate tickers in an array | Deduplicated automatically; quota counts post-dedup |
| Idempotency key | (exchange, ticker, topic); repeated subscribes return already and consume no new quota |
| Relation to trading subscriptions | The market subscription pool is independent of the trading pool (4) in §6 — they do not share quota |
Error codes for market subscriptions reuse Section 12: INVALID_REQUEST (invalid ticker / disallowed data fields / missing ts), INVALID_EXCHANGE (exchange not HYPERLIQUID/STOVE), TOO_MANY_SUBSCRIPTIONS (quota exceeded), REQUEST_TS_STALE (ts not strictly increasing), FEATURE_UNSUPPORTED (market feature disabled, during gray release).
11. marginCall.alert — Server-Initiated Margin Warning Push
No subscription required. After the connection is established, the server polls in the background (every ~10 seconds) all HYPERLIQUID vaults operable by the current AK and pushes through the priority channel when triggered; it does not count against the §6 subscription quota.
11.1 Tiers & Trigger Conditions
Tiering is based on the margin level: marginLevel = margin / maintenance margin.
| marginLevel range | Tier | Pushed? |
|---|---|---|
| > 1.25 | SAFE | No |
| (1.15, 1.25] | TIER_1 | Yes |
| (1.05, 1.15] | TIER_2 | Yes |
| ≤ 1.05 | TIER_3 | Yes |
- Cross (
CROSS) margin mode is tiered per vault as a whole; isolated (ISOLATED) is tiered independently per vault × symbol × side. - When any position reaches its liquidation price, the vault is considered in liquidation: pushes stop and do not auto-resume after liquidation; state resets on reconnect.
- Push cadence: a tier change (including the first transition from SAFE into a warning tier) → pushed immediately (
triggerType=TIER_CHANGED); an unchanged tier → pushed every 30 minutes (triggerType=PERIODIC); returning toSAFEis not pushed. - The maintenance margin is derived by the platform from positions; it may differ from upstream values or your own computation.
11.2 Event Frame
topic is fixed marginCall.alert, subscriptionId is fixed margincall, exchange is always HYPERLIQUID.
Cross example:
{
"v": "1.0.0",
"type": "event",
"topic": "marginCall.alert",
"subscriptionId": "margincall",
"seq": 128,
"exchange": "HYPERLIQUID",
"ts": 1787638868358,
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"marginMode": "CROSS",
"marginLevel": "1.20",
"tier": "TIER_1",
"triggerType": "TIER_CHANGED",
"margin": "0.602",
"maintenanceMargin": "0.50194",
"triggeredAt": 1787638868358,
"positions": [
{
"symbol": "ETH",
"side": "LONG",
"quantity": "0.01",
"entryPrice": "2496.6",
"unrealizedPnl": "0.131",
"marginUsed": "0.602",
"maintenanceMargin": "0.50194"
}
]
}
}
Isolated frames additionally carry symbol / side (the position that triggered the push) and positions contains only that position; cross frames include all cross positions of the vault.
11.3 data Fields
| Field | Description |
|---|---|
vaultAddress | Vault address |
marginMode | CROSS / ISOLATED |
symbol / side | Isolated only: the triggering position's symbol/side; omitted for cross |
marginLevel | Margin level (decimal string, e.g. "1.20"; omitted when there is no position) |
tier | Current tier TIER_1 / TIER_2 / TIER_3 |
triggerType | TIER_CHANGED (tier change) / PERIODIC (unchanged-tier periodic) |
margin | Margin: cross = account margin; isolated = the position's isolated margin |
maintenanceMargin | Maintenance margin (platform-computed): cross = sum over positions; isolated = the single position |
triggeredAt | Trigger timestamp (epoch ms) |
positions[] | Related positions (cross = all cross positions of the vault, isolated = the triggering position only). Element fields: symbol / side / quantity / entryPrice / unrealizedPnl / marginUsed / maintenanceMargin, same conventions as the position fields in §7.5 |
All tier state is cleared when the connection drops and is not inherited on reconnect; after reconnecting, a vault still in a warning tier is pushed again as a tier change.
12. Error Codes
| code | message |
|---|---|
SUCCESS | Success |
INVALID_REQUEST | Invalid request: JSON/field/business param missing or invalid |
UPLINK_TOO_LARGE | Uplink frame exceeds the limit; shrink and resend |
REQUEST_TS_STALE | Request ts is stale or repeated |
VAULT_NOT_PERMITTED | vaultAddress is not operable |
INVALID_EXCHANGE | Unsupported exchange |
EXCHANGE_NOT_CONFIGURED | Exchange account or required credentials not configured |
SYMBOL_NOT_SUPPORTED | Trading pair/topic not enabled |
TOO_MANY_INFLIGHT_REQUESTS | In-flight request limit reached for this connection |
DUPLICATE_REQUEST_ID | Request ID reused on this connection |
TOO_MANY_SUBSCRIPTIONS | Subscription limit reached for this connection |
RATE_LIMIT | Too many requests; retry later |
DUPLICATE_REQUEST_CONFLICT | Same clientOrderId with different request content |
ORDER_NOT_FOUND | Order not found |
INVALID_TIME_RANGE | Historical order time range invalid or span exceeds 7 days |
INSUFFICIENT_MARGIN | Insufficient margin or balance |
INVALID_PRICE_TICK | Invalid price precision |
INVALID_SIZE_STEP | Invalid quantity precision |
REDUCE_ONLY_REJECTED | Reduce-only request would increase the position, or the position has changed |
EXCHANGE_REJECTED | Exchange business rejection |
UPSTREAM_TIMEOUT | Retryable query request timeout |
UPSTREAM_RESULT_UNKNOWN | Write result unknown; you must query first |
UPSTREAM_UNAVAILABLE | Exchange temporarily unavailable |
FEATURE_UNSUPPORTED | The exchange does not support the requested capability |
INTERNAL_ERROR | Unexpected error |
13. Order Status
| Unified status | Description |
|---|---|
PENDING | Order accepted, not yet filled |
PARTIALLY_FILLED | Partially filled (upstream may not have this status; OpenAPI maps it) |
FILLED | Fully filled |
CANCELED | Canceled |
EXPIRED | Expired or terminated due to TTL (status retained, currently unused) |
REJECTED | Rejected by the exchange |
ERROR | Generic error, used when the precise error cannot be distinguished |
The
order.updatedevent pushes only in-flight statuses:PENDINGandPARTIALLY_FILLED(in-flight snapshot, re-sent every cycle, see 9.2). Terminal states (FILLED/CANCELED/REJECTED/ERROR) are not actively pushed — an order disappearing from the in-flight list is the terminal signal; the final state value should be obtained viaorder.query.
14. Java Connection Example
Demo: connect + handshake signing + limit order + proactive Pong keepalive. AK masked, private key not printed. Pure JDK (
java.net.http.WebSocketrequires JDK 11+, Ed25519 requires JDK 15+,HexFormatrequires JDK 17+).
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HexFormat;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class HlWsExample {
private static final String CLIENT_ID = "ak_xxxxxxxxxxxxxxxxxxxxxxxxx";
private static final String PRIV_B64 = "<ed25519 PKCS#8 private key, base64(PEM body, no header/footer)>"; // SK, never log
private static final String BASE = "wss://<host>";
private static final String PATH = "/api/openapi/v1/ws";
private WebSocket ws;
private final Object sendLock = new Object();
private CompletableFuture<WebSocket> lastSend = null;
private final ScheduledExecutorService keepalive = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args) throws Exception {
PrivateKey sk = loadEd25519(PRIV_B64);
String ts = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String signature = HexFormat.of().formatHex(
sign(sk, (CLIENT_ID + "\n" + ts + "\n" + nonce).getBytes(StandardCharsets.UTF_8)));
String url = BASE + PATH + "?clientId=" + enc(CLIENT_ID) + "×tamp=" + ts
+ "&nonce=" + enc(nonce) + "&signature=" + signature;
HlWsExample c = new HlWsExample();
WebSocket ws = HttpClient.newHttpClient().newWebSocketBuilder().buildAsync(URI.create(url), c.listener()).join();
c.ws = ws;
Thread.sleep(45_000);
c.keepalive.shutdownNow();
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").get();
}
WebSocket.Listener listener() {
return new WebSocket.Listener() {
@Override public void onOpen(WebSocket webSocket) {
System.out.println("[open]"); HlWsExample.this.ws = webSocket; webSocket.request(1);
// limit order example
sendText("""
{"v":"1.0.0","type":"request","id":"order.create-4","op":"order.create",
"ts":1787623689692,"exchange":"HYPERLIQUID","data":{
"vaultAddress":"0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol":"ETH","side":"BUY","orderType":"LIMIT","quantity":"0.01",
"price":"2500.1","leverage":"20","timeInForce":"GTC","reduceOnly":false}}""");
keepalive.scheduleAtFixedRate(HlWsExample.this::sendPong, 2, 20, TimeUnit.SECONDS);
}
@Override public CompletionStage<?> onText(WebSocket w, CharSequence data, boolean last) {
System.out.println("[rx] " + data); w.request(1); return CompletableFuture.completedStage(null);
}
@Override public CompletionStage<?> onClose(WebSocket w, int code, String reason) {
System.out.println("[close] code=" + code + ", reason=" + reason); return null;
}
@Override public void onError(WebSocket w, Throwable e) { System.err.println("[error] " + e); }
};
}
private void sendText(String text) {
synchronized (sendLock) {
CompletableFuture<WebSocket> base = (lastSend == null) ? CompletableFuture.completedFuture(ws) : lastSend;
lastSend = base.thenCompose(w -> w.sendText(text, true));
}
}
private void sendPong() {
try { synchronized (sendLock) {
CompletableFuture<WebSocket> base = (lastSend == null) ? CompletableFuture.completedFuture(ws) : lastSend;
lastSend = base.thenCompose(w -> w.sendPong(ByteBuffer.wrap("ka".getBytes(StandardCharsets.UTF_8))));
} } catch (Exception e) { System.err.println("pong err: " + e); }
}
private static PrivateKey loadEd25519(String base64) throws Exception {
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64)));
}
private static byte[] sign(PrivateKey sk, byte[] data) throws Exception {
Signature s = Signature.getInstance("Ed25519"); s.initSign(sk); s.update(data); return s.sign();
}
private static String enc(String v) { return URLEncoder.encode(v, StandardCharsets.UTF_8); }
}
Note:
timestampuses Unix seconds;nonceis recommended to be a UUID;signatureis the 128-char lowercase hex of the Ed25519 signature. The server sends Pings periodically; the client is advised to also send Pongs proactively to avoid being closed after 60s with no Pong (4002). Only one active connection is kept per AK; a new connection causes the old one to be closed (4001).