DevelopersJavaScript client
API and stream clients
REST and WebSocket clients for the AXIS API, the TokenBalance RPC helper and the utility functions of the JS client.
The Axis class builds on these pieces (see Axis, markets and accounts). Use them directly when you
need raw API data, for example in a router, a bot or a backend that already has its own state. The wire formats are
documented in the REST API and WebSocket API pages.
AxisApiClient#
import {AxisApiClient} from '@axis-markets/client'
const api = new AxisApiClient('https://demo-api.axis.markets')
const {ticker} = await api.getTicker24h()
new AxisApiClient(serverUrl) trims trailing slashes and throws a TypeError without a URL. It uses the global
fetch and has no other dependency. Responses are the API's JSON as is, with assets as C... token contract addresses. Order, trade, quote and backing
responses carry integer base units and 18-decimal prices as strings, while getDepth, getCandles and
getTicker24h return display decimals.
Methods#
| Method | Route | Parameters | Result |
|---|---|---|---|
quoteSell({sellingAsset, buyingAsset, amount, direct, maxPrice}) |
GET /quote, direction=strict_send |
amount: exact input in base units. direct: true restricts the route to one hop. maxPrice (with direct) quotes the crossing of a limit order. |
Quote with up to 10 routes, best first |
quoteBuy({sellingAsset, buyingAsset, amount, direct, maxPrice}) |
GET /quote, direction=strict_receive |
amount: exact output in base units. |
Quote |
getDepth({market, depth, step, limit}) |
GET /depth |
market is BASE/QUOTE. |
Aggregated bids and asks |
getCandles({market, from, to, resolution, order}) |
GET /candles |
Always pass from (UNIX seconds). Without it the window starts at 0 and comes back empty. |
[ts, open, high, low, close, baseVolume, quoteVolume, trades] rows |
getTicker24h() |
GET /ticker/24h |
none | {ledger, timestamp, ticker} |
getMarkets({cursor, limit}) |
GET /markets |
Paging. | Pairs with live orders |
getContract() |
GET /contract |
none | {address, frozen, config, markets} |
getOrders({owner, asset, cursor, limit}) |
GET /order |
asset is one address or an array. Two addresses select a pair. |
Live orders, oldest first |
getOrder(id) |
GET /order/:id |
Decimal ID or bigint. |
Live order, AxisApiError with status 404 when it is gone |
getAccount(address) |
GET /account/:address |
{address, ledger, orders, backing} |
|
getOrderHistory({owner, pair, cursor, limit}) |
GET /order-history |
pair is an array of two token addresses. |
Filled, canceled and expired orders, newest first |
getTrades({trader, pair, cursor, limit}) |
GET /trades |
pair is an array of two token addresses. |
Trade and swap records, newest first |
getFailures({account, fn, cursor, limit}) |
GET /failures |
account matches the caller and both parties of a failed transfer, fn the contract function. |
Failed AXIS calls (also through another contract, or caught by one), newest first, for diagnostics |
Parameters that are undefined or null are left out of the query, and arrays become repeated keys. Quote and market
parameters accept XLM, native, CODE:ISSUER, CODE-ISSUER or a C... address. Order IDs are not sequential:
page with the cursor of the last row.
Quotes#
const XLM = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'
const USDC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA'
// sell 10 XLM for USDC
const quote = await api.quoteSell({sellingAsset: XLM, buyingAsset: USDC, amount: '100000000'})
if (quote.status === 'success') {
const best = quote.paths[0]
console.log(best.sold, best.bought, best.path.length)
}
A quote has a status:
success:pathsholds up to 10 routes, best first. Each route hassold,boughtandpath, a list of hops{selling, buying, orders, worstPrice}.unfeasible: the book cannot fill the whole amount.errorsays why. A quote returns a partial route only withmaxPrice, seeGET /quote.rejected: the contract is frozen, or the asset to buy requires issuer authorization that the AXIS contract does not have yet.errorsays which.
These are normal responses. AxisApiError is thrown only for HTTP errors, for example status 409 while the service is
not ready to quote. A strict_send route can sell slightly less than amount: input worth less than one unit of
output at the worst crossed price stays with the trader, so a quote for 100000000 can report sold: "99999999".
From a quote to a swap#
Each hop becomes a TradeStep: the hop's buying asset and its order IDs. The
amounts come from the route, with a slippage margin on the side that is not fixed:
import {TradeDirection} from '@axis-markets/client'
function swapFromQuote(quote, trader, slippageBps = 50n) {
const best = quote.paths[0]
const path = best.path.map(hop => ({asset: hop.buying, orders: hop.orders.map(id => BigInt(id))}))
const sold = BigInt(best.sold)
const bought = BigInt(best.bought)
if (quote.direction === 'strict_send') {
// exact input, minimum output
const buyingAmount = bought * (10_000n - slippageBps) / 10_000n
return {direction: TradeDirection.Sell, trader, selling: quote.sellingAsset, sellingAmount: sold, buyingAmount, path}
}
// exact output, maximum input
const sellingAmount = (sold * (10_000n + slippageBps) + 9_999n) / 10_000n
return {direction: TradeDirection.Buy, trader, selling: quote.sellingAsset, sellingAmount, buyingAmount: bought, path}
}
const params = swapFromQuote(quote, trader)
const [sold, bought] = await client.swap({...params, approve: {amount: params.sellingAmount, liveUntil}})
client, trader and liveUntil are set up as in Contract client. The contract
checks the bounds on the whole route and fails with 709 when the book moved beyond them.
From a quote to a market order limit#
A direct quote (direct: true) has one hop whose worstPrice is the highest price among the crossed orders, in the
makers' terms (maker buying per maker selling). Use it as the limit of a Fill trade:
- A
BuyusesworstPriceas is: it is the maximumsellingperbuying. - A
SellusesinvertPrice(worstPrice), rounded down: the minimumbuyingperselling.
Either limit accepts every quoted order.
import {OrderKind, invertPrice} from '@axis-markets/client'
const direct = await api.quoteSell({sellingAsset: XLM, buyingAsset: USDC, amount: '100000000', direct: true})
if (direct.status !== 'success') {
throw new Error(direct.error)
}
const [hop] = direct.paths[0].path
const [sold, bought] = await client.sell({
kind: OrderKind.Fill,
trader,
amount: 100_000_000n,
selling: XLM,
buying: USDC,
price: invertPrice(BigInt(hop.worstPrice)),
orders: hop.orders.map(id => BigInt(id))
})
This is what AxisAccount does for a sell or buy without a price.
AxisApiError#
Every non-success HTTP response throws an AxisApiError. message is the API's error text (or the HTTP status
text when the body is not JSON) and status is the status of the JSON body, or the HTTP status code when the body
has none.
import {AxisApiError} from '@axis-markets/client'
try {
await api.getOrder('104716339259087188276464058217096427616')
} catch (e) {
if (e instanceof AxisApiError && e.status === 404) {
console.log('the order is gone: filled, removed or expired')
} else {
throw e
}
}
AxisStreamClient#
import {AxisStreamClient} from '@axis-markets/client'
const stream = new AxisStreamClient('wss://demo-api.axis.markets/ws')
stream.on('open', () => console.log('connected'))
stream.on('close', () => console.log('disconnected, reconnecting'))
const unsubscribe = stream.subscribe('trades', {market: '*'}, message => {
console.log(message.type, message.topic)
})
Stream options#
new AxisStreamClient(url, options) throws a TypeError without a URL, and an Error when no WebSocket
implementation is available.
| Option | Default | Meaning |
|---|---|---|
WebSocket |
global WebSocket |
WebSocket class to use. |
pingInterval |
20000 |
Milliseconds between {op: 'ping'} messages. With no message received for two intervals, the connection counts as dead and is reopened. |
minReconnectDelay |
1000 |
First reconnect delay in milliseconds, doubled after every failed attempt. |
maxReconnectDelay |
30000 |
Upper bound of the reconnect delay. |
Subscriptions#
subscribe(channel, params, handler) returns an unsubscribe function. The first subscription opens the connection.
- The client sends
{op: 'subscribe', id, channel, ...params}. The server answers with a snapshot that echoesidand names atopic, then pushes updates tagged with that topic. - The handler receives the snapshot, every update, and a message with
type: 'error'when the server rejects the subscription. A rejected subscription is dropped. - Unsubscribing sends
{op: 'unsubscribe', channel, ...params}once no other local subscription shares the topic. - A handler that throws is logged and does not affect the others.
connectedtells whether the socket is open.close()closes it, drops every subscription and stops reconnecting.- The client emits
open,close, anderrorfor server errors that are not tied to a subscription.
| Channel | Params | Content |
|---|---|---|
contract |
none | Contract state (frozen flag, configuration, markets) and new ledgers. |
account |
address |
Open orders, backing, swaps and the trades in which the account was the taker. |
trades |
market (BASE/QUOTE) or * |
Recent trades, then every new trade. |
ticker |
none | 24h ticker of every market, then updates. |
depth |
market, depth, step, limit |
Aggregated depth, then updates. |
candles |
market, resolution, limit (1 to 200) |
Recent candles, then every candle a trade changes. |
The server accepts at most 20 subscriptions and 10 accounts per connection. Message formats and the other limits are in the WebSocket API.
Reconnection#
When the socket closes, the client emits close and schedules a reconnect after minReconnectDelay * 2^failures,
capped at maxReconnectDelay. The failure count resets after a successful open. On open it sends every active
subscription again, so each handler receives a fresh snapshot, and emits open. It stops reconnecting after close()
or once no subscription is left. Changes that happened while disconnected arrive only through the new snapshots, so
handlers should treat every snapshot as a full replacement.
TokenBalance#
TokenBalance reads token state over RPC with free simulations. Nothing is submitted. Axis creates one as
axis.tokenBalances when rpcUrl is set.
import {TokenBalance} from '@axis-markets/client'
import {Networks} from '@stellar/stellar-sdk'
const tokens = new TokenBalance({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: Networks.TESTNET,
spender: 'CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS'
})
const allowance = await tokens.getAllowance(USDC, trader)
const balance = await tokens.getBalance(USDC, trader)
const ledger = await tokens.getLatestLedger()
| Member | Returns | Meaning |
|---|---|---|
new TokenBalance({rpcUrl, networkPassphrase, spender, server}) |
spender is the AXIS contract. server reuses an existing rpc.Server. |
|
getAllowance(token, owner) |
Promise<bigint> |
The token's allowance(owner, spender). Zero once expired. Throws when the simulation fails. |
getBalance(token, owner) |
Promise<bigint> |
The token's balance(owner). Zero when the simulation fails, for example without a trustline. For XLM it includes the account reserve. |
getLatestLedger() |
Promise<number> |
Latest ledger sequence. |
The backing the contract checks at match time is min(balance, allowance), read the same way (see
Effective depth).
Helper functions#
| Function | Returns | Meaning |
|---|---|---|
canonicalPair(x, y) |
[base, quote] |
The pair in the contract's market order, which compares encoded addresses, not strings. |
compareAssets(x, y) |
number |
Comparator for that order: negative when x sorts first, 0 when equal. |
pairKey(x, y) |
string |
Canonical key base/quote, used by axis.markets and market.key. |
invertPrice(price) |
bigint |
floor(10^36 / price), the price seen from the other side. 0n for a non-positive price. |
invertTicker(entry) |
ticker entry | The 24h entry of base/quote seen as quote/base: prices inverted, high and low swapped, volumes swapped, change recomputed. |
parseApiOrder(raw) |
AccountOrder |
API order JSON to the client form: bigint amount and price, timestamps in milliseconds, expires in seconds, backing fields. |
parseApiDate(value) |
number |
API timestamp YYYY-MM-DD HH:MM:SS (UTC) to UNIX milliseconds. 0 when missing or invalid. Numbers pass through. |
toWsUrl(apiUrl) |
string |
WebSocket endpoint of an API base URL. https://demo-api.axis.markets becomes wss://demo-api.axis.markets/ws. |
maxQuoteSpend(amount, price) |
bigint |
ceil(amount * price / 10^18), the most a Buy of amount at limit price can cost. 0n for non-positive input. |
planApproval(params) |
approval or undefined |
The approval decision of automatic allowance management. |
orderId(owner, nonce) |
bigint |
Order ID for an owner and a u64 nonce. Throws a RangeError outside u64. |
planApproval({required, committed, allowance, liveUntil, ledger}) returns undefined when allowance covers
required + committed and liveUntil is 0 or not within APPROVAL_RENEW_LEDGERS (17,280) of ledger. Otherwise it
returns {amount: required + committed, liveUntil: ledger + APPROVAL_TTL_LEDGERS} with APPROVAL_TTL_LEDGERS equal
to 518,400. The amount can be lower than the current allowance when the call renews an allowance close to expiry.
import {PRICE_SCALE, canonicalPair, invertPrice, maxQuoteSpend, pairKey, planApproval} from '@axis-markets/client'
const [base, quote] = canonicalPair(XLM, USDC) // [USDC, XLM]
console.log(pairKey(XLM, USDC) === `${base}/${quote}`) // true
console.log(invertPrice(PRICE_SCALE / 4n)) // 4000000000000000000n, 0.25 seen from the other side is 4
console.log(maxQuoteSpend(1_000_000_000n, PRICE_SCALE / 4n)) // 250000000n, 100 XLM at 0.25 costs at most 25 USDC
// sell 100 XLM with 50 XLM already in open orders and an allowance of 120 XLM
console.log(planApproval({
required: 1_000_000_000n,
committed: 500_000_000n,
allowance: 1_200_000_000n,
liveUntil: 6_000_000,
ledger: 5_000_000
})) // {amount: 1500000000n, liveUntil: 5518400}
Emitter is the event base class of Axis, AxisAccount and AxisStreamClient: on(event, listener) and
once(event, listener) return an unsubscribe function, and off(event, listener) removes a listener.