Skip to Content

SDK Internals

The Velocity SDK handles onchain interactions, account subscriptions, and transaction construction. Understanding its internals helps you optimize performance, debug issues, and use advanced features.

The code examples below use the TypeScript SDK (@velocity-exchange/sdk), the only SDK Velocity ships. There is no Python SDK. A Rust client (velocity-rs) exists in the monorepo  but is source-only and unpublished — see SDK Setup for details.

Core architecture

The SDK has three main components:

VelocityClient - Main interface for protocol interactions

  • Constructs transactions
  • Manages account subscriptions
  • Provides helper methods for orders, deposits, etc.
  • Caches market and state data

User - Represents a single user account

  • Subscribes to user account updates
  • Calculates positions, PnL, health
  • Provides convenience methods for account queries

AccountSubscriber - Handles real-time account updates

  • Polls or streams account data from RPC
  • Notifies clients when data changes
  • Caches account data for fast access

Account subscription patterns

The SDK supports multiple subscription strategies, each with different performance characteristics:

Polling subscription

How it works: Periodically calls connection.getAccountInfo() for each account

import { VelocityClient, BulkAccountLoader } from "@velocity-exchange/sdk"; const accountLoader = new BulkAccountLoader(connection, "confirmed", 1000); const velocityClient = new VelocityClient({ connection, wallet, accountSubscription: { type: "polling", accountLoader, }, });
Example Polling subscriptionReference ↗
TypeScript docs unavailable for Polling subscription.

Pros:

  • Simple and reliable
  • Works with any RPC endpoint
  • Predictable resource usage

Cons:

  • Higher latency (poll interval delay)
  • More RPC calls
  • Not real-time

Best for: Development, low-frequency trading, simple bots

WebSocket subscription

How it works: Uses Solana’s onAccountChange WebSocket notifications

import { VelocityClient } from "@velocity-exchange/sdk"; const velocityClient = new VelocityClient({ connection, wallet, accountSubscription: { type: "websocket", }, });
Example WebSocket subscriptionReference ↗
TypeScript docs unavailable for WebSocket subscription.

Pros:

  • Lower latency than polling
  • Fewer RPC calls
  • Real-time updates

Cons:

  • WebSocket can disconnect (needs reconnection handling)
  • Some RPC endpoints have connection limits
  • Slightly more complex error handling

Best for: Market makers, latency-sensitive bots, production trading

gRPC subscription (fastest)

How it works: Uses Yellowstone gRPC plugin for Solana validators

import { VelocityClient } from "@velocity-exchange/sdk"; const velocityClient = new VelocityClient({ connection, wallet, accountSubscription: { type: "grpc", grpcConfigs: { endpoint: "https://grpc.mainnet.jito.wtf", token: "YOUR_GRPC_TOKEN", }, }, });
Example gRPC subscriptionReference ↗
TypeScript docs unavailable for gRPC subscription.

Pros:

  • Lowest latency (sub-second updates)
  • Most efficient bandwidth usage
  • Best for high-frequency trading

Cons:

  • Requires gRPC-enabled RPC (e.g., Jito, Triton)
  • More complex setup
  • May require authentication/payment

Best for: HFT bots, JIT market makers, competitive filling

BulkAccountLoader

For loading many accounts efficiently, the SDK provides BulkAccountLoader:

import { VelocityClient, BulkAccountLoader } from "@velocity-exchange/sdk"; // Constructor: (connection, commitment, pollingFrequencyMs) const bulkAccountLoader = new BulkAccountLoader(connection, "confirmed", 1000); // Typically you don't call addAccount directly. Instead, pass the loader // to VelocityClient and it registers the accounts it needs automatically. const velocityClient = new VelocityClient({ connection, wallet, accountSubscription: { type: "polling", accountLoader: bulkAccountLoader }, });
Example BulkAccountLoaderReference ↗

Batches many accounts behind a single periodic `getMultipleAccounts` RPC poll instead of one WebSocket subscription per account. Multiple independent callbacks (e.g. from different `PollingUserAccountSubscriber`/`PollingVelocityClientAccountSubscriber` instances) can register against the same `publicKey`; polling starts automatically on the first `addAccount` and stops when the last callback for the last account is removed. A buffer is only re-delivered to callbacks when its slot is not older than the last-seen slot for that account and its bytes actually changed, so a callback never observes a stale or duplicate update. `load()` calls are coalesced: concurrent callers share one in-flight RPC batch rather than issuing redundant requests.

PropertyTypeRequired
connection
Connection
Yes
commitment
Commitment
Yes
pollingFrequency
number
Yes
accountsToLoad
Map<string, AccountToLoad>
Yes
bufferAndSlotMap
Map<string, BufferAndSlot>
Yes
errorCallbacks
Map<string, (e: Error) => void>
Yes
intervalId
Timeout
No
loadPromise
Promise<void>
No
loadPromiseResolver
any
Yes
lastTimeLoadingPromiseCleared
number
Yes
mostRecentSlot
number
Yes
addAccount
(publicKey: PublicKey, callback: (buffer: Buffer, slot: number) => void) => Promise<string>
Registers a callback to be invoked whenever `publicKey`'s account data changes on a poll. Multiple callbacks may be registered for the same account. Starts polling automatically if this is the loader's first account. Awaits any in-flight `load()` before returning so a caller can immediately follow with its own `load()` without racing the poll interval.
Yes
removeAccount
(publicKey: PublicKey, callbackId: string | undefined) => void
Unregisters a callback previously returned by `addAccount`. Once an account has no remaining callbacks, its cached buffer/slot is dropped and it stops being polled; if no accounts remain at all, polling stops entirely. A no-op if `callbackId` is undefined.
Yes
addErrorCallbacks
(callback: (error: Error) => void) => string
Registers a callback invoked whenever a `load()` batch throws (e.g. RPC failure or timeout). The loader continues polling afterward; errors do not stop the interval.
Yes
removeErrorCallbacks
(callbackId: string | undefined) => void
Unregisters an error callback previously returned by `addErrorCallbacks`. A no-op if `callbackId` is undefined.
Yes
chunks
<T>(array: readonly T[], size: number) => T[][]
Yes
load
() => Promise<void>
Fetches every registered account in one or more chunked, concurrent `getMultipleAccounts` batches (via `loadChunk`) and dispatches changed accounts to their callbacks. Concurrent calls while a load is already in flight share that same in-flight promise rather than issuing a duplicate RPC batch, unless the previous load has been running for over a minute (treated as stuck and restarted). On failure, invokes every registered error callback instead of throwing.
Yes
loadChunk
(accountsToLoadChunks: AccountToLoad[][]) => Promise<void>
Yes
handleAccountCallbacks
(accountToLoad: AccountToLoad, buffer: Buffer | undefined, slot: number) => void
Yes
getBufferAndSlot
(publicKey: PublicKey) => BufferAndSlot | undefined
Returns the last-fetched raw buffer/slot for `publicKey`, or undefined if it has never been loaded.
Yes
getSlot
() => number
Returns the highest slot number observed across any account fetched by this loader so far.
Yes
startPolling
() => void
Starts the polling interval if not already running and `pollingFrequency !== 0`. Called automatically by `addAccount`.
Yes
stopPolling
() => void
Stops the polling interval, if running. Called automatically by `removeAccount` once no accounts remain.
Yes
log
(msg: string) => void
Yes
updatePollingFrequency
(pollingFrequency: number) => void
Restarts polling at a new interval (ms), preserving all registered accounts and callbacks.
Yes

The loader batches multiple accounts into single getMultipleAccounts RPC calls for efficiency.

Transaction construction

The SDK builds transactions in several layers:

// 1. Get instruction const ix = await velocityClient.getPlacePerpOrderIx(orderParams); // 2. Build transaction // getVersionedTransaction(ixs, lookupTableAccounts, additionalSigners?, opts?, blockhash?) // — the fee payer/signer is `velocityClient`'s own wallet, not a positional arg. const tx = await velocityClient.txSender.getVersionedTransaction( [ix], [], // lookup tables ); // 3. Send transaction const { txSig } = await velocityClient.txSender.sendVersionedTransaction( tx, [], velocityClient.opts );
Example Transaction construction layersReference ↗
TypeScript docs unavailable for Transaction construction layers.

Higher-level methods like placePerpOrder() do all three steps automatically.

Remaining accounts pattern

Many Velocity instructions need dynamic account lists (oracles, markets, positions). The SDK’s getRemainingAccounts() method builds this list:

const remainingAccounts = velocityClient.getRemainingAccounts({ userAccounts: [user.getUserAccount()], writableSpotMarketIndexes: [0], // USDC market }); // These accounts get passed to the instruction const ix = await velocityClient.program.methods .placePerpOrder(params) .accounts({ user: userAccountPubkey, // ... other fixed accounts }) .remainingAccounts(remainingAccounts) .instruction();
Example Remaining accountsReference ↗
TypeScript docs unavailable for Remaining accounts.

This handles oracle accounts, market accounts, and cross-position accounts automatically.

Event subscriptions

The SDK can subscribe to onchain program events. See Events for the full event catalog (including the fee-sweep/revenue-share/LP-borrow-lend records) and query helpers:

import { EventSubscriber, isVariant } from "@velocity-exchange/sdk"; const eventSubscriber = new EventSubscriber(connection, velocityClient.program, { commitment: "confirmed", logProviderConfig: { type: "websocket" }, }); await eventSubscriber.subscribe(); // All events come through "newEvent", filter by eventType eventSubscriber.eventEmitter.on("newEvent", (event) => { if (event.eventType === "OrderActionRecord" && isVariant(event.action, "fill")) { console.log("Order filled:", event); console.log(" Market:", event.marketIndex); } });
Example Event subscriptionsReference ↗
TypeScript docs unavailable for Event subscriptions.

Common event types:

  • OrderActionRecord (with action: fill, place, cancel, etc.) - Order lifecycle events
  • DepositRecord - Deposits and withdrawals
  • FundingPaymentRecord - Funding payments
  • LiquidationRecord - Liquidations

Caching and performance

// Market account caching: // First call after subscribe: data from subscription cache (no extra RPC) const market = velocityClient.getPerpMarketAccount(0); // Subsequent calls: same cached data, updates via subscription const market2 = velocityClient.getPerpMarketAccount(0); // Oracle price caching: const oracle = velocityClient.getOracleDataForPerpMarket(0); // Price is cached from account subscription // User account caching: const user = velocityClient.getUser(); const position = user.getPerpPosition(0); // No RPC call, data from subscription
Example Caching examplesReference ↗
TypeScript docs unavailable for Caching examples.

This makes queries fast (microseconds vs milliseconds for RPC).

UserMap for multiple users

To track many user accounts efficiently:

import { UserMap } from "@velocity-exchange/sdk"; const userMap = new UserMap({ velocityClient, subscriptionConfig: { type: "websocket", }, }); await userMap.subscribe(); // Add a specific user account to the map await userMap.addPubkey(userAccountPubkey); // Get user data const user = userMap.get(userAccountPubkey.toString()); const position = user.getPerpPosition(0);
Example UserMapReference ↗

In-memory cache of every `User` account on the program, keyed by the `User` account's own public key.

Sync/subscription filtering (see `getFilters`) is built from memcmp filters: the base `getUserFilter()` (matches the `User` account discriminator — required, or every account on chain would match) is always present; `getNonIdleUserFilter()` is added unless `includeIdle` is set (idle accounts are excluded by default to reduce subscription volume); `getUsersWithPoolId(filterByPoolId)` is added when `filterByPoolId` is set; and any `additionalFilters` from the config are appended last. The same filter set is used for the initial `getProgramAccounts` sync and for the live websocket/gRPC subscription, so what you sync is what you keep getting updates for.

Automatically does a full `sync()` whenever the program's `StateAccount.numberOfSubAccounts` changes (new/deleted user accounts), unless `disableSyncOnTotalAccountsChange` is set.

PropertyTypeRequired
userMap
any
Yes
velocityClient
VelocityClient
Yes
eventEmitter
StrictEventEmitter<EventEmitter, UserEvents>
Yes
connection
any
Yes
commitment
any
Yes
includeIdle
any
Yes
filterByPoolId
any
No
additionalFilters
any
No
disableSyncOnTotalAccountsChange
any
Yes
lastNumberOfSubAccounts
any
No
subscription
any
Yes
stateAccountUpdateCallback
any
Yes
decode
any
Yes
mostRecentSlot
any
Yes
syncConfig
any
Yes
syncPromise
any
No
syncPromiseResolver
any
Yes
throwOnFailedSync
any
Yes
subscribe
() => Promise<void>
Populates the map with a full initial `sync()` (no-op if already populated) and starts the configured live subscription (`'websocket'`/`'polling'`/`'grpc'`), plus (unless `disableSyncOnTotalAccountsChange`) a listener that triggers a full re-sync whenever `StateAccount.numberOfSubAccounts` changes.
Yes
addPubkey
(userAccountPublicKey: PublicKey, userAccount?: UserAccount | undefined, slot?: number | undefined, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<...>
Adds `userAccountPublicKey` to the map, creating a `User` for it. By default (`accountSubscription` omitted), subscribes it with a `OneShotUserAccountSubscriber` seeded from `userAccount`/`slot` rather than a live per-account websocket subscription — the map already gets live updates in bulk via its own subscription (`WebsocketSubscription`/ `PollingSubscription`/`grpcSubscription`), so per-`User` subscriptions here would needlessly multiply RPC load.
Yes
has
(key: string) => boolean
Returns true if a `User` account keyed by `key` (the `User` account pubkey, base58) is cached in the map.
Yes
get
(key: string) => User | undefined
gets the User for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getWithSlot
(key: string) => DataAndSlot<User> | undefined
Like `get`, but also returns the slot at which the `User` account was last observed.
Yes
mustGet
(key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<User>
gets the User for a particular userAccountPublicKey, if no User exists, new one is created
Yes
mustGetWithSlot
(key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<DataAndSlot<User>>
Like `mustGet`, but also returns the slot at which the `User` account was observed.
Yes
mustGetUserAccount
(key: string) => Promise<UserAccount>
Like `mustGet`, but returns the underlying `UserAccount` data directly (throws if the `User`'s account is not loaded).
Yes
getUserAuthority
(key: string) => PublicKey | undefined
gets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getDLOB
(slot: number) => Promise<DLOB>
Implements the `DLOBSource` interface: builds a `DLOB` from every subscribed user's open orders.
Yes
updateWithOrderRecord
(record: OrderRecord) => Promise<void>
Ensures an entry exists in the map for `record.user`, adding it via `addPubkey` if not already present.
Yes
updateWithEventRecord
(record: any) => Promise<void>
Incrementally updates the map in response to a single program event, ensuring an entry exists for every `User` account the event references (deposit/funding/liquidation/order/order-action/settle-pnl/new-user records). Unrecognized event types are silently ignored.
Yes
values
() => IterableIterator<User>
Iterates all cached `User` instances.
Yes
valuesWithSlot
() => IterableIterator<DataAndSlot<User>>
Like `values`, but paired with the slot each `User` was last observed at.
Yes
entries
() => IterableIterator<[string, User]>
Iterates all `[userAccountPublicKey, User]` pairs in the map.
Yes
entriesWithSlot
() => IterableIterator<[string, DataAndSlot<User>]>
Like `entries`, but paired with the slot each `User` was last observed at.
Yes
size
() => number
Number of `User` accounts currently cached in the map.
Yes
getUniqueAuthorities
(filterCriteria?: UserAccountFilterCriteria | undefined) => PublicKey[]
Returns a unique list of authorities for all users in the UserMap that meet the filter criteria
Yes
sync
() => Promise<void>
Runs a full sync using the strategy configured in `UserMapConfig.syncConfig` (`'default'` or `'paginated'` — see `SyncConfig`).
Yes
getFilters
any
Builds the memcmp filter set for both the initial sync and the live subscription: always the `User`-account discriminator filter; plus a non-idle filter unless `includeIdle`; plus a pool-id filter if `filterByPoolId` is set; plus any caller-supplied `additionalFilters`.
Yes
defaultSync
any
Syncs the UserMap using the default sync method (single getProgramAccounts call with filters). This method may fail when velocity has too many users. (nodejs response size limits)
Yes
paginatedSync
any
Syncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters). This method is more reliable when velocity has many users.
Yes
unsubscribe
() => Promise<void>
Tears down the live subscription, unsubscribes and removes every cached `User`, and (if registered) removes the `stateAccountUpdate` listener that triggers auto-resync on `numberOfSubAccounts` changes.
Yes
updateUserAccount
(key: string, userAccount: UserAccount, slot: number) => Promise<void>
Applies a fresh `userAccount` observation for `key` at `slot`. If the user is already cached, updates in place only if `slot` is at least as new as the cached slot (stale/out-of-order updates are dropped) and emits `'userUpdate'`. If not cached yet, adds it via `addPubkey`. Also advances `getSlot()`'s tracked most-recent slot.
Yes
updateLatestSlot
(slot: number) => void
Advances the map's tracked most-recent slot to `slot` if it's newer.
Yes
getSlot
() => number
Returns the most recent slot at which any account update has been observed.
Yes

UserMap handles subscription lifecycle for all users automatically.

Common patterns

import { VelocityClient } from "@velocity-exchange/sdk"; import { ComputeBudgetProgram } from "@solana/web3.js"; // Initialize and subscribe const velocityClient = new VelocityClient({ connection, wallet, env: "mainnet-beta", }); await velocityClient.subscribe(); const user = velocityClient.getUser(); await user.subscribe(); // Transaction with priority fee const ix = await velocityClient.getPlacePerpOrderIx(orderParams); const tx = await velocityClient.txSender.getVersionedTransaction([ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50000 }), ix, ], []); // lookup tables const { txSig } = await velocityClient.txSender.sendVersionedTransaction( tx, [], velocityClient.opts ); // Switch subaccounts await velocityClient.switchActiveUser(1); // Switch to subaccount 1 const user1 = velocityClient.getUser(); // Now returns subaccount 1 // Batch operations: place multiple orders in one transaction await velocityClient.placeOrders([order1, order2, order3]);
Example Common patternsReference ↗
TypeScript docs unavailable for Common patterns.

Error handling

Common error scenarios:

// Insufficient collateral try { await velocityClient.placePerpOrder(params); } catch (e) { if (e.message.includes("InsufficientCollateral")) { console.log("Need to deposit more collateral"); } } // Account subscription errors velocityClient.eventEmitter.on("error", (e) => { console.error("Subscription error:", e); // Reconnect logic here });
Example Error handlingReference ↗
TypeScript docs unavailable for Error handling.

Performance tips

Use appropriate commitment:

  • processed - Fastest, some risk of reorgs
  • confirmed - Balanced (recommended)
  • finalized - Slowest, most secure
// Precompute values: convert once, reuse const size = velocityClient.convertToPerpPrecision(1); const price = velocityClient.convertToPricePrecision(100); // Use lookup tables (ALTs) to reduce transaction size const lookupTables = [/* your AddressLookupTableAccount objects */]; const tx = await velocityClient.txSender.getVersionedTransaction( instructions, lookupTables );
Example Performance tipsReference ↗
TypeScript docs unavailable for Performance tips.
Last updated on