# SDK Internals

> Canonical: https://docs.velocity.exchange/developers/velocity-sdk/sdk-internals

This page covers the machinery underneath the call-level pages: how the SDK keeps account state fresh, which subscribers and maps it ships, how it layers instruction building into transactions, and where the caches are that make reads free. It is the page for deciding how a long-running process should stay subscribed, and for working out why a value read back is not the expected one.

> **Info:**
>
> The examples below use the TypeScript SDK (`@velocity-exchange/sdk`). There is no Python SDK. A Rust client, `velocity-rs`, ships from the same private monorepo but is not published; it is not something an outside integrator can clone or install.

## Core architecture

Three pieces carry almost everything the SDK does, and knowing which one owns a given responsibility is usually enough to find the right method.

**VelocityClient**: the write path and the protocol-wide cache. It builds and sends every instruction, owns the market, oracle, and state accounts, and exposes the precision helpers. Nearly every call that changes onchain state starts here.

**User**: one subaccount, wrapped. It holds that account's positions, orders, and balances, and computes the derived values on top of them: margin requirement, free collateral, health, unrealized PnL. See [PnL & Risk](/developers/velocity-sdk/pnl-risk.md).

**AccountSubscriber**: the update transport underneath both. It polls or streams account data from RPC, notifies its owner when the data changes, and holds the cached copy that every read above returns.

## Account subscription strategies

`accountSubscription` on `VelocityClientConfig` selects how the client keeps market, oracle, and user accounts current. All three modes present the same cached-read API to the caller; they differ in how updates arrive, and therefore in latency, RPC cost, and what can go wrong.

| Mode | How updates arrive | Latency | Cost and failure modes | Suits |
|---|---|---|---|---|
| `polling` | A `BulkAccountLoader` batches the accounts the client needs into periodic `getMultipleAccounts` calls | One poll interval, so a 1,000 ms loader means up to 1,000 ms of staleness | Predictable request volume against any RPC endpoint, and nothing to reconnect | Development, low-frequency strategies, backfills and scripts |
| `websocket` | Solana's `onAccountChange` notifications, per subscribed account | Push, so roughly one network hop behind the cluster | Fewer requests, but a websocket can drop and needs resubscribe handling, and many endpoints cap concurrent subscriptions | Production trading, market makers, anything reacting to fills |
| `grpc` | A Yellowstone gRPC (Geyser) stream from a provider running the plugin | The lowest of the three, and the most bandwidth-efficient per update | Needs a gRPC-enabled provider such as Jito or Triton, an auth token, and usually a paid plan | Latency-sensitive fillers, JIT market makers |

Websocket is the default. Polling takes a `BulkAccountLoader`, whose constructor is `(connection, commitment, pollingFrequencyMs)`. Accounts rarely need registering on the loader directly: hand it to the client and it adds the market, oracle, and user accounts it needs, batching them into as few `getMultipleAccounts` calls as it can.

  
```js
import { BulkAccountLoader, VelocityClient } from "@velocity-exchange/sdk";

// Websocket, the default: omit accountSubscription entirely, or name it.
const wsClient = new VelocityClient({
  connection,
  wallet,
  accountSubscription: { type: "websocket" },
});

// Polling, driven by a shared BulkAccountLoader at 1,000 ms.
const accountLoader = new BulkAccountLoader(connection, "confirmed", 1000);
const pollingClient = new VelocityClient({
  connection,
  wallet,
  accountSubscription: { type: "polling", accountLoader },
});

// gRPC, against a Geyser-enabled provider.
const grpcClient = new VelocityClient({
  connection,
  wallet,
  accountSubscription: {
    type: "grpc",
    grpcConfigs: {
      endpoint: "<GRPC_ENDPOINT>",
      token: "<GRPC_TOKEN>",
    },
  },
});
```
  

One `BulkAccountLoader` can back several clients and maps at once, which is usually the right arrangement: it deduplicates the accounts and keeps the batch count down instead of each consumer polling on its own schedule.

## Transaction construction

A write goes through three layers, and the high-level methods collapse all three into one call. Drop down a layer to batch several instructions into one transaction, attach a custom compute budget, or hand the signed bytes somewhere the SDK does not know about.

  
```js
// 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
);
```
  

Both the builder and the sender 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](/developers/velocity-sdk/transactions.md) for the four sender implementations, their retry and timeout defaults, and the compute-unit and priority-fee parameters.

## Remaining accounts

Most Velocity instructions need an account list that depends on the caller's state: the oracles and markets touched by the position set, plus any cross-position accounts the margin check has to read. `getRemainingAccounts()` assembles that list in the order the onchain handler expects, so the caller does not have to reproduce the program's ordering rules.

  
```js
const remainingAccounts = velocityClient.getRemainingAccounts({
  userAccounts: [user.getUserAccount()],
  writableSpotMarketIndexes: [0], // quote-asset spot 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();
```
  

## Event subscriptions

The SDK deserializes program events out of transaction logs and emits them to the caller. See [Events](/developers/velocity-sdk/events.md) for the full catalog, including the fee-sweep, revenue-share, and LP borrow-lend records, and for the query helpers over the in-memory buffer.

  
```ts
import { EventSubscriber, WrappedEvent, isVariant } 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 the discriminant is checked.
  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);
    }
  }
});
```
  

The four reached for first:

- `OrderActionRecord`: order lifecycle, discriminated by `action` (`fill`, `place`, `cancel`, and so on)
- `DepositRecord`: deposits and withdrawals
- `FundingPaymentRecord`: funding payments
- `LiquidationRecord`: liquidations

## Caching

Once a client is subscribed, every accessor named `get...` reads out of the subscription cache rather than the network. Market accounts, oracle data, state, and the subscribed subaccounts are all served from memory and refreshed by the subscriber in the background, so calling one in a tight quoting loop costs nothing and never adds RPC latency.

The tradeoff is that a cached read is exactly as fresh as the subscription behind it. Under `polling` that is one poll interval; under `websocket` or `grpc` it is whatever the last push delivered. If the feed breaks, the reads keep returning the last good value rather than failing, which is why the `error` listener in [Error handling](#error-handling) matters.

  
```js
// All of these are served from the subscription cache. No RPC call.
const market = velocityClient.getPerpMarketAccount(0);
const oracle = velocityClient.getOracleDataForPerpMarket(0);
const position = velocityClient.getUser().getPerpPosition(0);

// Force a refresh from RPC when a value has to be certain.
await velocityClient.getUser().fetchAccounts();
```
  

## UserMap for multiple users

`UserMap` keeps many `User` accounts subscribed and addressable by pubkey, which is what liquidation bots, fillers, and anything scanning the whole protocol build their state from. It owns the subscription lifecycle for every account in it, so the map is subscribed rather than each user.

  
```js
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);
```
  

## Subscribers and maps reference

Beyond the account-subscription strategies above, the SDK root exports a set of standalone subscribers (live feeds started 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.

| Export | What it tracks | Notes |
|---|---|---|
| `SlotSubscriber` | The current slot, via `connection.onSlotChange` | Ignores updates that are not strictly greater than the tracked slot. Optional stall-detection resubscribe. |
| `SlothashSubscriber` | The 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. |
| `ClockSubscriber` | The onchain unix timestamp from the `Clock` sysvar | Gives "onchain 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. |
| `BlockhashSubscriber` | A short history of recent blockhashes | Polls `getLatestBlockhashAndContext` every 1,000 ms 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`. |
| `ChainClock` | Latest known block height, slot, and timestamp per commitment level | Used by transaction senders to decide whether a blockhash has expired, without an RPC call. |
| `AuctionSubscriber` | `User` accounts that currently have an order inside its auction window | A filtered websocket program-account subscription, so fillers react to auction-eligible orders without scanning every user. |
| `OrderSubscriber` | Every `User` account on the program, and therefore every open order | Polling, websocket, or gRPC depending on `subscriptionConfig.type`. Backs `DLOBSubscriber`. Emits `orderCreated`, `userUpdated`, and `updateReceived`. |
| `DLOBSubscriber` | A continuously rebuilt DLOB | Built on top of an order source such as `OrderSubscriber`. See [DLOB](/developers/velocity-sdk/dlob.md). |
| `UserMap` | `User` accounts, keyed by user account pubkey | The general-purpose account cache, described above. |
| `UserStatsMap` | `UserStats` accounts, keyed by authority pubkey | One per trading authority, shared across that authority's subaccounts. Uses a shared `BulkAccountLoader` by default. |
| `ReferrerMap` | Authority to referrer, plus `ReferrerInfo` per referrer | Built by scanning `UserStats` with memcmp filters on the referrer-status byte instead of subscribing to full accounts. The referrer counterpart of `RevenueShareEscrowMap`. |
| `RevenueShareEscrowMap` | `RevenueShareEscrow` accounts, keyed by authority | The builder and referral fee-accrual escrow. Fillers read it to attach a taker's escrow to a fill. |
| `ConstituentMap` | LP-pool `Constituent` accounts | Look up by pubkey, spot market index, or constituent index. |
| `PythLazerSubscriber` | Pyth Lazer price feeds over websocket | Takes 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` / `PriorityFeeSubscriberMap` | Recent priority fees for selected accounts or markets | Feeds the compute-unit price attached to transactions. See [Transactions](/developers/velocity-sdk/transactions.md). |
| `EventSubscriber` | Program 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

The shape most integrations end up with: construct and subscribe once at startup, build instructions rather than sending one at a time where a compute budget or a batch is needed, and switch subaccounts on the client rather than holding several clients.

  
```js
import { ComputeBudgetProgram } from "@solana/web3.js";
import { VelocityClient } from "@velocity-exchange/sdk";

// Initialize and subscribe
const velocityClient = new VelocityClient({
  connection,
  wallet,
  env: "mainnet-beta",
});
await velocityClient.subscribe();

const user = velocityClient.getUser();
await user.subscribe();

// Transaction with an explicit priority fee
const ix = await velocityClient.getPlacePerpOrderIx(orderParams);
const tx = await velocityClient.txSender.getVersionedTransaction(
  [ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }), ix],
  [] // lookup tables
);
const { txSig } = await velocityClient.txSender.sendVersionedTransaction(
  tx,
  [],
  velocityClient.opts
);

// Switch subaccounts on the client rather than holding several clients
await velocityClient.switchActiveUser(1);
const user1 = velocityClient.getUser(); // now subaccount 1

// Batch: several orders in one transaction
await velocityClient.placeOrders([order1, order2, order3]);
```
  

## Error handling

A failed SDK call is one of three things, and they want different responses.

**A program error.** The instruction reached the program and the program rejected it. These are Anchor errors numbered from `6000` up, and the program logs carry both the name and the number, for example `AnchorError occurred. Error Code: InsufficientCollateral. Error Number: 6003.` Match on the number, not on the message text: the `msg` strings change between releases, the numbers do not. The full list lives in the IDL the SDK ships, so a code maps back to a name without a hardcoded table.

**A send or confirmation failure.** The transaction never reached the program, or the SDK never saw its outcome. These surface as `TxSendError` with a numeric `code`. The important one is `NOT_CONFIRMED_ERROR_CODE` (`-1001`), which means the sender timed out without seeing either a confirmation or a definite failure. It is an unknown outcome, not a failure: re-check the signature before resubmitting anything, or the same order can be placed twice. See [Transactions](/developers/velocity-sdk/transactions.md).

**A subscription error.** The account feed broke while the process kept running. The client emits these on its own event emitter rather than throwing at a call site, so a process that does not listen for them goes on trading against a stale cache.

  
```js
import { NOT_CONFIRMED_ERROR_CODE, TxSendError } from "@velocity-exchange/sdk";

// The client's Anchor program carries the IDL, and the IDL carries every
// error code the program can return. No hardcoded table to keep in sync.
const errorsByCode = new Map(
  velocityClient.program.idl.errors.map((e) => [e.code, e.name])
);

function programErrorName(e) {
  const match = /Error Number: (\d+)/.exec((e.logs ?? []).join("\n"));
  return match ? errorsByCode.get(Number(match[1])) : undefined;
}

try {
  await velocityClient.placePerpOrder(orderParams);
} catch (e) {
  if (e instanceof TxSendError && e.code === NOT_CONFIRMED_ERROR_CODE) {
    // Unknown outcome. Check the signature before retrying.
    return;
  }
  switch (programErrorName(e)) {
    case "InsufficientCollateral":
      console.log("deposit more collateral before retrying");
      break;
    case "SpotDlobTradingDisabled":
      console.log("this market cannot take order-book orders");
      break;
    default:
      throw e;
  }
}

// Subscription failures do not throw at a call site.
velocityClient.eventEmitter.on("error", (e) => {
  console.error("subscription error:", e);
  // Resubscribe, and treat cached reads as stale until it recovers.
});
```
  

## Performance notes

Commitment is the first knob. `processed` is the fastest and can be rolled back, `confirmed` is the default and the right choice for almost everything, `finalized` is the slowest and only worth it where a reorg cannot be tolerated at all.

Beyond that, two habits cost nothing and save real time. Convert once and reuse the `BN`: `convertToPerpPrecision` and `convertToPricePrecision` are pure, so hoisting them out of a quoting loop removes allocation from the hot path. And attach address lookup tables when batching instructions, because a versioned transaction that resolves accounts through an ALT fits more instructions inside the 1,232-byte limit.

  
```js
// Convert once, reuse across the loop.
const size = velocityClient.convertToPerpPrecision(1);
const price = velocityClient.convertToPricePrecision(100);

// Lookup tables let a batched transaction reference more accounts.
const lookupTables = await velocityClient.fetchAllLookupTableAccounts();
const tx = await velocityClient.txSender.getVersionedTransaction(
  instructions,
  lookupTables
);
```
  

## Related

- [Setup](/developers/velocity-sdk/setup.md): constructing and subscribing the client
- [Transactions](/developers/velocity-sdk/transactions.md): tx senders, compute units, priority fees
- [Bot Architecture](/developers/market-makers/bot-architecture.md): production bot patterns
- [Program Structure](/developers/concepts/program-structure.md): the onchain accounts the SDK wraps
