Skip to Content
DevelopersDrift SDKSDK Internals

SDK Internals

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

While the code examples below use the TypeScript SDK (@drift-labs/sdk), the architectural concepts , subscription patterns, account caching, transaction construction layers, and remaining accounts , apply equally to the Python (driftpy) and Rust SDKs.

Core architecture

The SDK has three main components:

DriftClient - 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 { BulkAccountLoader } from "@drift-labs/sdk"; const accountLoader = new BulkAccountLoader(connection, "confirmed", 1000); const driftClient = new DriftClient({ 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

const driftClient = new DriftClient({ 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

const driftClient = new DriftClient({ 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 { BulkAccountLoader } from "@drift-labs/sdk"; // Constructor: (connection, commitment, pollingFrequencyMs) const bulkAccountLoader = new BulkAccountLoader(connection, "confirmed", 1000); // Typically you don't call addAccount directly. Instead, pass the loader // to DriftClient and it registers the accounts it needs automatically. const driftClient = new DriftClient({ connection, wallet, accountSubscription: { type: "polling", accountLoader: bulkAccountLoader }, });
Example BulkAccountLoaderReference ↗
PropertyTypeRequired
connection
Connection
Yes
commitment
Commitment
Yes
pollingFrequency
number
Yes
accountsToLoad
Map<string, AccountToLoad>
Yes
bufferAndSlotMap
Map<string, BufferAndSlot>
Yes
errorCallbacks
Map<string, (e: any) => void>
Yes
intervalId
Timeout
No
loadPromise
Promise<void>
No
loadPromiseResolver
() => void
Yes
lastTimeLoadingPromiseCleared
number
Yes
mostRecentSlot
number
Yes
addAccount
(publicKey: PublicKey, callback: (buffer: Buffer, slot: number) => void) => Promise<string>
Yes
removeAccount
(publicKey: PublicKey, callbackId: string) => void
Yes
addErrorCallbacks
(callback: (error: Error) => void) => string
Yes
removeErrorCallbacks
(callbackId: string) => void
Yes
chunks
<T>(array: readonly T[], size: number) => T[][]
Yes
load
() => Promise<void>
Yes
loadChunk
(accountsToLoadChunks: AccountToLoad[][]) => Promise<void>
Yes
handleAccountCallbacks
(accountToLoad: AccountToLoad, buffer: Buffer, slot: number) => void
Yes
getBufferAndSlot
(publicKey: PublicKey) => BufferAndSlot | undefined
Yes
getSlot
() => number
Yes
startPolling
() => void
Yes
stopPolling
() => void
Yes
log
(msg: string) => void
Yes
updatePollingFrequency
(pollingFrequency: number) => void
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 driftClient.getPlacePerpOrderIx(orderParams); // 2. Build transaction const tx = await driftClient.txSender.getVersionedTransaction( [ix], [], // lookup tables wallet.publicKey ); // 3. Send transaction const { txSig } = await driftClient.txSender.sendVersionedTransaction( tx, [], driftClient.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 Drift instructions need dynamic account lists (oracles, markets, positions). The SDK’s getRemainingAccounts() method builds this list:

const remainingAccounts = driftClient.getRemainingAccounts({ userAccounts: [user.getUserAccount()], writableSpotMarketIndexes: [0], // USDC market }); // These accounts get passed to the instruction const ix = await driftClient.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:

import { EventSubscriber, isVariant } from "@drift-labs/sdk"; const eventSubscriber = new EventSubscriber(connection, driftClient.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 = driftClient.getPerpMarketAccount(0); // Subsequent calls: same cached data, updates via subscription const market2 = driftClient.getPerpMarketAccount(0); // Oracle price caching: const oracle = driftClient.getOracleDataForPerpMarket(0); // Price is cached from account subscription // User account caching: const user = driftClient.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 "@drift-labs/sdk"; const userMap = new UserMap({ driftClient, subscriptionConfig: { type: "websocket", }, }); // Subscribe to a specific user await userMap.addUserAccount(userAccountPubkey); // Get user data const user = userMap.get(userAccountPubkey.toString()); const position = user.getPerpPosition(0);
Example UserMapReference ↗
PropertyTypeRequired
userMap
any
Yes
driftClient
DriftClient
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
Yes
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>
Yes
addPubkey
(userAccountPublicKey: PublicKey, userAccount?: UserAccount | undefined, slot?: number | undefined, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<...>
Yes
has
(key: string) => boolean
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
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>>
Yes
mustGetUserAccount
(key: string) => Promise<UserAccount>
Yes
getUserAuthority
(key: string) => PublicKey | undefined
gets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getDLOB
(slot: number, protectedMakerParamsMap?: ProtectMakerParamsMap | undefined) => Promise<DLOB>
implements the DLOBSource interface create a DLOB from all the subscribed users
Yes
updateWithOrderRecord
(record: OrderRecord) => Promise<void>
Yes
updateWithEventRecord
(record: any) => Promise<void>
Yes
values
() => IterableIterator<User>
Yes
valuesWithSlot
() => IterableIterator<DataAndSlot<User>>
Yes
entries
() => IterableIterator<[string, User]>
Yes
entriesWithSlot
() => IterableIterator<[string, DataAndSlot<User>]>
Yes
size
() => number
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>
Yes
getFilters
any
Yes
defaultSync
any
Syncs the UserMap using the default sync method (single getProgramAccounts call with filters). This method may fail when drift 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 drift has many users.
Yes
unsubscribe
() => Promise<void>
Yes
updateUserAccount
(key: string, userAccount: UserAccount, slot: number) => Promise<void>
Yes
updateLatestSlot
(slot: number) => void
Yes
getSlot
() => number
Yes

UserMap handles subscription lifecycle for all users automatically.

Common patterns

import { ComputeBudgetProgram } from "@solana/web3.js"; // Initialize and subscribe const driftClient = new DriftClient({ connection, wallet, env: "mainnet-beta", }); await driftClient.subscribe(); const user = driftClient.getUser(); await user.subscribe(); // Transaction with priority fee const ix = await driftClient.getPlacePerpOrderIx(orderParams); const tx = await driftClient.txSender.getVersionedTransaction([ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50000 }), ix, ], [], wallet.publicKey); const { txSig } = await driftClient.txSender.sendVersionedTransaction( tx, [], driftClient.opts ); // Switch subaccounts await driftClient.switchActiveUser(1); // Switch to subaccount 1 const user1 = driftClient.getUser(); // Now returns subaccount 1
Example Common patternsReference ↗
TypeScript docs unavailable for Common patterns.

Error handling

Common error scenarios:

// Insufficient collateral try { await driftClient.placePerpOrder(params); } catch (e) { if (e.message.includes("InsufficientCollateral")) { console.log("Need to deposit more collateral"); } } // Account subscription errors driftClient.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
// Batch operations: place multiple orders in one transaction await driftClient.placeOrders([order1, order2, order3]); // Precompute values: convert once, reuse const size = driftClient.convertToPerpPrecision(1); const price = driftClient.convertToPricePrecision(100); // Use lookup tables (ALTs) to reduce transaction size const lookupTables = [/* your AddressLookupTableAccount objects */]; const tx = await driftClient.txSender.getVersionedTransaction( instructions, lookupTables, wallet.publicKey );
Example Performance tipsReference ↗
TypeScript docs unavailable for Performance tips.
Last updated on