Skip to content

Spot Trading

This guide places a spot order with the TypeScript client: discover the spot markets, fund the spot subaccount, buy and sell, read balances and fills. For how spot markets work — subaccount bucket, order rules, fees — see Spot Markets.

Everything below assumes a connected CompositeClient and a LocalWallet, as set up in Wallet Setup.

Find the market

Spot markets are listed separately from perpetuals. The response carries everything needed to size and price an order.

const MARKET = 'MARTI-USDC';
 
const { markets } = await client.indexerClient.markets.getSpotMarkets(MARKET);
const market = markets[MARKET];
 
console.log(market.tickSize, market.stepSize);          // '0.01' '0.001'
console.log(market.baseAssetId, market.quoteAssetId);   // '10' '0'
console.log(market.buyFeeAssetId, market.sellFeeAssetId); // '0' '10'  — fee is paid in the asset sold

Omit the argument to get all spot markets. The ticker is BASE-QUOTE; marketType is always SPOT.

Use the spot subaccount

Spot orders must come from a subaccount in the reserved bucket 1 (number % 128 === 1). The client exports the number as SPOT_PARENT_SUBACCOUNT_NUMBER:

import { SubaccountInfo, SPOT_PARENT_SUBACCOUNT_NUMBER } from '@ritbit/v4-client-js';
 
const spot = SubaccountInfo.forLocalWallet(wallet, SPOT_PARENT_SUBACCOUNT_NUMBER); // subaccount 1

The subaccount is created on the first deposit. Placing a spot order from subaccount 0 fails with Spot orders must use subaccounts in the parent 1 bucket.

Fund it

A spot subaccount holds plain asset balances. Deposit the asset you intend to sell — the quote asset to buy, the base asset to sell. Amounts are human-readable strings; the client converts them with the asset's decimals.

// 100 USDC from the main account (asset 0 is the default)
await client.depositToSubaccount(spot, '100');
 
// 50 MARTI from the main account — the asset id comes from the market
await client.depositToSubaccount(spot, '50', undefined, Number(market.baseAssetId));
 
// or move USDC from the perpetual subaccount 0
const main = SubaccountInfo.forLocalWallet(wallet, 0);
await client.transferToSubaccount(main, wallet.address, SPOT_PARENT_SUBACCOUNT_NUMBER, '100');

Bank coins for an ERC-20 token are obtained by converting the token first — see Converting tokens.

Place a limit order

placeOrder is the same call as for perpetuals, with TickerType.SPOT as the last argument. Only OrderType.LIMIT is accepted; reduceOnly must be false.

import {
  OrderExecution, OrderSide, OrderTimeInForce, OrderType, TickerType,
} from '@ritbit/v4-client-js';
 
// Buy 10 MARTI at 9.90 USDC, resting for one hour
const tx = await client.placeOrder(
  spot,
  MARKET,
  OrderType.LIMIT,
  OrderSide.BUY,
  9.9,                      // price, in quote asset
  10,                       // size, in base asset
  Date.now() % 2_147_483_647, // clientId
  OrderTimeInForce.GTT,
  3600,                     // goodTilTimeInSeconds
  OrderExecution.DEFAULT,   // or POST_ONLY
  false,                    // postOnly
  false,                    // reduceOnly — must be false for spot
  undefined,                // triggerPrice — conditional orders are not supported
  undefined, undefined, undefined, undefined, undefined, undefined, undefined,
  TickerType.SPOT,
);

The order is checked against balances at placement: the quote balance must cover 10 × 9.90 USDC plus the fee. A sell order is checked against the base balance the same way.

Buy or sell at market

Spot v1 has no native market orders. Emulate one with an immediate-or-cancel limit order whose price protects against slippage: read the book, take the best opposite level (or the average over the levels you will consume), and allow a few percent.

const INDEXER = 'https://indexer.testnet.rubin.trade';
const book = await fetch(`${INDEXER}/v4/orderbooks/spotMarket/${MARKET}`).then((r) => r.json());
const bestAsk = Number(book.asks[0].price);
const limit = Math.ceil((bestAsk * 1.02) / Number(market.tickSize)) * Number(market.tickSize);
 
await client.placeOrder(
  spot, MARKET, OrderType.LIMIT, OrderSide.BUY,
  limit, 10, Date.now() % 2_147_483_647,
  OrderTimeInForce.IOC,     // short-term: fills what it can, cancels the rest
  undefined, OrderExecution.IOC, false, false,
  undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
  TickerType.SPOT,
);

Cancel

import { OrderFlags } from '@ritbit/v4-client-js';
 
await client.cancelOrder(
  spot, clientId, OrderFlags.LONG_TERM, MARKET,
  undefined, 3600,          // goodTilBlock (short-term) / goodTilTimeInSeconds (long-term)
  TickerType.SPOT,
);

Read balances, orders and fills

Balances are the assetPositions of subaccount 1. Orders and fills are filtered with the ticker and TickerType.SPOT, because a spot pair and a perpetual may share a ticker.

const { subaccount } = await client.indexerClient.account.getSubaccount(wallet.address, 1);
console.log(subaccount.assetPositions);
// { USDC: { symbol: 'USDC', size: '1.000000', ... }, MARTI: { symbol: 'MARTI', size: '10.000', ... } }
 
const orders = await client.indexerClient.account.getSubaccountOrders(
  wallet.address, 1, MARKET, TickerType.SPOT,
);
 
const { fills } = await client.indexerClient.account.getSubaccountFills(
  wallet.address, 1, MARKET, TickerType.SPOT,
);
console.log(fills[0].fee, fills[0].feeAssetId); // fee in the asset sold: '0' for a buy, '10' for a sell

Withdraw

// MARTI back to the main account as erc20:0x… coins
await client.withdrawFromSubaccount(spot, '10', undefined, undefined, Number(market.baseAssetId));

Streaming

The WebSocket channels are shared with perpetuals. Spot ids are product-qualified with the SPOT: prefix; a bare ticker is a perpetual market.

Channelid
v4_orderbookSPOT:MARTI-USDC
v4_tradesSPOT:MARTI-USDC
v4_candlesSPOT:MARTI-USDC/1MIN
v4_subaccountsrit1…/1
socket.subscribeToOrderbook('SPOT:MARTI-USDC');
socket.subscribeToTrades('SPOT:MARTI-USDC');

REST endpoints

Endpoint
MarketsGET /v4/spotMarkets
Order bookGET /v4/orderbooks/spotMarket/{ticker}
TradesGET /v4/trades/spotMarket/{ticker}
CandlesGET /v4/candles/spotMarkets/{ticker}
Orders / fillsGET /v4/orders, GET /v4/fills — subaccountNumber=1, market=MARTI-USDC, marketType=SPOT

Common errors

MessageCause
Spot orders must use subaccounts in the parent 1 bucketOrder placed from subaccount 0 or another non-spot bucket.
Spot orders only support LIMIT ordersOrderType.MARKET or a conditional type was passed.
spot v1 does not support market ordersA zero price reached the chain. Use an IOC limit order.
perpetual orders cannot use reserved spot parent subaccount 1A perpetual order from the spot subaccount. Only short-term IOC reduce-only is allowed there.
Collateralization failure on placementThe sold asset (plus fee) is not in the spot subaccount. Deposit first.
Spot market has no usable quote oracleThe quote asset's oracle price is missing (RUB1 pairs). Retry later.