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
fetchandWebSocket. Pass aWebSocketimplementation as an option where there is no global one. @stellar/stellar-sdk17 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}
}
xdris the base64 transaction envelope, already simulated, with its footprint and resource fee set.- The second argument contains
networkPassphrase, plusaddress(the source account) forkeepalive. - Resolve to
{signedTxXdr}with the signed base64 envelope.signerAddressis optional. Throw, or resolve to{error}, to cancel. - Sign with the transaction source account: the
publicKeyofAxisContractClient, or the address passed toaxis.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
signeroption ofAxis, an object{publicKey, signTransaction}, forkeepalive,market.requote()andmarket.subsidize(). - The
publicKeyandsignTransactionoptions ofAxisContractClient.
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:
- 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.
- It checks the XLM allowance of the AXIS contract. When the allowance is short or expires within about a day, the
trade carries an
approvefor exactly this trade plus your other open XLM orders, valid for about 30 days (see automatic allowance management). - It simulates the
tradecall, completes the footprint, asks your callback to sign once and submits. - It returns the filled amounts and the ID of the created order, which shows up in
account.getOrders()right away withpending: 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_000nis 100 XLM. - Prices are
buyingbase units per 1sellingbase unit, timesPRICE_SCALE(10^18). For two 7-decimal assets,250_000_000_000_000_000nis 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.