DevelopersAXIS API
WebSocket API
Stream contract state, account changes, trades, tickers, depth and candles from the AXIS API: subscribe, receive a snapshot, then live updates.
Endpoint#
| Network | URL |
|---|---|
| Testnet | wss://demo-api.axis.markets/ws |
| Mainnet | Not available yet |
One connection carries many subscriptions. Messages are JSON text frames in both directions. Payloads use the shapes
and conventions of the REST API: big integers as strings, token contract addresses for assets and
canonical base/quote market keys for trades, contract and ticker. depth and candles keep the requested orientation.
Protocol#
Client messages#
| Message | Reply |
|---|---|
{op: 'subscribe', channel, id?, ...params} |
The channel snapshot, echoing id |
{op: 'unsubscribe', channel, id?, ...params} |
{type: 'unsubscribed', id, channel, topic} |
{op: 'ping', id?} |
{type: 'pong', id} |
idis optional and can be any JSON value. The server echoes it in the reply and in errors, so you can match them to requests.- Channel parameters sit at the top level of the message, next to
opandchannel. unsubscribetakes the same parameters as thesubscribeit cancels, because the server derives the topic from them. It answersunsubscribedeven when nothing was subscribed, provided the parameters are valid.- Subscribing again to a topic you already hold sends a fresh snapshot without adding a subscription.
pingis an application-level heartbeat for clients that cannot send WebSocket ping frames, such as browsers.
{"op": "subscribe", "id": 5, "channel": "depth", "market": "XLM/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", "limit": 3}
{"type": "pong", "id": "p1"}
{
"type": "unsubscribed",
"id": 9,
"channel": "trades",
"topic": "trades:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
}
Server messages#
Every server message has a type. Messages of a subscription also carry its topic.
- The snapshot answers
subscribe. It carries the requestidand thetopic, and holds the full current state of the channel. - Updates follow as the state changes. They carry the
topicbut noid.
Route messages by topic, not by your request parameters. The server normalizes the parameters: asset codes become
contract addresses, trades markets take the canonical order and defaults are filled in.
Topics#
| Channel | Topic |
|---|---|
contract |
contract |
ticker |
ticker |
account |
account:<address> |
trades |
trades:<base>/<quote>, or trades:* for every market |
depth |
depth:<base>/<quote>/<depth>/<step>/<limit>, with auto as <step> when no step was given |
candles |
candles:<base>/<quote>/<resolution in seconds> |
For example, a depth subscription to XLM/<USDC address> with limit: 3 gets the topic
depth:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/20/auto/3.
Errors#
Errors are {type: 'error', id?, error}. A failed subscribe creates no subscription, and the connection stays open.
| Cause | error |
|---|---|
| Malformed JSON | Invalid JSON, without id |
| Binary frame | Binary messages are not supported, without id |
| A JSON value that is not an object or an array | Invalid message |
Unknown or missing op, an array included |
Unknown op: <op> |
| Unknown channel | Unknown channel: <channel> |
| Invalid account address | Invalid account address |
| Invalid channel parameter | The REST error text, for example Bad request. Invalid parameter: "market". Expected "BASE/QUOTE" format |
| Too many topics | Too many subscriptions (max 20) |
| Too many accounts | Too many account subscriptions (max 10) |
| Unexpected server error | Internal error |
{"type": "error", "id": 7, "error": "Unknown channel: nope"}
Channels#
contract#
Contract state and ledger progress. No parameters.
The snapshot is {type: 'contract', id, topic, ledger, data}:
| Field | Type | Description |
|---|---|---|
ledger |
number | Last ledger processed. Absent until the service has processed one after a restart. |
data |
object | Same body as GET /contract |
Updates:
| Message | When |
|---|---|
{type: 'contract', kind, market?, data, topic} |
The state changed. kind is freeze, config or market. market is the canonical base/quote key, for kind: 'market' only. data is the full /contract body after the change. |
{type: 'ledger', ledger, topic} |
A ledger was processed, with or without AXIS events. A burst of ledgers, for example while older ledgers are replayed after a restart, is coalesced into the latest one. |
kind: 'market' arrives for every refresh event: a market opened with subsidize, or checked against the oracle
again with requote or subsidize.
Snapshot, with one of the six markets shown:
{
"type": "contract",
"id": 1,
"topic": "contract",
"ledger": 5002168,
"data": {
"address": "CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS",
"frozen": false,
"config": {
"safetyAdmin": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
"oracle": "CDEIQLTE3Y3XQMYPSZFJC2OXIS67F4MURGP73NCE2C2NZZ22RNSPV7ZI",
"listingMinDays": 90,
"marketListingFee": "180000000000",
"minTradeSize": "10000",
"ledgerTime": 5
},
"markets": [
{
"base": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
"quote": "CCUUDM434BMZMYWYDITHFXHDMIVTGGD6T2I5UKNX5BSLXLW7HVR4MCGZ",
"created": "2026-10-05 21:12:47",
"refreshed": "2026-10-05 21:12:47"
}
]
}
}
Ledger update:
{"type": "ledger", "ledger": 5002169, "topic": "contract"}
account#
One account's live orders and backing, plus the fills and swaps it takes part in.
| Parameter | Type | Default | Description |
|---|---|---|---|
address |
string | required | Account address, G... or C... |
The snapshot is {type: 'account', id, topic, address, ledger, orders, backing}, the body of
GET /account/:address. A subscribed account is watched: backing covers every token of
the open markets, whether or not the account has orders, and changes are pushed as they happen. The snapshot waits up
to 5 seconds for these records to load.
Updates:
| Message | When |
|---|---|
{type: 'order', address, action, order, fill?, topic} |
An order of the account changed. order is the full order after the change. |
{type: 'backing', address, asset, backing, backed, topic} |
The account's balance, allowance or authorization in asset changed, or an order selling asset changed. backing is the Backing record, with headroom for a trustline asset, or null once it is no longer tracked. backed maps every live order ID selling asset to its backed amount. |
{type: 'trade', address, market, trade, topic} |
The account was the taker of a fill. trade is a Trade record and market its canonical base/quote key. |
{type: 'swap', address, swap, topic} |
The account made a swap. swap is a Swap record. |
action |
Meaning |
|---|---|
new |
Order created |
fill |
Partially filled. The order stays live. |
filled |
Filled completely and removed |
update |
Amount, price or expiration changed, or an expired order revived |
cancel |
Removed by its owner |
expire |
Passed its expiration |
fill comes with the fill and filled actions and describes the fill from the maker's side:
| Field | Type | Description |
|---|---|---|
sold |
string | Amount of the order's selling token the maker delivered |
bought |
string | Amount of the order's buying token the maker received |
taker |
string | Taker address |
trade |
string | Trade ID |
ts |
number | Fill time, UNIX seconds |
crossfill |
boolean | true on the fill of a crossfill taker order, where taker is the caller who keeps the surplus. Absent otherwise. |
Snapshot, with one of the account's orders and one of its backing records shown:
{
"type": "account",
"id": 2,
"topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"ledger": 5002168,
"orders": [
{
"id": "295788099352601974601093423915094661279",
"status": "ACTIVE",
"buying": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"selling": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"price": "3504154500000000000",
"rprice": 3.5041545,
"quote": "5497500",
"amount": "5497500",
"owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"backed": "5497500",
"backing": {
"balance": "69240923294",
"allowance": "355869776",
"liveUntil": 5520550,
"authorized": true,
"updated": "2026-10-03 13:33:27"
},
"created": "2026-10-03 09:46:52",
"updated": "2026-10-03 09:46:52",
"cursor": "1407218664943353069569"
}
],
"backing": {
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC": {
"owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"asset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"balance": "69240923294",
"allowance": "355869776",
"liveUntil": 5520550,
"authorized": true,
"budget": "355869776",
"updated": "2026-10-03 13:33:27"
}
}
}
A partial fill of one of the account's orders:
{
"type": "order",
"address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"action": "fill",
"order": {
"id": "9238900405409148379285264828899405993",
"status": "ACTIVE",
"buying": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"selling": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"price": "3400183201870916805",
"rprice": 3.4001832018709166,
"quote": "6442790",
"amount": "2098101",
"owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"backed": "2098101",
"backing": {
"balance": "69238809011",
"allowance": "353355132",
"liveUntil": 5520567,
"authorized": true,
"updated": "2026-10-03 13:33:54"
},
"created": "2026-10-03 13:33:17",
"updated": "2026-10-03 13:34:17",
"cursor": "1407983432454002180096"
},
"fill": {
"sold": "4344689",
"bought": "14772739",
"taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"trade": "1407986810154796449795",
"ts": 1791034457
},
"topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}
A backing change, with two of the order IDs in backed shown:
{
"type": "backing",
"address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"backing": {
"owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"balance": "489443642225",
"allowance": "558137166",
"liveUntil": 5520557,
"authorized": true,
"budget": "558137166",
"updated": "2026-10-03 13:33:54"
},
"backed": {
"183931260950848202116713872522883870362": "24846547",
"153390214124585565809379122683319801444": "10341628"
},
"topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}
The taker side of a fill:
{
"type": "trade",
"address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"trade": {
"type": "trade",
"id": "1407985402778570719235",
"order": "309519727566973417685211056811511198536",
"taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"maker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"soldAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"boughtAsset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"sold": "1389711",
"bought": "4661256",
"price": 3.3541189499111685,
"cursor": "1407985402778570719235",
"timestamp": "2026-10-03 13:33:52",
"left": "0"
},
"topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}
Notes:
- Updates of the account can arrive before its snapshot while the snapshot waits for the backing to load. The snapshot is built after them, so replace your state with it when it arrives.
- The maker side of a fill arrives as an
ordermessage withfill, the taker side as atrademessage. A self-trade produces both, as in the examples above. - The taker order fill of a
crossfill(crossfill: true) is not pushed to the caller as atrademessage. The owner of the taker order receives it as anordermessage whosefillcarriescrossfill: true. - Several
backingchanges of the same token within one processing step are coalesced into one message. expirecomes from the service's clock. The contract emits no event when an order expires.- The service watches a limited number of accounts at a time. Past that limit, a new subscription gets backing records only for the tokens of the account's orders.
trades#
Fills of one market or of all markets.
| Parameter | Type | Default | Description |
|---|---|---|---|
market |
string | every market | BASE/QUOTE in either orientation and any asset format, or * for every market |
The snapshot is {type: 'trades', id, topic, market, trades}. market is the canonical base/quote key or *, and trades
holds the latest 50 records, newest first, as returned by GET /trades.
Updates are {type: 'trade', market, trade, topic}, one per fill as it is indexed, with market set to the fill's
canonical key.
Snapshot of XLM/USDC, requested as XLM/<USDC address>, with one of 50 records shown:
{
"type": "trades",
"id": 3,
"topic": "trades:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"market": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"trades": [
{
"type": "trade",
"id": "1407976395581195026434",
"order": "133725813046786288052094016278953483903",
"taker": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
"maker": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
"soldAsset": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
"boughtAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"sold": "835531",
"bought": "3741722",
"price": 4.478256342373892,
"cursor": "1407976395581195026434",
"timestamp": "2026-10-03 13:31:12",
"left": "0"
}
]
}
Update on a trades:* subscription:
{
"type": "trade",
"market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"trade": {
"type": "trade",
"id": "1407991032280789286915",
"order": "9238900405409148379285264828899405993",
"taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"maker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
"soldAsset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
"boughtAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"sold": "7133928",
"bought": "2098101",
"price": 0.29410179076660153,
"cursor": "1407991032280789286915",
"timestamp": "2026-10-03 13:35:32",
"left": "0"
},
"topic": "trades:*"
}
Notes:
- The snapshot can include
swaprecords, while updates carry fills only. Swaps are pushed on the trader'saccountchannel. - When you merge the snapshot with pages from
GET /trades, deduplicate byid. - Updates include the fill of a
crossfilltaker order, flaggedcrossfill: true. Leave it out of volume sums, as forGET /trades.
ticker#
24-hour statistics of every market. No parameters.
The snapshot is {type: 'ticker', id, topic, data}, where data is the body of
GET /ticker/24h. Updates have the same shape without id and always carry every market.
An update follows fills and refresh events of the contract, at most once every 5 seconds.
Snapshot, with one of the six entries shown:
{
"type": "ticker",
"id": 4,
"topic": "ticker",
"data": {
"ledger": 5002168,
"timestamp": 1791034432,
"ticker": [
{
"symbol": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"openPrice": "0.2968348",
"highPrice": "0.3140691",
"lowPrice": "0.2879428",
"lastPrice": "0.2981408",
"volume": "2702.6187351",
"quoteVolume": "810.8429328",
"change": 0.44,
"avgPrice": "0.3000212",
"trades": 2854
}
]
}
}
depth#
Aggregated orderbook of one market, in the orientation you request.
| Parameter | Type | Default | Description |
|---|---|---|---|
market |
string | required | BASE/QUOTE, each side in any asset format |
depth |
number | 20 |
Band around the mid price in percent, greater than 0 and at most 100 |
step |
decimal | automatic | Price step of the levels |
limit |
integer | 100 |
Levels per side, 1 to 500 |
The parameters work as in GET /depth. The snapshot is {type: 'depth', id, topic, market, depth, data}, where market is <base>/<quote> as contract addresses in the requested orientation, depth the band and
data the /depth body.
Updates have the same shape without id. Each one carries the whole book, not a delta. An update is sent at most once
per second per topic, after an order or a backing change that can affect the market, and is skipped when its levels
are identical to the previous update. The first update after the snapshot can repeat the snapshot's levels.
{
"type": "depth",
"id": 5,
"topic": "depth:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/20/auto/3",
"market": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
"depth": 20,
"data": {
"ledger": 5002168,
"timestamp": 1791034432,
"step": "0.00001",
"bestBid": "0.2233046",
"bestAsk": "0.2242675",
"bids": [
["0.2233", "0.0297168"],
["0.22259", "0.5495841"],
["0.21885", "0.11365"]
],
"asks": [
["0.22427", "0.8481886"],
["0.22513", "0.5766172"],
["0.22594", "0.340319"]
]
}
}
candles#
Candles of one market, in the orientation you request.
| Parameter | Type | Default | Description |
|---|---|---|---|
market |
string | required | BASE/QUOTE, each side in any asset format |
resolution |
seconds or alias | required | A resolution accepted by GET /candles, such as 300 or 5m |
limit |
integer | 200 |
Candles in the snapshot, 1 to 200 |
The snapshot is {type: 'candles', id, topic, market, resolution, candles}. resolution is in seconds and candles
holds the latest limit buckets up to and including the current one, oldest first, in the tuple format of
/candles. Buckets without fills are left out, so the snapshot can hold fewer rows than limit.
Updates are {type: 'candle', market, resolution, candle, topic}, one per bucket that fills changed, oldest first, at
most once per second per topic. Replace the candle with the same timestamp, or append it when it is new.
Hourly XLM/USDC snapshot with limit: 2:
{
"type": "candles",
"id": 6,
"topic": "candles:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/3600",
"market": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
"resolution": 3600,
"candles": [
[1791028800, "0.2201979", "0.2259326", "0.2159081", "0.2223594", "9.6849475", "2.1374418", 36],
[1791032400, "0.2230679", "0.2250866", "0.2188517", "0.2233012", "5.925785", "1.3193004", 23]
]
}
Update of a 5-minute CETES/XLM subscription:
{
"type": "candle",
"market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
"resolution": 300,
"candle": [1791034500, "0.2941018", "0.3033564", "0.2937915", "0.3033564", "2.269334", "0.6778706", 3],
"topic": "candles:CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/300"
}
Notes:
- The snapshot is sorted oldest first, the opposite of the REST default.
limitonly sizes the snapshot and is not part of the topic. Subscriptions that differ only inlimitshare a topic.- With a small
limit, the snapshot can be empty early in a bucket, before its first fill.
Limits#
| Limit | Value | When exceeded |
|---|---|---|
| Inbound message size | 16 KiB | The server closes the connection with code 1009 |
| Topics per connection | 20 | subscribe fails with Too many subscriptions (max 20) |
| Account topics per connection | 10 | subscribe fails with Too many account subscriptions (max 10) |
| Server ping | Every 30 seconds | A connection that sent neither a pong frame nor any message since the previous ping is terminated |
| Outgoing buffer | 4 MiB | A client that does not read fast enough is disconnected |
Browsers and most WebSocket libraries answer ping frames automatically, so a live client stays connected without extra work. Open more connections if you need more than 20 topics.
Reconnecting#
- Subscriptions belong to the connection. After a reconnect, subscribe again to every channel you need.
- The server does not replay updates missed while you were disconnected. The new snapshots replace whatever state you built before.
- Reconnect with a growing delay.
AxisStreamClientstarts at 1 second and doubles the delay up to 30 seconds. - Detect half-open connections: send
{op: 'ping'}periodically and reconnect when nothing arrives for twice that period.AxisStreamClientpings every 20 seconds. - While the connection is down, poll the REST API instead, for example
GET /contract,GET /account/:addressandGET /depth. The JS client'sAxisclass polls/contractand its accounts every 15 seconds by default until the stream is back. - A client that falls 4 MiB behind is dropped. Keep message handlers fast or subscribe to fewer topics.
Example#
A minimal client with the native WebSocket of a browser or of Node.js 22 or later:
const USDC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA'
const market = `XLM/${USDC}`
const ws = new WebSocket('wss://demo-api.axis.markets/ws')
ws.onopen = () => {
ws.send(JSON.stringify({op: 'subscribe', id: 1, channel: 'depth', market, limit: 10}))
ws.send(JSON.stringify({op: 'subscribe', id: 2, channel: 'trades', market}))
}
ws.onmessage = event => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'depth': //snapshot and updates both carry the whole book
console.log('bid', message.data.bestBid, 'ask', message.data.bestAsk)
break
case 'trades': //snapshot: the latest fills, newest first
console.log('recent fills', message.trades.length)
break
case 'trade': //update: one fill
console.log('fill', message.trade.sold, 'of', message.trade.soldAsset)
break
case 'error':
console.error('request', message.id, 'failed:', message.error)
break
}
}
ws.onclose = () => {
//every subscription is gone: reconnect with a backoff, then subscribe again
}
In an application, use AxisStreamClient from the JS client instead.
It reconnects with a backoff, resubscribes, routes messages by topic and pings the server. The Axis and
AxisAccount classes build on it to keep markets and accounts current, see
Axis, markets and accounts.