AXIS Docs

DevelopersJavaScript client

Getting started

Install the AXIS JavaScript client, point it at Testnet, sign transactions and place your first order.

The JS client is @axis-markets/client on npm (MIT). It wraps the AXIS contract, the AXIS API (REST and WebSocket) and the allowance bookkeeping a trader needs. This page uses version 0.8.2.

Install#

pnpm add @axis-markets/client @stellar/stellar-sdk

With npm:

npm install @axis-markets/client @stellar/stellar-sdk

Requirements#

  • Node.js 22 or newer, or a modern browser. The client uses the global fetch and WebSocket. Pass a WebSocket implementation as an option where there is no global one.
  • @stellar/stellar-sdk 17 or newer as a peer dependency. Version 17 changed the XDR representation, so older versions do not work.
  • TypeScript definitions ship with the package.

ESM and CommonJS#

The package is ESM first. A CommonJS bundle is published next to it, and the footprint helpers have their own subpath.

import {Axis, AxisContractClient, PRICE_SCALE} from '@axis-markets/client'
import {completeFootprint} from '@axis-markets/client/footprint'
const {Axis, AxisContractClient, PRICE_SCALE} = require('@axis-markets/client')
const {completeFootprint} = require('@axis-markets/client/footprint')

Module map#

Export Use it for
Axis Entry point for apps: contract state, markets and accounts, kept current through the AXIS API. See Axis, markets and accounts.
AxisMarket One market: depth, trades and candles streams, requote, subsidize. Get it from axis.getMarket().
AxisAccount One trader: open orders, balances, allowances, and trading with automatic approvals. Get it from axis.account().
AxisContractClient Direct contract calls: simulate, complete the footprint, sign and submit. Needs only an RPC server. See Contract client.
AxisApiClient, AxisApiError REST client of the AXIS API: quotes, depth, candles, orders, trades. See API and stream clients.
AxisStreamClient WebSocket client of the AXIS API with automatic reconnection.
TokenBalance Token balance, allowance and latest ledger reads over RPC.
@axis-markets/client/footprint Ledger keys and footprint completion for custom transaction pipelines.
Helpers orderId, planApproval, maxQuoteSpend, canonicalPair, compareAssets, pairKey, invertPrice, invertTicker, parseApiOrder, parseApiDate, toWsUrl.
Constants OrderKind, TradeDirection, ContractErrors, TokenErrors, PRICE_SCALE, APPROVAL_TTL_LEDGERS, APPROVAL_RENEW_LEDGERS, and the Emitter base class.

Use Axis when you build a trading UI or a bot that follows the book. Use AxisContractClient alone when you already know which orders to cross, for example from your own indexer.

Testnet configuration#

The client identifies assets by their token contract address, and all four Testnet assets have 7 decimals. The values below come from Networks and deployments, which also lists the asset issuers. Axis reads the contract address from the AXIS API, so contractId is optional there. AxisContractClient needs it.

import {Networks} from '@stellar/stellar-sdk'

export const TESTNET = {
    contractId: 'CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS',
    apiUrl: 'https://demo-api.axis.markets',
    wsUrl: 'wss://demo-api.axis.markets/ws',
    rpcUrl: 'https://soroban-testnet.stellar.org',
    networkPassphrase: Networks.TESTNET
}

export const XLM = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'
export const USDC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA'
export const EURC = 'CCUUDM434BMZMYWYDITHFXHDMIVTGGD6T2I5UKNX5BSLXLW7HVR4MCGZ'
export const CETES = 'CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC'

Important

networkPassphrase defaults to the public network in both Axis and AxisContractClient. On Testnet always pass Networks.TESTNET, otherwise every signature is made for the wrong network. Mainnet is not deployed yet.

Signing transactions#

The client never holds keys. Every call that submits a transaction hands it to a callback:

async function signTransaction(xdr, {networkPassphrase}) {
    // sign the base64 transaction envelope for this network
    return {signedTxXdr, signerAddress}
}
  • xdr is the base64 transaction envelope, already simulated, with its footprint and resource fee set.
  • The second argument contains networkPassphrase, plus address (the source account) for keepalive.
  • Resolve to {signedTxXdr} with the signed base64 envelope. signerAddress is optional. Throw, or resolve to {error}, to cancel.
  • Sign with the transaction source account: the publicKey of AxisContractClient, or the address passed to axis.account(). As the source account it also authorizes the contract call and any in-call approval, so one signature per transaction is enough.

Where the callback goes:

  • axis.account(address, {signTransaction}) for trading.
  • The signer option of Axis, an object {publicKey, signTransaction}, for keepalive, market.requote() and market.subsidize().
  • The publicKey and signTransaction options of AxisContractClient.

Browser wallet#

With Stellar Wallets Kit (package @creit-tech/stellar-wallets-kit, version 2 API), the kit's own signTransaction already resolves to {signedTxXdr, signerAddress}:

import {StellarWalletsKit, Networks} from '@creit-tech/stellar-wallets-kit'
import {FreighterModule, FREIGHTER_ID} from '@creit-tech/stellar-wallets-kit/modules/freighter'

StellarWalletsKit.init({modules: [new FreighterModule()], network: Networks.TESTNET})
StellarWalletsKit.setWallet(FREIGHTER_ID)
const {address} = await StellarWalletsKit.fetchAddress()

async function signTransaction(xdr, {networkPassphrase}) {
    return StellarWalletsKit.signTransaction(xdr, {address, networkPassphrase})
}

const account = axis.account(address, {signTransaction})

See Wallet integration for wallet-specific notes.

Server key#

On a server or in a bot, sign with a Keypair:

import {Keypair, TransactionBuilder} from '@stellar/stellar-sdk'

const keypair = Keypair.fromSecret(process.env.AXIS_SECRET)

async function signTransaction(xdr, {networkPassphrase}) {
    const tx = TransactionBuilder.fromXdr(xdr, networkPassphrase)
    tx.sign(keypair)
    return {signedTxXdr: tx.toXdr(), signerAddress: keypair.publicKey()}
}

contract.basicNodeSigner(keypair, Networks.TESTNET) from @stellar/stellar-sdk returns an object with an equivalent signTransaction.

Your first order#

This example sells 100 XLM for USDC at 0.25 USDC per XLM, listens for fills and cancels the rest.

1. Prepare a Testnet account#

The account needs XLM and a USDC trustline. Without the trustline the contract rejects the order with 708, because the account could not receive USDC.

import {Asset, Keypair, Networks, Operation, TransactionBuilder, rpc} from '@stellar/stellar-sdk'

const server = new rpc.Server('https://soroban-testnet.stellar.org')
const keypair = Keypair.random()
console.log('AXIS_SECRET', keypair.secret())

// create and fund the account
await fetch(`https://friendbot.stellar.org/?addr=${keypair.publicKey()}`)

// trust USDC
const source = await server.getAccount(keypair.publicKey())
const trust = new TransactionBuilder(source, {fee: '100000', networkPassphrase: Networks.TESTNET})
    .addOperation(Operation.changeTrust({
        asset: new Asset('USDC', 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5')
    }))
    .setTimeout(60)
    .build()
trust.sign(keypair)
const {hash} = await server.sendTransaction(trust)
await server.pollTransaction(hash)

2. Trade#

import {Axis} from '@axis-markets/client'
import {Keypair, Networks, TransactionBuilder} from '@stellar/stellar-sdk'

const XLM = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'
const USDC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA'

const keypair = Keypair.fromSecret(process.env.AXIS_SECRET)

async function signTransaction(xdr, {networkPassphrase}) {
    const tx = TransactionBuilder.fromXdr(xdr, networkPassphrase)
    tx.sign(keypair)
    return {signedTxXdr: tx.toXdr(), signerAddress: keypair.publicKey()}
}

const axis = new Axis({
    apiUrl: 'https://demo-api.axis.markets',
    rpcUrl: 'https://soroban-testnet.stellar.org',
    networkPassphrase: Networks.TESTNET
})
await axis.connect() // the contract address comes from the AXIS API

// assets in either order, the market keeps the contract's canonical order
const market = axis.getMarket(XLM, USDC)
console.log('market', market.key)

const account = axis.account(keypair.publicKey(), {signTransaction})
await account.ready

account.on('fill', ({order, sold, bought}) => console.log(`order ${order.id}: sold ${sold}, received ${bought}`))
account.on('filled', ({order}) => console.log(`order ${order.id} filled`))

// limit sell: crosses any matching orders first, the remainder stays on the book
const {sold, bought, orderId} = await account.sell({
    selling: XLM,
    buying: USDC,
    amount: 1_000_000_000n, // 100 XLM
    price: 250_000_000_000_000_000n // 0.25 USDC per XLM
})
console.log({sold, bought, orderId})

// keep the order for a minute, fills are reported meanwhile
await new Promise(resolve => setTimeout(resolve, 60_000))

if (orderId && account.getOrder(orderId)) {
    await account.cancel([orderId])
}
axis.close()

What happens in account.sell:

  1. The account asks the AXIS API for a direct quote of 100 XLM limited to 0.25 USDC per XLM and lists the quoted orders in the trade: every backed order within the limit, even when they cover only part of 100 XLM. When no order reaches the limit, the quote lists nothing and the whole order is placed on the book.
  2. It checks the XLM allowance of the AXIS contract. When the allowance is short or expires within about a day, the trade carries an approve for exactly this trade plus your other open XLM orders, valid for about 30 days (see automatic allowance management).
  3. It simulates the trade call, completes the footprint, asks your callback to sign once and submits.
  4. It returns the filled amounts and the ID of the created order, which shows up in account.getOrders() right away with pending: true.

Units#

The client passes amounts and prices to the contract as raw bigint values:

  • Amounts are integer base units. A 7-decimal token has 10,000,000 base units per token, so 1_000_000_000n is 100 XLM.
  • Prices are buying base units per 1 selling base unit, times PRICE_SCALE (10^18). For two 7-decimal assets, 250_000_000_000_000_000n is 0.25. See price and amount units.

Order, trade, quote and backing responses of the AXIS API carry these integer base units and 18-decimal prices as strings. /depth, /candles and /ticker/24h return display decimals instead (token amounts and quote-per-base prices). Convert human values with a small helper:

import {PRICE_SCALE} from '@axis-markets/client'

// '12.5' -> 125000000n for a 7-decimal token
function toUnits(value, decimals = 7) {
    const [whole, fraction = ''] = String(value).split('.')
    return BigInt(whole + fraction.padEnd(decimals, '0').slice(0, decimals))
}

// 125000000n -> '12.5'
function fromUnits(units, decimals = 7) {
    const scale = 10n ** BigInt(decimals)
    const fraction = (units % scale).toString().padStart(decimals, '0').replace(/0+$/, '')
    return (units / scale).toString() + (fraction ? '.' + fraction : '')
}

// human price (buying tokens per selling token) -> contract price
function toPrice(value, sellingDecimals = 7, buyingDecimals = 7) {
    return toUnits(value, 18 + buyingDecimals - sellingDecimals)
}

console.log(toUnits('100')) // 1000000000n
console.log(fromUnits(125_000_000n)) // '12.5'
console.log(toPrice('0.25') === PRICE_SCALE / 4n) // true

The helpers truncate digits beyond the token precision and handle non-negative values only.