# Transactions

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

Every write the SDK performs goes through two injectable pieces:

**`TxHandler`**: builds and signs. It resolves the blockhash, injects the compute-budget instructions, compiles address lookup tables, and calls the wallet.

**`TxSender`**: broadcasts and confirms. Four implementations ship with the SDK, each with a different retry and confirmation model.

`VelocityClient` creates both, and either can be replaced through its config:

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

const txHandler = new TxHandler({
  connection,
  wallet,
  confirmationOptions: { commitment: "confirmed", preflightCommitment: "confirmed" },
});

const velocityClient = new VelocityClient({
  connection,
  wallet,
  env: "mainnet-beta",
  txHandler,
  txSender: new WhileValidTxSender({ connection, wallet, txHandler }),
  txParams: { computeUnits: 600_000, computeUnitsPrice: 0 },
});
```
  

With neither passed, `VelocityClient` builds a `TxHandler` from its own connection and wallet, then wraps it in a `RetryTxSender`. Default `txParams` are `computeUnits: 600_000` and `computeUnitsPrice: 0`, so the default pays no priority fee.

## Choosing a tx sender

All four senders share the same `TxSender` interface (`send`, `sendVersionedTransaction`, `getVersionedTransaction`, `sendRawTransaction`, `simulateTransaction`, plus `getTimeoutCount`, `getTxLandRate`, `getSuggestedPriorityFeeMultiplier`). They differ in how the transaction reaches the cluster and how long they keep trying.

| Sender | Broadcast behavior | Default `retrySleep` | Default `timeout` | Pick it when |
|---|---|---|---|---|
| `RetryTxSender` | Sends once, then resends the same signed bytes to `connection` and every `additionalConnections` entry on a fixed interval | 2,000 ms | 35,000 ms | General purpose. This is the default `VelocityClient` builds. |
| `WhileValidTxSender` | Same resend loop as `RetryTxSender`, and additionally records each in-flight signature's blockhash and `lastValidBlockHeight` in its `untilValid` map | 2,000 ms | 35,000 ms (inherited from `BaseTxSender`; not a constructor option) | Per-transaction blockhash validity bookkeeping has to be available to the calling code, for example a UI that reports "expired" separately from "dropped". |
| `FastSingleTxSender` | Sends exactly once and relies on the RPC node's own forwarding. Can skip confirmation, or confirm in the background so the call returns right after the send | not applicable (no resend loop) | 35,000 ms | Latency-sensitive keepers that would rather fire again with fresh inputs than pay for a resend loop. |
| `ForwardOnlyTxSender` | Never calls `sendRawTransaction` on `connection`. Hands the base58 transaction to `additionalTxSenderCallbacks` and re-invokes them on an interval. `connection` is used only to confirm | 5,000 ms | 35,000 ms | Submission is delegated to a relay or bundler (for example a Jito bundle submitter) while the SDK still handles confirmation and land-rate tracking. |

Shared constructor options:

| Option | Meaning | Default |
|---|---|---|
| `opts` | `ConfirmOptions` used for sends and confirmations | `DEFAULT_CONFIRMATION_OPTS` (`commitment` and `preflightCommitment` both `confirmed`) with `maxRetries: 0`, so the RPC node does not retry on top of the sender's own loop |
| `confirmationStrategy` | `ConfirmationStrategy.WebSocket` (an `onSignature` subscription), `Polling` (`getSignatureStatuses`), or `Combo` (race both, resolve on whichever lands first) | `Combo` |
| `additionalConnections` | Extra connections to also broadcast to | `[]` (and always `[]` for `ForwardOnlyTxSender`, which does not accept the option) |
| `additionalTxSenderCallbacks` | Callbacks invoked with each sent transaction's base58 encoding | `[]` |
| `trackTxLandRate` | Record sent signatures so `getTxLandRate()` and `getSuggestedPriorityFeeMultiplier()` return real numbers | off |
| `txLandRateLookbackWindowMinutes` | Rolling window for the land-rate calculation | 10 minutes |
| `landRateToFeeFunc` | Maps a land rate to a priority-fee multiplier | Built-in: returns `1` while the land rate is at or above 0.9 or fewer than 3 samples exist, otherwise grows logarithmically, capped at `10` |
| `throwOnTimeoutError` | Whether a confirmation timeout throws `TxSendError` | `true` |

`FastSingleTxSender` adds `skipConfirmation` (return immediately after sending), `confirmInBackground` (confirm without awaiting, so the returned `slot` is `undefined`), `blockhashRefreshInterval` (default 10,000 ms, `0` disables the loop), and `blockhashCommitment` (default `finalized`). `WhileValidTxSender` adds `throwOnTransactionError` (default `true`).

> **Warning:**
>
> `TxSendError` carries a numeric `code`. A confirmation timeout uses `NOT_CONFIRMED_ERROR_CODE` (`-1001`), which is an SDK-internal code meaning "we never saw a confirmation or a definite failure", not a Solana or program error. Treat it as unknown outcome and re-check the signature before resubmitting.

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

// Fire and forget: return as soon as the send lands, confirm in the background.
const txSender = new FastSingleTxSender({
  connection,
  wallet,
  confirmInBackground: true,
  blockhashRefreshInterval: 5_000,
  trackTxLandRate: true,
});

const velocityClient = new VelocityClient({
  connection,
  wallet,
  env: "mainnet-beta",
  txSender,
});
```
  

  
```js
import { ForwardOnlyTxSender } from "@velocity-exchange/sdk";

// Submission is delegated: the SDK only confirms.
const txSender = new ForwardOnlyTxSender({
  connection,
  wallet,
  retrySleep: 5_000,
  additionalTxSenderCallbacks: [
    (base58EncodedTx) => myRelay.submit(base58EncodedTx),
  ],
});
```
  

> **Info:**
>
> `ForwardOnlyTxSender` defaults `additionalTxSenderCallbacks` to an empty array. With no callbacks it never broadcasts the transaction anywhere, so it will always time out.

## Blockhash handling in TxHandler

`TxHandler` owns blockhash resolution. By default it uses a caching fetcher that reuses a recent blockhash for 2,000 ms, which collapses per-build `getLatestBlockhash` calls during bursts. Blockhashes stay valid onchain far longer than that window, so this is safe.

| `txHandlerConfig` field | Meaning | Default |
|---|---|---|
| `blockhashCachingEnabled` | `false` forces a fresh RPC fetch on every build | `true` |
| `blockhashCachingConfig.retryCount` | Fetch retries | 3 |
| `blockhashCachingConfig.retrySleepTimeMs` | Sleep between retries | 200 ms |
| `blockhashCachingConfig.staleCacheTimeMs` | How long a cached blockhash is reused | 2,000 ms |

The commitment used for blockhash fetches comes from `confirmationOptions.preflightCommitment`, falling back to `connection.commitment`, then `confirmed`.

  
```js
import { TxHandler } from "@velocity-exchange/sdk";

const txHandler = new TxHandler({
  connection,
  wallet,
  confirmationOptions: { commitment: "confirmed", preflightCommitment: "confirmed" },
  config: {
    blockhashCachingEnabled: true,
    blockhashCachingConfig: { staleCacheTimeMs: 2_000 },
  },
});

// Build a v0 transaction from raw instructions.
const tx = await txHandler.buildTransaction({
  instructions: [ix],
  txVersion: 0,
  connection,
  fetchAllMarketLookupTableAccounts: () => velocityClient.fetchAllLookupTableAccounts(),
  txParams: { computeUnits: 400_000, computeUnitsPrice: 25_000 },
});
```
  

## Compute units and priority fee params

`txParams` is a `BaseTxParams` plus a `ProcessingTxParams`. The base half is what ends up in the compute-budget instructions; the processing half tells the SDK how to derive those numbers.

| Field | Meaning |
|---|---|
| `computeUnits` | Compute-unit limit to request. `VelocityClient` defaults to 600,000. |
| `computeUnitsPrice` | Priority fee in micro-lamports per compute unit. `VelocityClient` defaults to `0`. |
| `useSimulatedComputeUnits` | Simulate the transaction and use the measured usage as the limit instead of the static value. |
| `computeUnitsBufferMultiplier` | Multiplier applied to the simulated usage. Defaults to `1.2` (20% headroom). |
| `useSimulatedComputeUnitsForCUPriceCalculation` | Also derive the price from the simulated unit count. Requires `useSimulatedComputeUnits` and `getCUPriceFromComputeUnits`. |
| `getCUPriceFromComputeUnits` | Function mapping a compute-unit count to a price in micro-lamports. |
| `lowerBoundCu` | Floor applied to the derived limit. |

> **Warning:**
>
> Solana charges the priority fee on the **requested** limit, not on what the transaction consumes. A 600,000-unit request at 25,000 micro-lamports per unit costs the same whether the instruction burns 40,000 units or 400,000. Right-sizing `computeUnits` (statically or by simulation) is the cheapest priority-fee optimization available.

### TransactionParamProcessor

`TransactionParamProcessor.process()` runs the simulation pipeline. It rebuilds the transaction with the limit forced to 1,400,000 units so simulation is not constrained by an undersized budget, simulates it with `replaceRecentBlockhash: true` (which avoids spurious `blockHashNotFound` failures from an already-stale blockhash), then multiplies the measured usage by the buffer and clamps the result to 1,400,000.

Simulation failure is not fatal. `getTxSimComputeUnits` catches every error and returns `{ success: false }`, and `process()` then leaves the original `computeUnits` in place. The only cases that throw are misconfiguration: `useSimulatedComputeUnitsForCUPriceCalculation` without `useSimulatedComputeUnits`, without `getCUPriceFromComputeUnits`, or when the simulated unit count is unavailable.

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

// Size the limit from a simulation and derive the price from the result.
// `priorityFeeSubscriber` is a subscribed PriorityFeeSubscriber; see below.
const velocityClient = new VelocityClient({
  connection,
  wallet,
  env: "mainnet-beta",
  txParams: {
    computeUnits: 600_000, // fallback if the simulation fails
    useSimulatedComputeUnits: true,
    computeUnitsBufferMultiplier: 1.2,
    lowerBoundCu: 100_000,
    useSimulatedComputeUnitsForCUPriceCalculation: true,
    getCUPriceFromComputeUnits: () =>
      Math.floor(priorityFeeSubscriber.getCustomStrategyResult()),
  },
});

// Or size one transaction directly.
const { success, computeUnits } = await TransactionParamProcessor.getTxSimComputeUnits(
  tx,
  connection,
  1.2,    // buffer multiplier, mandatory
  100_000 // optional lower bound
);
```
  

### PriorityFeeCalculator

`PriorityFeeCalculator` is a smaller, reactive helper: it watches a sender's timeout count and turns the priority fee on only after transactions start timing out, then latches it on for `priorityFeeLatchDurationMs` (default 10,000 ms). Its `generateComputeBudgetWithPriorityFeeIx()` converts a target total fee in micro-lamports into a per-unit price by dividing by the compute-unit limit.

  
```js
import { PriorityFeeCalculator } from "@velocity-exchange/sdk";

const calculator = new PriorityFeeCalculator(Date.now());

const usePriorityFee = calculator.updatePriorityFee(
  Date.now(),
  velocityClient.txSender.getTimeoutCount()
);

const computeBudgetIxs = calculator.generateComputeBudgetWithPriorityFeeIx(
  600_000,        // compute unit limit
  usePriorityFee,
  1_000_000       // additional fee to pay, in micro-lamports
);
```
  

## Priority fee subscribers

For a live fee estimate rather than a reactive switch, use the `priorityFee` module. `PriorityFeeSubscriber` polls one fee source on an interval (default 10,000 ms) and exposes the latest aggregate. Every value it returns is in micro-lamports per compute unit, the unit `ComputeBudgetProgram.setComputeUnitPrice` expects.

### Fee sources

`priorityFeeMethod` selects the upstream API. It defaults to `PriorityFeeMethod.SOLANA`.

| Method | Upstream call | Required config | Notes |
|---|---|---|---|
| `SOLANA` | `getRecentPrioritizationFees` on the configured RPC | `connection` | Samples are filtered to the most recent `slotsToCheck` slots (default 50) relative to the newest returned slot, then sorted descending by slot. Override the call itself with `fetchSolanaPriorityFee` to route through a local cache. |
| `HELIUS` | Helius `getPriorityFeeEstimate` with `includeAllPriorityFeeLevels: true` | `heliusRpcUrl`, or a `connection` whose endpoint contains `helius` | Returns percentile buckets: `min`, `low` (25th), `medium` (50th), `high` (75th), `veryHigh` (95th), `unsafeMax` (100th). Read them with `getHeliusPriorityFeeLevel(level)`, which defaults to `MEDIUM`. |
| `VELOCITY` | Velocity's hosted `/batchPriorityFees` endpoint | `velocityPriorityFeeEndpoint`, `velocityMarkets` | Per-market fee levels, same percentile buckets. See [`/batchPriorityFees`](/developers/ecosystem-builders/orderbook-and-ws.md). |

The constructor throws when `HELIUS` is selected without a resolvable Helius URL, or `SOLANA` without a `connection`.

`addresses` scopes SOLANA and HELIUS sampling to the accounts the transaction will write-lock, so it measures congestion on the markets being traded rather than on the whole cluster.

### Strategies

`customStrategy` is any object implementing `PriorityFeeStrategy.calculate(samples)`. Five implementations ship with the SDK.

| Strategy | Result | Empty sample set |
|---|---|---|
| `AverageOverSlotsStrategy` | Mean fee across the lookback window. This is the default `customStrategy`. | `0` |
| `AverageStrategy` | Mean fee across all samples, without the empty guard | `NaN` |
| `MaxOverSlotsStrategy` | Highest fee in the lookback window | `0` |
| `MaxStrategy` | Highest fee across all samples, without the empty guard | `-Infinity` |
| `EwmaStrategy` | Exponentially weighted moving average over slots, so recent samples dominate and larger slot gaps decay older samples faster. Half life defaults to 25 slots (roughly 10 seconds) | `0` |

Prefer the `...OverSlots` variants unless the caller handles the empty case.

Every getter (`getCustomStrategyResult`, `getAvgStrategyResult`, `getMaxStrategyResult`) multiplies by `priorityFeeMultiplier` (default `1.0`) and then clamps to `maxFeeMicroLamports` when one is set. All three return `0` before the first successful `load()`. `getHeliusPriorityFeeLevel` clamps but does not apply the multiplier. Both the multiplier and the clamp can be changed at runtime with `updatePriorityFeeMultiplier()` and `updateMaxPriorityFee()`.

  
```js
import {
  EwmaStrategy,
  PriorityFeeMethod,
  PriorityFeeSubscriber,
} from "@velocity-exchange/sdk";

const priorityFeeSubscriber = new PriorityFeeSubscriber({
  connection,
  frequencyMs: 5_000,
  priorityFeeMethod: PriorityFeeMethod.SOLANA,
  slotsToCheck: 50,
  addresses: [perpMarketPublicKey, userAccountPublicKey],
  customStrategy: new EwmaStrategy(25),
  priorityFeeMultiplier: 1.5,
  maxFeeMicroLamports: 1_000_000,
});

await priorityFeeSubscriber.subscribe();

// Micro-lamports per compute unit, multiplied and clamped.
const computeUnitsPrice = Math.floor(priorityFeeSubscriber.getCustomStrategyResult());

await priorityFeeSubscriber.unsubscribe();
```
  

`load()` swallows errors and logs them, so a transient RPC failure leaves the previous values in place until the next poll. Call `updateAddresses()` or `updateMarketTypeAndIndex()` to change what is sampled; both take effect on the next poll.

### PriorityFeeSubscriberMap

`PriorityFeeSubscriberMap` fetches per-market fee levels for many markets in one request and keeps them addressable individually, rather than collapsing them into a single aggregate. It only supports the Velocity endpoint.

  
```js
import { PriorityFeeSubscriberMap } from "@velocity-exchange/sdk";

const subscriberMap = new PriorityFeeSubscriberMap({
  velocityPriorityFeeEndpoint: "<DLOB_ENDPOINT>", // e.g. https://dlob.velocity.exchange
  frequencyMs: 5_000,
  velocityMarkets: [
    { marketType: "perp", marketIndex: 0 },
    { marketType: "perp", marketIndex: 1 },
    { marketType: "spot", marketIndex: 2 },
  ],
});

await subscriberMap.subscribe();

// undefined until the first successful load
const fees = subscriberMap.getPriorityFees("perp", 0);
console.log(fees?.medium, fees?.veryHigh);
```
  

## Reacting to poor landing

With `trackTxLandRate: true`, the sender records each signature and computes a rolling land rate. Feed the suggested multiplier into the compute-unit price to back off automatically under congestion:

  
```js
import { RetryTxSender } from "@velocity-exchange/sdk";

const txSender = new RetryTxSender({
  connection,
  wallet,
  trackTxLandRate: true,
  txLandRateLookbackWindowMinutes: 10,
});

const landRate = txSender.getTxLandRate();                    // 0 to 1
const multiplier = txSender.getSuggestedPriorityFeeMultiplier(); // 1 to 10
const timeouts = txSender.getTimeoutCount();

const computeUnitsPrice = Math.floor(
  priorityFeeSubscriber.getCustomStrategyResult() * multiplier
);
```
  

## Related

- [Setup](/developers/velocity-sdk/setup.md): creating the client and its config
- [SDK Internals](/developers/velocity-sdk/sdk-internals.md): subscription modes, instruction building, and error handling
- [Orderbook and DLOB websocket](/developers/ecosystem-builders/orderbook-and-ws.md): the hosted `/priorityFees` and `/batchPriorityFees` endpoints
- [Bot Architecture](/developers/market-makers/bot-architecture.md): production bot patterns
