Skip to main content
Best practices and recommended patterns for agents using the Helius TypeScript SDK. For installation and getting started, see the overview.

Recommendations for Agents

Use getTransactionsForAddress instead of two-step lookup

getTransactionsForAddress combines signature lookup and transaction fetching into a single call with server-side filtering. It supports time/slot ranges, token account filtering, and pagination.
// GOOD: Single call, server-side filtering
const txs = await helius.getTransactionsForAddress([
  "address",
  {
    transactionDetails: "full",
    limit: 100,
    filters: {
      tokenAccounts: "balanceChanged",
      blockTime: { gte: Math.floor(Date.now() / 1000) - 86400 },
    },
  },
]);

// BAD: Two calls, client-side filtering, no token account support
const sigs = await helius.raw.getSignaturesForAddress(address).send();
const txs = await Promise.all(sigs.map(s => helius.raw.getTransaction(s.signature).send()));

Use sendSmartTransaction for standard sends

It automatically simulates, estimates compute units, fetches priority fees, and confirms. Do not manually build ComputeBudget instructions — the SDK adds them automatically.
const sig = await helius.tx.sendSmartTransaction({
  instructions: [yourInstruction],
  signers: [walletSigner],
  commitment: "confirmed",
  priorityFeeCap: 100_000,   // Optional: cap fees in microlamports/CU
  bufferPct: 0.1,            // 10% compute unit headroom (default)
});

Use Helius Sender for ultra-low latency

For time-sensitive transactions (arbitrage, sniping, liquidations), use sendTransactionWithSender. It routes through Helius’s multi-region infrastructure and Jito.
const sig = await helius.tx.sendTransactionWithSender({
  instructions: [yourInstruction],
  signers: [walletSigner],
  region: "US_EAST",          // Default, US_SLC, US_EAST, EU_WEST, EU_CENTRAL, EU_NORTH, AP_SINGAPORE, AP_TOKYO
  swqosOnly: true,            // Route through SWQOS only (lower tip requirement)
  pollTimeoutMs: 60_000,
  pollIntervalMs: 2_000,
});

Use getAssetBatch for multiple assets

When fetching more than one asset, batch them. Do not call getAsset in a loop.
// GOOD: Single request
const assets = await helius.getAssetBatch({
  ids: ["mint1", "mint2", "mint3"],
  options: { showFungible: true, showCollectionMetadata: true },
});

// BAD: N requests
const assets = await Promise.all(mints.map(id => helius.getAsset({ id })));

Use webhooks or WebSockets instead of polling

Do not poll getTransactionsForAddress in a loop. Use webhooks for server-to-server notifications or WebSockets for real-time client-side streaming.
// Webhook: server receives POST on matching transactions
const webhook = await helius.webhooks.create({
  webhookURL: "https://your-server.com/webhook",
  webhookType: "enhanced",
  transactionTypes: ["TRANSFER", "NFT_SALE", "SWAP"],
  accountAddresses: ["address_to_monitor"],
  authHeader: "Bearer your-secret",
});

// WebSocket: stream logs in real-time
const req = await helius.ws.logsNotifications({ mentions: ["address"] });
const stream = await req.subscribe({ abortSignal: controller.signal });
for await (const log of stream) {
  console.log(log);
}

Pagination

The SDK uses different pagination strategies depending on the method.

Token/Cursor-Based (RPC V2 Methods)

// getTransactionsForAddress uses paginationToken
let paginationToken = null;
const allTxs = [];
do {
  const result = await helius.getTransactionsForAddress([
    "address",
    { limit: 100, paginationToken },
  ]);
  allTxs.push(...result.data);
  paginationToken = result.paginationToken;
} while (paginationToken);

// getProgramAccountsV2 uses paginationKey
let paginationKey = null;
do {
  const result = await helius.getProgramAccountsV2([
    programId,
    { limit: 1000, paginationKey },
  ]);
  // process result.accounts
  paginationKey = result.paginationKey;
} while (paginationKey);

Page-Based (DAS API)

let page = 1;
const allAssets = [];
while (true) {
  const result = await helius.getAssetsByOwner({ ownerAddress: "...", page, limit: 1000 });
  allAssets.push(...result.items);
  if (result.items.length < 1000) break;
  page++;
}

tokenAccounts Filter

When querying getTransactionsForAddress, the tokenAccounts filter controls whether token account activity is included:
ValueBehaviorUse When
omitted / "none"Only transactions directly involving the addressYou only care about SOL transfers and program calls
"balanceChanged"Also includes token transactions that changed a balanceRecommended for most agents — shows token sends/receives without noise
"all"Includes all token account transactionsYou need complete token activity (can return many results)

changedSinceSlot — Incremental Account Fetching

changedSinceSlot returns only accounts modified after a given slot. Useful for syncing or indexing workflows. Supported by getProgramAccountsV2, getTokenAccountsByOwnerV2, getAccountInfo, getMultipleAccounts, getProgramAccounts, and getTokenAccountsByOwner.
// First fetch: get all accounts
const baseline = await helius.getProgramAccountsV2([programId, { limit: 10_000 }]);
const lastSlot = currentSlot;

// Later: only get accounts that changed since your last fetch
const updates = await helius.getProgramAccountsV2([
  programId,
  { limit: 10_000, changedSinceSlot: lastSlot },
]);

Common Mistakes

  1. transactionDetails: "full" is not the default — By default, getTransactionsForAddress returns signatures only. Set transactionDetails: "full" to get full transaction data.
  2. Do not add ComputeBudget instructions with sendSmartTransaction — The SDK adds them automatically. Adding your own causes duplicate instructions and transaction failure.
  3. Priority fees are in microlamports per compute unit — Not lamports. Values from getPriorityFeeEstimate are already in the correct unit for SetComputeUnitPrice.
  4. DAS pagination is 1-indexedpage: 1 is the first page, not page: 0.
  5. blockTime is Unix seconds, not milliseconds — Use Math.floor(Date.now() / 1000) when filtering by blockTime.
  6. getAsset hides fungible tokens by default — Pass options: { showFungible: true } to include them.
  7. WebSocket streams need cleanup — Always use an AbortController signal and call helius.ws.close() when done to avoid connection leaks.

Error Handling and Retries

The SDK throws native Error objects with the HTTP status code embedded in the message string (e.g., "API error (429): ..."). There is no .status property on the error object, so status detection requires message parsing.
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const msg = error instanceof Error ? error.message : "";
      const status = msg.match(/\b(\d{3})\b/)?.[1];
      const retryable = status === "429" || (status && status.startsWith("5"));
      if (!retryable || attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
    }
  }
  throw new Error("Unreachable");
}
StatusMeaningAction
401Invalid or missing API keyCheck API key
429Rate limited or out of creditsBack off and retry
5xxServer errorRetry with exponential backoff