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). There is no Python SDK. A Rust client (velocity-rs) also exists in the monorepo , under rust/velocity-rs.

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.

Both layers are injectable. TxHandler builds and signs (blockhash resolution, compute-budget instructions, lookup tables), and a TxSender broadcasts and confirms. VelocityClient defaults to a RetryTxSender wrapping its own TxHandler. See Transactions for the four sender implementations, their retry and timeout defaults, and the compute-unit and priority-fee parameters.

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], // USDT 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, WrappedEvent } 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) => { // `event.eventType === "OrderActionRecord"` alone doesn't narrow `event`'s type // (WrappedEvent is generic over EventType, so TS can't correlate the check with // the rest of the shape): cast explicitly once you've checked the discriminant. if (event.eventType === "OrderActionRecord") { const orderEvent = event as WrappedEvent<"OrderActionRecord">; if (isVariant(orderEvent.action, "fill")) { console.log("Order filled:", orderEvent); console.log(" Market:", orderEvent.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.

Subscribers and maps reference

Beyond the account-subscription strategies above, the SDK root exports a set of standalone subscribers (live feeds you start with subscribe()) and maps (in-memory caches of many accounts of one kind). Keepers, fillers, and market makers assemble most of their state from these.

ExportWhat it tracksNotes
SlotSubscriberThe current slot, via connection.onSlotChangeIgnores updates that are not strictly greater than the tracked slot. Optional stall-detection resubscribe.
SlothashSubscriberThe newest entry of the SlotHashes sysvar (slot plus base58 blockhash)Fetches once via RPC, then subscribes. Commitment defaults to processed. Needed for signed-message and other slothash-dependent flows.
ClockSubscriberThe on-chain unix timestamp from the Clock sysvarGives you “on-chain now” without an RPC round trip, for evaluating time-based order and auction conditions the way the program would. No initial fetch: currentTs and latestSlot stay undefined until the first account-change notification arrives.
BlockhashSubscriberA short history of recent blockhashesPolls getLatestBlockhashAndContext every 1000ms by default, so transaction building needs no blockhash fetch per transaction. Block height is derived from the same response (lastValidBlockHeight - 150). Takes either a connection or an rpcUrl.
ChainClockLatest known block height, slot, and timestamp per commitment levelUsed by transaction senders to decide whether a blockhash has expired, without an RPC call.
AuctionSubscriberUser accounts that currently have an order inside its auction windowA filtered websocket program-account subscription, so fillers react to auction-eligible orders without scanning every user.
OrderSubscriberEvery User account on the program, and therefore every open orderPolling, websocket, or gRPC depending on subscriptionConfig.type. Backs DLOBSubscriber. Emits orderCreated, userUpdated, and updateReceived.
DLOBSubscriberA continuously rebuilt DLOBBuilt on top of an order source such as OrderSubscriber. See DLOB.
UserMapUser accounts, keyed by user account pubkeyThe general-purpose account cache, described above.
UserStatsMapUserStats accounts, keyed by authority pubkeyOne per trading authority, shared across that authority’s subaccounts. Uses a shared BulkAccountLoader by default.
ReferrerMapAuthority to referrer, plus ReferrerInfo per referrerBuilt by scanning UserStats with memcmp filters on the referrer-status byte instead of subscribing to full accounts. The referrer counterpart of RevenueShareEscrowMap.
RevenueShareEscrowMapRevenueShareEscrow accounts, keyed by authorityThe builder and referral fee-accrual escrow. Fillers read it to attach a taker’s escrow to a fill.
ConstituentMapLP-pool Constituent accountsLook up by pubkey, spot market index, or constituent index.
PythLazerSubscriberPyth Lazer price feeds over websocketTakes endpoints, an auth token, and feed groups; filters out non-stable feeds and handles resubscribe. Feed properties must include price and exponent for getPriceFromMarketIndex to work. Relevant because deployed markets use Pyth Lazer.
PriorityFeeSubscriber / PriorityFeeSubscriberMapRecent priority fees for the accounts or markets you care aboutFeeds the compute-unit price you attach to transactions. See Transactions.
EventSubscriberProgram events (see above)Websocket or polling log provider.

Each subscriber owns its own connection lifecycle. Call subscribe() before reading, and unsubscribe() on shutdown so intervals and websocket handles are released.

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