Velocity ProtocolDevelopers
Ecosystem Builders

Sending Actions

This page covers every way to send transactions from an application: the high-level VelocityClient methods, and the stateless VelocityCore instruction builders for full control without a subscribed client.

VelocityClient methods

Once subscribed (see setup), VelocityClient exposes high-level methods for the operations app builders use most. Each method handles transaction construction, compute budget, and sending.

Place a perp order

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

await velocityClient.placePerpOrder(
  getMarketOrderParams({
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1), // 1 SOL
  })
);

// Or a limit order:
await velocityClient.placePerpOrder(
  getLimitOrderParams({
    marketIndex: 0,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(1),
    price: velocityClient.convertToPricePrecision(120),
  })
);

Cancel orders

// null args = cancel all orders (any market, any direction)
await velocityClient.cancelOrders(null, null, null);

Settle PnL

const user = velocityClient.getUser();
await velocityClient.settlePNL(user.userAccountPublicKey, user.getUserAccount(), 0);

Deposit and withdraw

await velocityClient.deposit(
  depositAmount,  // BN, spot market's mint precision (e.g. 1e6 for USDT/dUSDT)
  0,              // spotMarketIndex
  userTokenAccount
);

await velocityClient.withdraw(
  withdrawAmount,
  0,
  userTokenAccount
);

VelocityCore: stateless instruction builders

For advanced use cases (indexers, keeper bots without a full subscribed client, or any context where only the instructions are needed), VelocityCore provides pure instruction builders that take explicit account pubkeys instead of reading from a subscription cache.

ApproachUse when
VelocityClient methods (above)A subscribed client already exists: the shortest path for most apps
VelocityCore.build*Instruction (below)A full subscription is unwanted, several instructions have to go in one transaction, or the integration is lightweight or serverless

Get an Anchor Program<Velocity>

VelocityCore builders take an Anchor program: VelocityProgram argument rather than reading it from a client instance:

import { VelocityCore } from "@velocity-exchange/sdk";
import { Program, AnchorProvider } from "@coral-xyz/anchor";

const provider = new AnchorProvider(connection, wallet, {});
const program = new Program(VelocityCore.defaultIdl(), provider);

Derive the required accounts

const state = await VelocityCore.pdas.getVelocityStateAccountPublicKey(program.programId);
const userPda = VelocityCore.pdas.getUserAccountPublicKeySync(program.programId, authority, 0);
const userStatsPda = VelocityCore.pdas.getUserStatsAccountPublicKey(program.programId, authority);
const userAccount = await VelocityCore.fetchUserAccount(connection, userPda);

// getRemainingAccounts(ctx, params): ctx supplies resolver hooks + caller-owned caches,
// params describes which markets/positions this call needs represented.
const remainingAccounts = VelocityCore.remainingAccounts.getRemainingAccounts(
  {
    getPerpMarketAccount: (marketIndex) => perpMarketAccountsByIndex[marketIndex],
    getSpotMarketAccount: (marketIndex) => spotMarketAccountsByIndex[marketIndex],
    getUserAccountAndSlot: () => undefined, // no subscription cache in the stateless path
    activeSubAccountId: 0,
    authority,
    perpMarketLastSlotCache: new Map(),
    spotMarketLastSlotCache: new Map(),
    mustIncludePerpMarketIndexes: new Set(),
    mustIncludeSpotMarketIndexes: new Set(),
  },
  {
    userAccounts: userAccount ? [userAccount] : [],
    writablePerpMarketIndexes: [0],
  }
);

getRemainingAccounts takes two arguments (a RemainingAccountsContext of caller-owned resolver hooks and caches, and a RemainingAccountParams describing the markets/positions for this call), not a single combined object. It derives the same oracle/market account set VelocityClient's internal builder does, without needing a live subscription.

Build a deposit instruction

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

const depositIx = await VelocityCore.buildDepositInstruction({
  program,
  marketIndex: 0,
  amount: depositAmountBN,
  reduceOnly: false,
  state,
  spotMarket,
  spotMarketVault,
  user: userPda,
  userStats: userStatsPda,
  userTokenAccount,
  authority: wallet.publicKey,
  tokenProgram: TOKEN_PROGRAM_ID,
  remainingAccounts,
});

Build a place-perp-order instruction

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

const orderParams = getMarketOrderParams({
  marketIndex: 0,
  direction: PositionDirection.LONG,
  baseAssetAmount: new BN(1_000_000_000), // 1 SOL, BASE_PRECISION (1e9)
});

const placeOrderIx = await VelocityCore.buildPlacePerpOrderInstruction({
  program,
  orderParams,
  state,
  user: userPda,
  userStats: userStatsPda,
  authority: wallet.publicKey,
  remainingAccounts,
});

Compose and send the transaction

VelocityCore builders return raw TransactionInstructions, so sending them is the caller's job: Anchor's sendAndConfirm, @solana/web3.js directly, or a separately constructed VelocityClient.txSender.

import { ComputeBudgetProgram, VersionedTransaction, TransactionMessage } from "@solana/web3.js";

const { blockhash } = await connection.getLatestBlockhash();
const message = new TransactionMessage({
  payerKey: wallet.publicKey,
  recentBlockhash: blockhash,
  instructions: [
    ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
    placeOrderIx,
  ],
}).compileToV0Message();

const tx = new VersionedTransaction(message);
tx.sign([wallet.payer]);
const signature = await connection.sendTransaction(tx);
console.log("Transaction:", signature);

Atomic cancel-and-place (order replacement) via VelocityClient

A common pattern for trading UIs is atomically canceling existing orders and placing new ones in a single transaction, closing the window with no resting orders while prices are updated. With a subscribed client, build the individual instructions and compose them:

import { ComputeBudgetProgram } from "@solana/web3.js";
import { getLimitOrderParams, PositionDirection, PostOnlyParams } from "@velocity-exchange/sdk";

const cancelIx = await velocityClient.getCancelOrdersIx(
  null,   // marketType (null = all)
  null,   // marketIndex (null = all)
  null    // direction (null = both)
);

const bidParams = getLimitOrderParams({
  marketIndex: 0,
  direction: PositionDirection.LONG,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1),
  price: velocityClient.convertToPricePrecision(120),
  postOnly: PostOnlyParams.MUST_POST_ONLY,
});

const askParams = getLimitOrderParams({
  marketIndex: 0,
  direction: PositionDirection.SHORT,
  baseAssetAmount: velocityClient.convertToPerpPrecision(1),
  price: velocityClient.convertToPricePrecision(125),
  postOnly: PostOnlyParams.MUST_POST_ONLY,
});

const bidIx = await velocityClient.getPlacePerpOrderIx(bidParams);
const askIx = await velocityClient.getPlacePerpOrderIx(askParams);

const tx = await velocityClient.txSender.getVersionedTransaction(
  [
    ComputeBudgetProgram.setComputeUnitLimit({ units: 600000 }),
    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50000 }),
    cancelIx,
    bidIx,
    askIx,
  ],
  []
);

const signature = await velocityClient.txSender.sendVersionedTransaction(
  tx,
  [],
  velocityClient.opts
);
console.log("Cancel + place transaction:", signature);

Transaction performance

For batched or frequent transaction sending:

  • Priority fees: Set ComputeUnitPrice to land transactions faster during congestion
  • Compute budget: Use setComputeUnitLimit to allocate enough CU for multi-instruction transactions
  • Address Lookup Tables (ALTs): Reduce transaction size when referencing many accounts
  • Retry logic: Transactions can be dropped; implement retries with backoff for production systems
  • Bulk order margin enforcement: placeOrders and placeScaleOrders run the initial-margin check once per risk scope touched by the batch rather than as a single end-of-batch check. A batch that previously succeeded may now revert with InsufficientCollateral.