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
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
| Type | Description |
|---|---|
MARKET | Executes 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. |
LIMIT | Rests 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. |
ORACLE | Like 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_MARKET | A 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_LIMIT | A 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:
| Value | Behavior |
|---|---|
PostOnlyParams.NONE | Order can be maker or taker (default) |
PostOnlyParams.MUST_POST_ONLY | Transaction fails if the order would cross the spread |
PostOnlyParams.TRY_POST_ONLY | Order is silently skipped (not placed) if it would cross; tx succeeds |
PostOnlyParams.SLIDE | Order 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 BNSDK 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.
| Field | Type | Meaning |
|---|---|---|
marketType | MarketType | Use MarketType.PERP. A SPOT ladder is rejected onchain with SpotDlobTradingDisabled. |
direction | PositionDirection | Side of every order in the ladder. |
marketIndex | number | Perp market index. |
totalBaseAssetAmount | BN | Total size to distribute, in BASE_PRECISION (1e9). |
startPrice | BN | First rung, in PRICE_PRECISION (1e6). |
endPrice | BN | Last rung, in PRICE_PRECISION (1e6). |
orderCount | number | Number of orders, minimum 2, maximum 32. |
sizeDistribution | SizeDistribution | FLAT, ASCENDING, or DESCENDING (see below). |
reduceOnly | boolean | Applied to every order in the ladder. |
postOnly | PostOnlyParams | Applied to every order in the ladder. |
bitFlags | number | Applied to the first order only; the rest are placed with bitFlags: 0. |
maxTs | BN | null | Expiry 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.LONGrequiresstartPrice > endPrice(buy the ladder down).PositionDirection.SHORTrequiresstartPrice < 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:
| Value | Sizes |
|---|---|
SizeDistribution.FLAT | Equal size per order (rounded down to the market's orderStepSize), with the leftover added to the last order. |
SizeDistribution.ASCENDING | Smallest order at startPrice, largest at endPrice. Sizes follow 1x, 1.5x, 2x, 2.5x, and so on. |
SizeDistribution.DESCENDING | The 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 whilemarketStats.lastTradeTsis withinTRIGGER_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 bymarketStats.lastFundingOracleTwap, scaled by 24, reduced byFUNDING_RATE_OFFSET_PERCENTAGE, applied to the oracle price, then decayed linearly to zero across one funding period as time passes sincelastFundingRateTs. It is zero whenlastFundingOracleTwapis 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 marketgetProviderSwapIx 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
});