Velocity ProtocolDevelopers
Velocity SDK

Orders

How it works

A taker order fills by price, not by source. It walks the DLOB (Decentralized Limit Order Book) from the best price level down, and at each level the AMM's quote, the resting order, and any JIT (Just-In-Time) maker quote compete on price. One order can therefore fill from more than one source.

A market order's auction bounds what price is acceptable while it runs, and the AMM comes last: it takes only the residual fill, once the maker list is exhausted and size still crosses the order's limit price. See Orderbook & Matching for the full rules.

How one taker order fills

Taker order to Take the next best price levelTake the next best price level to Is the AMM quote better than this level?yesyesnonothenthenBest quote at this level fills to Size left, and more levels?yesyesnonoResidual AMM fill to Fill completeTaker orderyour limit caps fillsTake the next best pricelevelbest price the filler bringsIs the AMM quote betterthan this level?AMM fills firstup to this priceBest quote at this levelfillsresting or JIT makerSize left, and more levels?Residual AMM fillif size still crossesFill complete
Shows how a single taker order fills across price levels. Source: Matching Engine and Orderbook and Matching in these docs. There is no fixed JIT, then orderbook, then AMM order: at each level the AMM quote and the best maker quote (resting or just in time) compete on price, and the AMM comes last only for the residual fill. The docs do not say which side wins when two quotes tie on price, only that a priority flag decides. The walk is per fill transaction and best effort: a filler can only match the maker orders it includes, so a better resting order can be skipped. Among tied resting orders, the earlier one fills first.

Orders are stored in the user account and have both an onchain order ID and an optional user-assigned ID for tracking. When an order fills, the position updates automatically, and fill events carry the execution details.

Perp markets only. Velocity's spot markets are collateral and borrow-lend only: order-book trading on spot markets was removed. placeSpotOrder, placeAndTakeSpotOrder, placeAndMakeSpotOrder, fillSpotOrder, and their getXIx builders all throw a client-side error without sending a transaction ("Spot DLOB trading is disabled; spot balances, deposits, and swaps remain available.").

The onchain SpotDlobTradingDisabled error (6350 / 0x18ce) is a separate thing. The spot order instructions are gone from the program entirely, so what raises it is a shared order instruction handed a MarketType::Spot. Everything on this page applies to perp orders.

Order Types

TypeDescription
MARKETExecutes immediately. Opens a JIT auction (controlled by auctionStartPrice, auctionEndPrice, auctionDuration); JIT makers, resting DLOB orders, and the AMM compete on price at each level, and any size left after the auction can still fill against the DLOB or AMM up to the order's limit.
LIMITRests on the DLOB at a fixed price until filled or canceled. Set postOnly: PostOnlyParams.MUST_POST_ONLY to guarantee maker status and avoid crossing the spread.
ORACLELike a market order, but auction prices and the resting limit price are expressed as offsets from the oracle price (not absolute prices). Useful for market makers who want tight spreads without hardcoding prices.
TRIGGER_MARKETA stop/take-profit market order. Executes as a market order when the market's trigger price crosses triggerPrice in the specified triggerCondition direction. That price is not always the oracle price: see Trigger Orders.
TRIGGER_LIMITA stop/take-profit limit order. Same trigger mechanism as TRIGGER_MARKET, but executes as a limit order at price once triggered.

Post-Only Params

When placing limit orders, postOnly controls maker and taker behavior:

ValueBehavior
PostOnlyParams.NONEOrder can be maker or taker (default)
PostOnlyParams.MUST_POST_ONLYTransaction fails if the order would cross the spread
PostOnlyParams.TRY_POST_ONLYOrder is silently skipped (not placed) if it would cross; tx succeeds
PostOnlyParams.SLIDEOrder price is adjusted one tick inside the spread to guarantee maker status

oraclePriceOffset is a BN

OrderParams.oraclePriceOffset (and Order.oraclePriceOffset on filled orders) is a signed BN in PRICE_PRECISION (1e6) units: it was widened from number to BN (i64 onchain). Always wrap raw numbers with new BN(...); passing a plain JS number will fail type-checking (and, if bypassed, will not round-trip correctly through Borsh encoding).

import { BN, PRICE_PRECISION } from "@velocity-exchange/sdk";

// +$0.30 above oracle
const oraclePriceOffset = new BN(0.3 * PRICE_PRECISION.toNumber());
// or, using client precision helpers:
const oraclePriceOffset2 = velocityClient.convertToPricePrecision(0.3); // already a BN

SDK Usage

This page focuses on placing and canceling perp orders via VelocityClient. The examples use helper builders like getMarketOrderParams(...) to stay concise.

Build Market Order Params

import { MarketType, PositionDirection, getMarketOrderParams } from "@velocity-exchange/sdk";

const orderParams = getMarketOrderParams({
  marketIndex: 0,
  marketType: MarketType.PERP,
  direction: PositionDirection.LONG,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1), // 1 SOL, BN at 1e9
});

Build Order Params (Generic Helper)

getOrderParams(...) is the single helper that covers limit, market, oracle, and trigger orders.

import { OrderType, PositionDirection, getOrderParams } from "@velocity-exchange/sdk";

const orderParams = getOrderParams({
  orderType: OrderType.LIMIT,
  marketIndex: 0,
  direction: PositionDirection.LONG,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1), // 1 base unit, BN(1e9)
  price: velocityClient.convertToPricePrecision(21.23),      // $21.23, BN(21_230_000)
});

Place a Perp Order

import { PositionDirection, getMarketOrderParams } from "@velocity-exchange/sdk";

// Assumes `velocityClient` is subscribed.
const txSig = await velocityClient.placePerpOrder(
  getMarketOrderParams({
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
  })
);
console.log(txSig);

Place Multiple Orders

import { MarketType, OrderType, PositionDirection } from "@velocity-exchange/sdk";

await velocityClient.placeOrders([
  {
    orderType: OrderType.LIMIT,
    marketType: MarketType.PERP,
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
    price: velocityClient.convertToPricePrecision(21.23),
  },
  {
    orderType: OrderType.LIMIT,
    marketType: MarketType.PERP,
    marketIndex: 0,
    direction: PositionDirection.SHORT,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
    oraclePriceOffset: velocityClient.convertToPricePrecision(0.05), // BN, not .toNumber()
  },
]);

Bulk margin enforcement. placeOrders and placeScaleOrders run the initial-margin check once per risk scope the batch touches, not once at the end of the batch. A batch that slipped past a weaker or absent margin gate under the old check may now be rejected with InsufficientCollateral (error 6003 / 0x1773).

Scale Orders (Price Ladder)

placeScaleOrders places a ladder of resting limit orders in one instruction. It takes a total size and a price range; the program splits the size across orderCount orders at evenly spaced prices and places them all. Every order in the ladder is OrderType.LIMIT and rests until matched, nothing fills inside the instruction.

FieldTypeMeaning
marketTypeMarketTypeUse MarketType.PERP. A SPOT ladder is rejected onchain with SpotDlobTradingDisabled.
directionPositionDirectionSide of every order in the ladder.
marketIndexnumberPerp market index.
totalBaseAssetAmountBNTotal size to distribute, in BASE_PRECISION (1e9).
startPriceBNFirst rung, in PRICE_PRECISION (1e6).
endPriceBNLast rung, in PRICE_PRECISION (1e6).
orderCountnumberNumber of orders, minimum 2, maximum 32.
sizeDistributionSizeDistributionFLAT, ASCENDING, or DESCENDING (see below).
reduceOnlybooleanApplied to every order in the ladder.
postOnlyPostOnlyParamsApplied to every order in the ladder.
bitFlagsnumberApplied to the first order only; the rest are placed with bitFlags: 0.
maxTsBN | nullExpiry timestamp applied to every order, or null for none.

Every field is required, there are no optional defaults.

Price range rules. startPrice and endPrice must differ, and the range has to run away from the market in the ladder's direction:

  • PositionDirection.LONG requires startPrice > endPrice (buy the ladder down).
  • PositionDirection.SHORT requires startPrice < endPrice (sell the ladder up).

Prices are spaced evenly: with orderCount = 2 the rungs are exactly startPrice and endPrice; with more, the step is |startPrice - endPrice| / (orderCount - 1) and the last rung is set to endPrice exactly so rounding cannot drift.

Size distribution. ASCENDING and DESCENDING are defined relative to the start price:

ValueSizes
SizeDistribution.FLATEqual size per order (rounded down to the market's orderStepSize), with the leftover added to the last order.
SizeDistribution.ASCENDINGSmallest order at startPrice, largest at endPrice. Sizes follow 1x, 1.5x, 2x, 2.5x, and so on.
SizeDistribution.DESCENDINGThe same ladder reversed: largest order at startPrice, smallest at endPrice.

totalBaseAssetAmount must be at least orderStepSize * orderCount, otherwise there is not enough size to give every rung a valid order. Each rung is rounded to the market's orderStepSize and the last one absorbs the rounding difference.

Errors. The program validates every parameter before it expands the ladder, and returns the specific error code, not a generic one: InvalidOrderScaleOrderCount (6346 / 0x18ca) for an orderCount outside 2 to 32, InvalidOrderScalePriceRange (6347 / 0x18cb) for equal prices or a range running the wrong way for the direction, and OrderAmountTooSmall (6059 / 0x17ab) when totalBaseAssetAmount is below orderStepSize * orderCount. Each carries the offending value in the transaction logs. The 32-order ceiling is also the account's total open-order capacity, so a ladder only fits if the subaccount has that many free order slots.

import {
  MarketType,
  PositionDirection,
  PostOnlyParams,
  SizeDistribution,
} from "@velocity-exchange/sdk";

// 10 SOL of bids laddered from $150 down to $140, in 5 orders,
// with the largest order at the top of the range.
await velocityClient.placeScaleOrders({
  marketType: MarketType.PERP,
  direction: PositionDirection.LONG,
  marketIndex: 0,
  totalBaseAssetAmount: velocityClient.convertToPerpPrecision(10),
  startPrice: velocityClient.convertToPricePrecision(150),
  endPrice: velocityClient.convertToPricePrecision(140),
  orderCount: 5,
  sizeDistribution: SizeDistribution.DESCENDING,
  reduceOnly: false,
  postOnly: PostOnlyParams.MUST_POST_ONLY,
  bitFlags: 0,
  maxTs: null,
});

preparePlaceScaleOrdersTx(params, txParams?, subAccountId?) returns the unsigned transaction as { placeScaleOrdersTx }, and getPlaceScaleOrdersIx(params, subAccountId?) returns the single instruction for batching it with others.

For the trader-facing description of scale orders, see Order Types.

Oracle / Auction-Style Orders

Oracle orders have prices that track the oracle feed with an offset. They go through a JIT auction before execution, with auction prices that gradually converge from the start offset to the end offset relative to oracle.

Important: For OrderType.ORACLE, auctionStartPrice, auctionEndPrice, and oraclePriceOffset are all offsets from the oracle price (in PRICE_PRECISION, 1e6), not absolute prices, and all three are BN.

import { OrderType, PRICE_PRECISION, PositionDirection } from "@velocity-exchange/sdk";

const marketIndex = 18;

// Offsets are relative to oracle price (in PRICE_PRECISION)
// For a long: auction starts at a better (lower) price and ends at a worse (higher) price
const auctionStartPrice = PRICE_PRECISION.muln(-5).divn(10); // -$0.50 below oracle
const auctionEndPrice = PRICE_PRECISION.muln(5).divn(10);    // +$0.50 above oracle

const orderParams = {
  orderType: OrderType.ORACLE,
  baseAssetAmount: velocityClient.convertToPerpPrecision(10),
  direction: PositionDirection.LONG,
  marketIndex,
  auctionStartPrice,
  auctionEndPrice,
  oraclePriceOffset: velocityClient.convertToPricePrecision(0.30), // +$0.30 from oracle, BN
  auctionDuration: 30, // 400ms wall-clock units, 12s
};

await velocityClient.placePerpOrder(orderParams);

Builder Codes

OrderParams accepts an optional builderIdx and builderFeeTenthBps pair to attribute an order to an approved builder and charge an extra builder fee. The fee is in tenths of a basis point, so 100 is 10 bps, or 0.1% of notional. Both fields are optional; omit them for ordinary orders.

builderIdx indexes into the placing user's RevenueShareEscrow.approvedBuilders list, so the builder must be approved first with changeApprovedBuilder(...). Whatever value is passed, the program caps the fee actually charged at MAX_BUILDER_FEE_TENTH_BPS, which is 1000 tenth-bps, or 1% of notional. That is an onchain Rust constant rather than an SDK export, so treat it as a fixed value instead of something to import.

import { OrderType, PositionDirection } from "@velocity-exchange/sdk";

const orderParams = {
  orderType: OrderType.LIMIT,
  marketIndex: 0,
  direction: PositionDirection.LONG,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1),
  price: velocityClient.convertToPricePrecision(21.23),
  builderIdx: 0,           // index into this user's approved-builders list
  builderFeeTenthBps: 100, // 10 bps, 0.1% of notional
};

await velocityClient.placePerpOrder(orderParams);

If the taker is referred (their UserStats.referrerStatus has the BuilderReferral bit) or the order itself carries a builder code, fillers must attach the taker's RevenueShareEscrow when filling (see getFillPerpOrderIx below) or the fill is rejected with UnableToLoadRevenueShareAccount.

Cancel Orders

Cancel a specific order by its onchain order ID.

await velocityClient.cancelOrder(1);

Cancel multiple specific orders by their onchain order IDs in a single transaction.

await velocityClient.cancelOrdersByIds([1, 2, 3]);

Cancel all orders matching the given market and direction filters. Omit a filter to match all on that field; omit all parameters to cancel every open order.

import { MarketType, PositionDirection } from "@velocity-exchange/sdk";

// Cancel all long perp orders on market 0
await velocityClient.cancelOrders(MarketType.PERP, 0, PositionDirection.LONG);

// Cancel all orders across all markets
await velocityClient.cancelOrders();

getCancelOrdersIx (the instruction-builder variant, used when composing a transaction by hand) accepts null explicitly for any filter, in addition to undefined.

Cancel and Place (Atomic)

Atomically cancels existing orders and places new ones in a single transaction. This is the preferred approach for market makers who need to replace quotes without risk of being filled on stale orders in the gap between a separate cancel and re-quote.

import { MarketType, OrderType, PositionDirection } from "@velocity-exchange/sdk";

await velocityClient.cancelAndPlaceOrders(
  { marketType: MarketType.PERP, marketIndex: 0 },
  [
    {
      orderType: OrderType.LIMIT,
      marketType: MarketType.PERP,
      marketIndex: 0,
      direction: PositionDirection.LONG,
      baseAssetAmount: velocityClient.convertToPerpPrecision(1),
      price: velocityClient.convertToPricePrecision(21.23),
    },
  ]
);

marketType is required on every order object passed to cancelAndPlaceOrders. It routes through the generic order-placement path, which throws must set param.marketType when the field is missing. placePerpOrder fills it in; this one does not.

Modify Orders

await velocityClient.modifyOrder({
  orderId: 1,
  newBaseAmount: velocityClient.convertToPerpPrecision(2),
});

await velocityClient.modifyOrderByUserOrderId({
  userOrderId: 1,
  newBaseAmount: velocityClient.convertToPerpPrecision(2),
});

Trigger Orders (Stop / Take-Profit)

import { OrderTriggerCondition, OrderType, PositionDirection } from "@velocity-exchange/sdk";

const orderParams = {
  orderType: OrderType.TRIGGER_MARKET,
  marketIndex: 0,
  direction: PositionDirection.SHORT,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1),
  triggerPrice: velocityClient.convertToPricePrecision(95),
  triggerCondition: OrderTriggerCondition.BELOW,
};

await velocityClient.placePerpOrder(orderParams);

The price a trigger is evaluated against

triggerPrice is not compared against the raw oracle price. State.featureBitFlags carries a MEDIAN_TRIGGER_PRICE bit; read it with useMedianTriggerPrice(stateAccount) and derive the price with getTriggerPrice(market, oraclePrice, now, useMedian) rather than comparing against the oracle directly.

With the bit clear, the trigger price is the absolute value of the oracle price. With the bit set, it is the median of three legs:

  • Last fill price: market.lastFillPrice, used only while marketStats.lastTradeTs is within TRIGGER_PRICE_LAST_FILL_MAX_AGE, 5 minutes. With no fill yet, or a stale one, the oracle price stands in for this leg.
  • Funding basis: oracle + basis. The basis is the last funding rate normalized by marketStats.lastFundingOracleTwap, scaled by 24, reduced by FUNDING_RATE_OFFSET_PERCENTAGE, applied to the oracle price, then decayed linearly to zero across one funding period as time passes since lastFundingRateTs. It is zero when lastFundingOracleTwap is zero.
  • 5-minute basis: oracle + (marketStats.lastMarkPriceTwap5Min - marketStats.historicalOracleData.lastOraclePriceTwap5Min).

The median is then clamped to a band around the raw oracle price set by the market's contract tier: 20 bps on tiers A and B, 100 bps on tier C, 250 bps below that. The trigger price therefore never sits further than that from the oracle, whatever the three legs say.

Instruction Builders (Advanced)

Higher-level methods like placePerpOrder() build, sign, and send a transaction in one call. Instruction (IX) builders return the raw TransactionInstruction objects, which makes it possible to:

  • Set a custom compute budget with priority fees for faster inclusion
  • Batch multiple instructions into a single transaction (e.g., cancel + place atomically)
  • Use Address Lookup Tables (ALTs) to fit more accounts into a transaction
  • Compose with other programs (e.g., add a memo or call another protocol in the same tx)

Complete Example: Batching IXs with Compute Budget

import { ComputeBudgetProgram } from "@solana/web3.js";
import {
  MarketType,
  OrderType,
  PositionDirection,
  getOrderParams,
} from "@velocity-exchange/sdk";

// 1. Build individual instructions
const cancelIx = await velocityClient.getCancelOrdersIx(
  MarketType.PERP, // marketType (null to cancel all types)
  0,               // marketIndex (null to cancel across all markets)
  null             // direction (null to cancel both sides)
);

const placeIx = await velocityClient.getPlacePerpOrderIx(
  getOrderParams({
    orderType: OrderType.LIMIT,
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
    price: velocityClient.convertToPricePrecision(21.0),
  })
);

// 2. Add compute budget instructions for priority fees
const computeUnitPrice = ComputeBudgetProgram.setComputeUnitPrice({
  microLamports: 50_000, // priority fee in micro-lamports per CU
});
const computeUnitLimit = ComputeBudgetProgram.setComputeUnitLimit({
  units: 400_000, // max compute units for the transaction
});

// 3. Build a versioned transaction with all instructions
const tx = await velocityClient.txSender.getVersionedTransaction(
  [computeUnitLimit, computeUnitPrice, cancelIx, placeIx],
  []                        // lookup table accounts (AddressLookupTableAccount[])
);

// 4. Send the transaction
const { txSig } = await velocityClient.txSender.sendVersionedTransaction(
  tx,
  [],
  velocityClient.opts
);
console.log("Batch tx:", txSig);

Individual IX Builders

getPlacePerpOrderIx builds an instruction to place a perp order.

// Params:
//   orderParams: OptionalOrderParams - same params as placePerpOrder()
//   subAccountId?: number - defaults to active subaccount
const ix = await velocityClient.getPlacePerpOrderIx(
  getOrderParams({
    orderType: OrderType.LIMIT,
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
    price: velocityClient.convertToPricePrecision(21.0),
  })
);

getCancelOrdersIx builds an instruction to cancel orders matching the given filters. Pass null for any filter to match all.

import { MarketType, PositionDirection } from "@velocity-exchange/sdk";

// Params:
//   marketType: MarketType | null - filter by PERP or SPOT (null = all)
//   marketIndex: number | null - filter by market index (null = all)
//   direction: PositionDirection | null - filter by LONG or SHORT (null = both)
//   subAccountId?: number - defaults to active subaccount

// Cancel all perp orders on market 0
const ix = await velocityClient.getCancelOrdersIx(MarketType.PERP, 0, null);

// Cancel ALL orders across all markets
const ixAll = await velocityClient.getCancelOrdersIx(null, null, null);

getFillPerpOrderIx builds an instruction to fill another user's perp order (used by filler/keeper bots).

// Params:
//   userAccountPublicKey: PublicKey - the taker's user account address
//   userAccount: UserAccount - the taker's deserialized user account
//   order: { marketIndex, orderId } - the order to fill
//   makerInfo?: MakerInfo | MakerInfo[] - optional maker(s) to match against
//   fillerSubAccountId?: number - filler's subaccount
//   isSignedMsg?: boolean
//   fillerAuthority?: PublicKey
//   hasBuilderFee?: boolean
//   takerEscrow?: RevenueShareEscrowAccount - the taker's decoded RevenueShareEscrow
//     (e.g. from a RevenueShareEscrowMap); REQUIRED when the taker's order carries a
//     builder code, otherwise the program rejects the fill with UnableToLoadRevenueShareAccount.
//   takerIsReferred?: boolean - set when the taker is referred (their UserStats.referrerStatus
//     has the BuilderReferral bit). This is the preferred way to satisfy the referred-taker
//     gate: it needs no account fetch. Passing a decoded takerEscrow for this case still
//     works, but is not required.

const takerPubkey = takerUser.userAccountPublicKey;
const takerAccount = takerUser.getUserAccount();
const order = takerAccount.orders[0]; // the order to fill

const ix = await velocityClient.getFillPerpOrderIx(
  takerPubkey,
  takerAccount,
  { marketIndex: order.marketIndex, orderId: order.orderId }
);

getTriggerOrderIx builds an instruction to trigger a conditional order (stop-loss or take-profit) that has met its trigger condition.

// Params:
//   userAccountPublicKey: PublicKey - the user whose order to trigger
//   userAccount: UserAccount - the deserialized user account
//   order: Order - the full order object (must have trigger condition met)
//   fillerPublicKey?: PublicKey - optional, defaults to the caller's user account

const userPubkey = targetUser.userAccountPublicKey;
const userAccount = targetUser.getUserAccount();
const triggerOrder = userAccount.orders.find(
  (o) => o.orderType.triggerMarket !== undefined || o.orderType.triggerLimit !== undefined
);

const ix = await velocityClient.getTriggerOrderIx(
  userPubkey,
  userAccount,
  triggerOrder
);

getRevertFillIx builds an instruction to revert a fill (used by filler bots when a fill was invalid).

// Params:
//   fillerPublicKey?: PublicKey - defaults to the caller's user account
const ix = await velocityClient.getRevertFillIx();

getSettlePNLsIxs builds instructions to settle PnL for one or more users across multiple markets. Returns an array of instructions (one per user per market).

// Params:
//   users: Array of { settleeUserAccountPublicKey, settleeUserAccount }
//   marketIndexes: number[] - perp market indexes to settle

const user = velocityClient.getUser();
const ixs = await velocityClient.getSettlePNLsIxs(
  [
    {
      settleeUserAccountPublicKey: user.userAccountPublicKey,
      settleeUserAccount: user.getUserAccount(),
    },
  ],
  [0, 1] // settle PnL on perp markets 0 and 1
);
// ixs is an array of TransactionInstruction, one per user per market

getProviderSwapIx builds a swap instruction, routed through an external provider (Jupiter or Titan) but settled against the user's Velocity deposits/vault balances rather than their wallet's own token accounts. swap(...) wraps this and sends the transaction.

import { BN } from "@velocity-exchange/sdk";

// Params:
//   swapProvider: SwapProvider - a UnifiedSwapClient, TitanClient, or JupiterClient
//   inMarketIndex: number - spot market index of the input token
//   outMarketIndex: number - spot market index of the output token
//   amount: BN - amount of the input token, in the token's own mint decimals
//     (not a fixed protocol precision)
//   slippageBps: number - max slippage in basis points
//   swapMode?: SwapMode - ExactIn (default) or ExactOut
//   reduceOnly?: SwapReduceOnly - require the position to reduce, not flip sign

const { ixs, lookupTables } = await velocityClient.getProviderSwapIx({
  swapProvider,
  inMarketIndex: 0,   // e.g. quote asset
  outMarketIndex: 1,  // e.g. SOL
  amount: new BN(10_000_000), // amount in the input token's own mint decimals
  slippageBps: 50,    // 0.5% max slippage
});