Setup
The examples below use placeholders like <RPC_URL> and <KEYPAIR_PATH>.
Program Addresses
| Network | Program ID |
|---|---|
| Velocity (mainnet & devnet) | vELoC1audYbSYVRXn1vPaV8Axoa9oU6BYmNGZZBDZ1P |
| Velocity Vaults | vAuLTsyrvSfZRuRB3XgvkPwNGgYSs9YRYymVebLKoxR |
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);
// vELoC1audYbSYVRXn1vPaV8Axoa9oU6BYmNGZZBDZ1PVariable VELOCITY_PROGRAM_IDReference ↗
Variable VELOCITY_PROGRAM_IDReference ↗Mainnet-beta velocity program id, base58.
| Property | Type | Required |
|---|---|---|
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 ↗
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 ↗
Class TokenFaucetReference ↗| Property | Type | Required |
|---|---|---|
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.jsonTo 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.jsonThen 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 ↗
Example Wallet / AuthenticationReference ↗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/sdkCreate 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 ↗
Method VelocityClient.subscribeReference ↗| Returns |
|---|
Promise<boolean> |
await velocityClient.unsubscribe();Method VelocityClient.unsubscribeReference ↗
Method VelocityClient.unsubscribeReference ↗| Returns |
|---|
Promise<void> |
Key VelocityClientConfig parameters:
| Parameter | Description | Optional | Default |
|---|---|---|---|
connection | Solana RPC connection | No | |
wallet | Wallet used to sign transactions | No | |
env | devnet or mainnet-beta, used to derive market accounts | Yes | |
perpMarketIndexes | Perp market accounts to subscribe to | Yes | Derived from env |
spotMarketIndexes | Spot market accounts to subscribe to | Yes | Derived from env |
oracleInfos | Oracle accounts to subscribe to | Yes | Derived from env |
accountSubscription | WebSocket or polling subscription mode | Yes | WebSocket |
activeSubAccountId | Which subaccount to use initially | Yes | 0 |
subAccountIds | All subaccount IDs to subscribe to | Yes | [] |
authority | Authority you’re signing for, only set for delegated accounts | Yes | wallet.publicKey |
Delegated accounts: When signing on behalf of a delegated account, you must explicitly set
subAccountIds,activeSubAccountId, andauthority. 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 ↗
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.
| Property | Type | Required |
|---|---|---|
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) => voidUnregisters 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) => stringRegisters 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) => voidUnregisters 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 | undefinedReturns the last-fetched raw buffer/slot for `publicKey`, or undefined if it has never been loaded. | Yes |
getSlot | () => numberReturns the highest slot number observed across any account fetched by this loader so far. | Yes |
startPolling | () => voidStarts the polling interval if not already running and `pollingFrequency !== 0`. Called automatically by `addAccount`. | Yes |
stopPolling | () => voidStops the polling interval, if running. Called automatically by `removeAccount` once no accounts remain. | Yes |
log | (msg: string) => void | Yes |
updatePollingFrequency | (pollingFrequency: number) => voidRestarts 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 ↗
Example Polling subscriptionReference ↗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 ↗
Method VelocityClient.hasUserReference ↗| Parameter | Type | Required |
|---|---|---|
subAccountId | numberSub-account id; defaults to `this.activeSubAccountId`. | No |
authority | PublicKeyAuthority owning the sub-account; defaults to `this.authority`. | No |
| Returns |
|---|
boolean |
await velocityClient.addUser(1);Method VelocityClient.addUserReference ↗
Method VelocityClient.addUserReference ↗| Parameter | Type | Required |
|---|---|---|
subAccountId | numberSub-account id to load. | Yes |
authority | PublicKeyAuthority owning the sub-account; defaults to `this.authority`. | No |
userAccount | UserAccountOptional 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> |