Skip to Content
DevelopersVelocity SDKSetup

Setup

The examples below use placeholders like <RPC_URL> and <KEYPAIR_PATH>.

Program Addresses

NetworkProgram ID
Velocity (mainnet & devnet)vELoC1audYbSYVRXn1vPaV8Axoa9oU6BYmNGZZBDZ1P
Velocity VaultsvAuLTsyrvSfZRuRB3XgvkPwNGgYSs9YRYymVebLKoxR

Velocity uses the same program ID on devnet and mainnet-beta. On-chain state does not carry over from Drift Protocol v2 — Velocity is an entirely new deployment, so user accounts must be re-initialized and balances start fresh.

You can also import the Velocity program ID directly from the SDK:

import { VELOCITY_PROGRAM_ID } from "@velocity-exchange/sdk"; // The Velocity program's public key on mainnet-beta and devnet. // Use this when deriving PDAs or referencing the program directly. console.log(VELOCITY_PROGRAM_ID); // vELoC1audYbSYVRXn1vPaV8Axoa9oU6BYmNGZZBDZ1P
Variable VELOCITY_PROGRAM_IDReference ↗

Mainnet-beta velocity program id, base58.

PropertyTypeRequired
toString
() => string
Yes
charAt
(pos: number) => string
Yes
charCodeAt
(index: number) => number
Yes
concat
(...strings: string[]) => string
Yes
indexOf
(searchString: string, position?: number | undefined) => number
Yes
lastIndexOf
(searchString: string, position?: number | undefined) => number
Yes
localeCompare
{ (that: string): number; (that: string, locales?: string | string[] | undefined, options?: CollatorOptions | undefined): number; (that: string, locales?: LocalesArgument, options?: CollatorOptions | undefined): number; }
Yes
match
{ (regexp: string | RegExp): RegExpMatchArray | null; (matcher: { [Symbol.match](string: string): RegExpMatchArray | null; }): RegExpMatchArray | null; }
Yes
replace
{ (searchValue: string | RegExp, replaceValue: string): string; (searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string; (searchValue: { ...; }, replaceValue: string): string; (searchValue: { ...; }, replacer: (substring: string, ...args: any[]) => string): string; }
Yes
search
{ (regexp: string | RegExp): number; (searcher: { [Symbol.search](string: string): number; }): number; }
Yes
slice
(start?: number | undefined, end?: number | undefined) => string
Yes
split
{ (separator: string | RegExp, limit?: number | undefined): string[]; (splitter: { [Symbol.split](string: string, limit?: number | undefined): string[]; }, limit?: number | undefined): string[]; }
Yes
substring
(start: number, end?: number | undefined) => string
Yes
toLowerCase
() => string
Yes
toLocaleLowerCase
{ (locales?: string | string[] | undefined): string; (locales?: LocalesArgument): string; }
Yes
toUpperCase
() => string
Yes
toLocaleUpperCase
{ (locales?: string | string[] | undefined): string; (locales?: LocalesArgument): string; }
Yes
trim
() => string
Yes
length
number
Yes
substr
(from: number, length?: number | undefined) => string
Yes
valueOf
() => string
Yes
codePointAt
(pos: number) => number | undefined
Yes
includes
(searchString: string, position?: number | undefined) => boolean
Yes
endsWith
(searchString: string, endPosition?: number | undefined) => boolean
Yes
normalize
{ (form: "NFC" | "NFD" | "NFKC" | "NFKD"): string; (form?: string | undefined): string; }
Yes
repeat
(count: number) => string
Yes
startsWith
(searchString: string, position?: number | undefined) => boolean
Yes
anchor
(name: string) => string
Yes
big
() => string
Yes
blink
() => string
Yes
bold
() => string
Yes
fixed
() => string
Yes
fontcolor
(color: string) => string
Yes
fontsize
{ (size: number): string; (size: string): string; }
Yes
italics
() => string
Yes
link
(url: string) => string
Yes
small
() => string
Yes
strike
() => string
Yes
sub
() => string
Yes
sup
() => string
Yes
padStart
(maxLength: number, fillString?: string | undefined) => string
Yes
padEnd
(maxLength: number, fillString?: string | undefined) => string
Yes
trimEnd
() => string
Yes
trimStart
() => string
Yes
trimLeft
() => string
Yes
trimRight
() => string
Yes
matchAll
(regexp: RegExp) => RegExpStringIterator<RegExpExecArray>
Yes
replaceAll
{ (searchValue: string | RegExp, replaceValue: string): string; (searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string; }
Yes
at
(index: number) => string | undefined
Yes
isWellFormed
() => boolean
Yes
toWellFormed
() => string
Yes
__@iterator@90
() => StringIterator<string>
Yes

Quote Mint

The protocol’s quote asset mint address is environment-specific and available via the SDK’s config presets:

import { getConfig, initialize } from "@velocity-exchange/sdk"; initialize({ env: "devnet" }); console.log(getConfig().QUOTE_MINT_ADDRESS); // devnet: GqmEqYsy8EyvofDpmtFxK8zhYrgWgNokAtYoduQdL7v6 (dUSDT, a devnet placeholder) // mainnet-beta: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (USDC, unchanged from Drift)
Function getConfigReference ↗

Returns the SDK's currently active `VelocityConfig` (the `devnet` preset by default, or whatever was last set via `initialize()`).

The active config.

Returns
VelocityConfig

On devnet, spot market index 0 is dUSDT (a placeholder quote token, 1e6 precision) rather than real USDC — mint it from the SDK’s TokenFaucet helper before depositing. On mainnet-beta the quote asset is still USDC at its original mint address.

import { TokenFaucet, BN } from "@velocity-exchange/sdk"; import { PublicKey } from "@solana/web3.js"; // <FAUCET_PROGRAM_ID> is the devnet token-faucet program's own program ID // (a separate deployment from the Velocity program), not documented here as a // fixed constant — obtain it from your devnet environment/deploy config. const tokenFaucet = new TokenFaucet( connection, wallet, new PublicKey("<FAUCET_PROGRAM_ID>"), new PublicKey("GqmEqYsy8EyvofDpmtFxK8zhYrgWgNokAtYoduQdL7v6") // dUSDT mint (devnet) ); const [associatedTokenAccount] = await tokenFaucet.createAssociatedTokenAccountAndMintTo( wallet.publicKey, new BN(1_000_000_000) // 1,000 dUSDT at 1e6 precision );
Class TokenFaucetReference ↗
PropertyTypeRequired
context
BankrunContextWrapper
No
connection
Connection
Yes
wallet
IWallet
Yes
program
Program<Idl>
Yes
provider
AnchorProvider
Yes
mint
PublicKey
Yes
opts
ConfirmOptions
No
getFaucetConfigPublicKeyAndNonce
() => Promise<[PublicKey, number]>
Yes
getMintAuthority
() => Promise<PublicKey>
Yes
getFaucetConfigPublicKey
() => Promise<PublicKey>
Yes
initialize
() => Promise<string>
Yes
fetchState
() => Promise<any>
Yes
mintToUserIx
any
Yes
mintToUser
(userTokenAccount: PublicKey, amount: BN) => Promise<string>
Yes
transferMintAuthority
() => Promise<string>
Yes
createAssociatedTokenAccountAndMintTo
(userPublicKey: PublicKey, amount: BN) => Promise<[PublicKey, string]>
Yes
createAssociatedTokenAccountAndMintToInstructions
(userPublicKey: PublicKey, amount: BN) => Promise<[PublicKey, TransactionInstruction, TransactionInstruction]>
Yes
getAssosciatedMockUSDMintAddress
(props: { userPubKey: PublicKey; }) => Promise<PublicKey>
Yes
getTokenAccountInfo
(props: { userPubKey: PublicKey; }) => Promise<Account>
Yes
subscribeToTokenAccount
(props: { userPubKey: PublicKey; callback: (accountInfo: Account) => void; }) => Promise<boolean>
Yes

Wallet / Authentication

To interact with Solana you need a keypair, which consists of a public key and a private key. The private key is used to sign transactions and should be kept secure.

Generate a new keypair using the Solana CLI :

solana-keygen new --outfile ~/.config/solana/my-keypair.json

To allow SDK code to use this keypair, set the ANCHOR_WALLET environment variable to the path of the keypair file:

export ANCHOR_WALLET=~/.config/solana/my-keypair.json

Then load the keypair in your code:

import { Wallet, loadKeypair } from "@velocity-exchange/sdk"; const keyPairFile = `${process.env.HOME}/.config/solana/my-keypair.json`; const wallet = new Wallet(loadKeypair(keyPairFile));
Example Wallet / AuthenticationReference ↗
TypeScript docs unavailable for Wallet / Authentication.

Make sure the wallet has some SOL, as it is used to pay for transaction fees and rent for account initializations.

Install

npm i @velocity-exchange/sdk

Create a Velocity Client

At a minimum you provide a Solana connection, a wallet, and the env. Then call subscribe() to start receiving account updates.

import { Connection } from "@solana/web3.js"; import { VelocityClient, Wallet, loadKeypair } from "@velocity-exchange/sdk"; const connection = new Connection("<RPC_URL>", "confirmed"); const wallet = new Wallet(loadKeypair("<KEYPAIR_PATH>")); const velocityClient = new VelocityClient({ connection, wallet, env: "mainnet-beta", }); await velocityClient.subscribe();
await velocityClient.subscribe();
Method VelocityClient.subscribeReference ↗
Returns
Promise<boolean>
await velocityClient.unsubscribe();
Method VelocityClient.unsubscribeReference ↗
Returns
Promise<void>

Key VelocityClientConfig parameters:

ParameterDescriptionOptionalDefault
connectionSolana RPC connectionNo
walletWallet used to sign transactionsNo
envdevnet or mainnet-beta, used to derive market accountsYes
perpMarketIndexesPerp market accounts to subscribe toYesDerived from env
spotMarketIndexesSpot market accounts to subscribe toYesDerived from env
oracleInfosOracle accounts to subscribe toYesDerived from env
accountSubscriptionWebSocket or polling subscription modeYesWebSocket
activeSubAccountIdWhich subaccount to use initiallyYes0
subAccountIdsAll subaccount IDs to subscribe toYes[]
authorityAuthority you’re signing for, only set for delegated accountsYeswallet.publicKey

Delegated accounts: When signing on behalf of a delegated account, you must explicitly set subAccountIds, activeSubAccountId, and authority. Omitting any of these will cause the client to subscribe to the wrong accounts.

Account Subscriptions (WebSocket vs Polling)

For most bots, websocket subscriptions are the easiest way to keep markets and users up to date. For read-only workflows or when you need tighter control over RPC load, you can switch to polling with a BulkAccountLoader.

import { BulkAccountLoader } from "@velocity-exchange/sdk";
Class 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
import { BulkAccountLoader } from "@velocity-exchange/sdk"; const accountLoader = new BulkAccountLoader(connection, "confirmed", 0); const velocityClient = new VelocityClient({ connection, wallet, env: "mainnet-beta", accountSubscription: { type: "polling", accountLoader, }, // Optional: explicitly list markets/oracles to load. // perpMarketIndexes: [0, 1], // spotMarketIndexes: [0], // oracleInfos: [{ publicKey: ORACLE_PUBKEY, source: ORACLE_SOURCE }], });
Example Polling subscriptionReference ↗
TypeScript docs unavailable for Polling subscription.

Multiple Subaccounts

Velocity supports multiple subaccounts per wallet, each with its own isolated position and order state. This lets you run separate strategies (e.g., a market-making bot and a hedging bot) under the same authority without cross-contaminating risk or PnL. Use addUser() to subscribe to additional subaccounts after initialization.

if (!velocityClient.hasUser(1)) { await velocityClient.addUser(1); }
Method VelocityClient.hasUserReference ↗
ParameterTypeRequired
subAccountId
number
Sub-account id; defaults to `this.activeSubAccountId`.
No
authority
PublicKey
Authority owning the sub-account; defaults to `this.authority`.
No
Returns
boolean
await velocityClient.addUser(1);
Method VelocityClient.addUserReference ↗
ParameterTypeRequired
subAccountId
number
Sub-account id to load.
Yes
authority
PublicKey
Authority owning the sub-account; defaults to `this.authority`.
No
userAccount
UserAccount
Optional pre-fetched `UserAccount` data to seed the subscription with, avoiding an extra RPC round-trip (e.g. when the caller already has it from a prior fetch).
No
Returns
Promise<boolean>
Last updated on