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 soldOmit 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 1The 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 sellWithdraw
// 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.
| Channel | id |
|---|---|
v4_orderbook | SPOT:MARTI-USDC |
v4_trades | SPOT:MARTI-USDC |
v4_candles | SPOT:MARTI-USDC/1MIN |
v4_subaccounts | rit1…/1 |
socket.subscribeToOrderbook('SPOT:MARTI-USDC');
socket.subscribeToTrades('SPOT:MARTI-USDC');REST endpoints
| Endpoint | |
|---|---|
| Markets | GET /v4/spotMarkets |
| Order book | GET /v4/orderbooks/spotMarket/{ticker} |
| Trades | GET /v4/trades/spotMarket/{ticker} |
| Candles | GET /v4/candles/spotMarkets/{ticker} |
| Orders / fills | GET /v4/orders, GET /v4/fills — subaccountNumber=1, market=MARTI-USDC, marketType=SPOT |
Common errors
| Message | Cause |
|---|---|
Spot orders must use subaccounts in the parent 1 bucket | Order placed from subaccount 0 or another non-spot bucket. |
Spot orders only support LIMIT orders | OrderType.MARKET or a conditional type was passed. |
spot v1 does not support market orders | A zero price reached the chain. Use an IOC limit order. |
perpetual orders cannot use reserved spot parent subaccount 1 | A perpetual order from the spot subaccount. Only short-term IOC reduce-only is allowed there. |
| Collateralization failure on placement | The sold asset (plus fee) is not in the spot subaccount. Deposit first. |
Spot market has no usable quote oracle | The quote asset's oracle price is missing (RUB1 pairs). Retry later. |