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 ↗
Example Polling subscriptionReference ↗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 ↗
Example WebSocket subscriptionReference ↗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 ↗
Example gRPC subscriptionReference ↗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 ↗
Example BulkAccountLoaderReference ↗| 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: 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 ↗
Example Transaction construction layersReference ↗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 ↗
Example Remaining accountsReference ↗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 ↗
Example Event subscriptionsReference ↗Event subscriptions.Common event types:
OrderActionRecord(with action:fill,place,cancel, etc.) - Order lifecycle eventsDepositRecord- Deposits and withdrawalsFundingPaymentRecord- Funding paymentsLiquidationRecord- 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 subscriptionExample Caching examplesReference ↗
Example Caching examplesReference ↗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 ↗
Example UserMapReference ↗| Property | Type | Required |
|---|---|---|
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 | undefinedgets 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 | undefinedgets 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 | anySyncs 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 | anySyncs 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 1Example Common patternsReference ↗
Example Common patternsReference ↗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 ↗
Example Error handlingReference ↗Error handling.Performance tips
Use appropriate commitment:
processed- Fastest, some risk of reorgsconfirmed- 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 ↗
Example Performance tipsReference ↗Performance tips.Related
- SDK Setup - Getting started with the SDK
- Bot Architecture - Production bot patterns
- Program Structure - onchain accounts the SDK interacts with