15 min read
Overview
Getting transaction history for a Solana address used to take more than one call. getSignaturesForAddress returns signatures, up to 1,000 per page, and every signature needs its own getTransaction call to see what the transaction did. Building a wallet's history means looping through that fan-out page by page and stitching the results together yourself. It multiplies if the wallet holds tokens. Token transfers reference the wallet's associated token accounts (ATAs) rather than the wallet itself, so you first discover those accounts, then repeat the whole loop for each one and merge everything client-side.
Quicknode's getTransactionsForAddress returns an address's transactions and its token account activity in a single call, with server-side filtering, sorting, and cursor pagination. This guide builds a complete wallet history feed with it.
- Build a complete Solana wallet history feed with
getTransactionsForAddress: one request per page, instead of the ~1,000 RPC calls thegetSignaturesForAddress+getTransactionpattern needs - Set
filters.tokenAccountsto pick up the token transfers a wallet-address-only history silently drops, andtransactionDetails: "full"to get decoded transactions andmetainline - Follow the
paginationTokencursor to the end of a wallet's history with a reusable async generator, then persist the token as a crash-resumable checkpoint - Apply four filter recipes: indexer backfill, fixed-window statement, slot-range audit, and failed-transaction forensics
- Every script is standalone TypeScript running against a Quicknode Solana Mainnet endpoint with
@solana/kit
What Is getTransactionsForAddress?
getTransactionsForAddress is a Quicknode Solana RPC method that returns an address's transaction history in a single call, including activity in the token accounts that address owns. It supports server-side filtering by slot, block time, signature position, and status, returns full decoded transactions on request, and paginates with a cursor.
The method accepts any account address: wallets, programs, program derived addresses (PDAs), vaults, and token accounts. This guide uses a wallet, because that is where the token account problem shows up. The recipes near the end apply the same helpers to program-owned accounts.
What You Will Do
- Query a wallet's transaction history with a single RPC call
- Include token account activity with the
tokenAccountsfilter and compare the result counts - Fetch full transaction payloads with versioned transaction support
- Build a reusable pagination helper that follows the cursor through every page
- Apply filter recipes for backfills, statements, slot audits, and failure forensics
What You Will Need
- A Quicknode account with a Solana Mainnet endpoint (the method is part of Quicknode's Solana JSON-RPC API, so no add-on is required)
- Node.js version 22 or later
- Basic TypeScript experience and familiarity with RPC requests
- A Solana Mainnet wallet address to inspect
Why Wallet History on Solana Is Hard
The standard RPC methods create two problems that compound each other.
The getSignaturesForAddress and getTransaction Loop
getSignaturesForAddress returns signatures only, up to 1,000 per page, walked backward with a before cursor. Every signature then needs its own getTransaction call to see what actually happened. A wallet with 1,000 transactions costs roughly 1,000 follow-up calls, each one subject to rate limits, retries, and backoff logic. There is also no server-side filtering. To find transactions from a specific timeframe, the client pages backward from the present and discards everything newer until it crosses the window.
The Token Account Gap
Token balances live in associated token accounts, not in the wallet, and a token transfer references the ATA rather than the owner. getSignaturesForAddress on the wallet address never sees those transactions. A complete history means calling getTokenAccountsByOwner to discover the wallet's token accounts, walking signature history for the wallet and each ATA separately, then merging, deduplicating, and sorting everything client-side.
Even that is not complete. getTokenAccountsByOwner only returns accounts that still exist. When a user closes a token account to reclaim rent, its history becomes undiscoverable through this pattern. getTransactionsForAddress avoids the problem entirely because the index is built on the owner relationship at ingest time, so closed token accounts remain part of the wallet's history.
getSignaturesForAddress vs. getTransactionsForAddress
In the comparison below, the call-count row assumes pulling the complete history, token transfers included, of a wallet holding 10 Solana Program Library (SPL) tokens with 1,000 total transactions.
| Capability | getSignaturesForAddress + getTransaction | getTransactionsForAddress |
|---|---|---|
| RPC calls for that scenario | ~1,012 (one getTokenAccountsByOwner, ~11 signature pages, 1,000 getTransaction calls) | 1 request per page, up to 1,000 results per page |
| Token account (ATA) activity | Not included; discover accounts yourself, then query each separately | Included via filters.tokenAccounts |
| Closed token accounts | Invisible; getTokenAccountsByOwner only returns accounts that still exist | Included; the index is built on the owner field |
| Merge, dedupe, sort across accounts | Your code | Server side |
| Decoded transaction in the same response | No, one getTransaction per signature | Yes, with transactionDetails: "full" |
| Filter by status | Page every signature, check err client side | filters.status |
| Filter by time range | Page backward and discard until you cross the window | filters.blockTime with gte, gt, lte, lt, eq |
| Filter by slot range | No slot cursor exists | filters.slot with gte, gt, lte, lt |
| Oldest-first traversal | Page backward to the first transaction, buffer everything, reverse | sortOrder: "asc" |
| Cursor | before / until; you must already hold a signature to anchor on | paginationToken, returned in every response |
| Resume from a known signature | before only, backward | filters.signature, positional in either direction |
| Ordering within a block | Not exposed | transactionIndex on every result |
For a Solana wallet holding 10 SPL tokens with 1,000 total transactions, the getSignaturesForAddress pattern costs about 1,012 RPC calls:
- One
getTokenAccountsByOwner - ~11 signature pages
- 1,000 individual
getTransactioncalls.
getTransactionsForAddress returns the same history in one request per page of up to 1,000 results, and it still includes any token account the wallet has since closed, which the old pattern cannot reach at any call count.
Set Up a Project
Create a project directory and install the dependencies:
mkdir gtfa-wallet-history && cd gtfa-wallet-history
npm init -y
npm pkg set type=module
npm install @solana/kit@8 tsx@4
npm install --save-dev @types/node
Create a .env file with your Quicknode endpoint and the wallet you want to inspect. You can find your endpoint on the Quicknode dashboard after creating a Solana Mainnet endpoint.
QN_ENDPOINT=https://your-endpoint-name.solana-mainnet.quiknode.pro/your-token/
WALLET_ADDRESS=ANY_MAINNET_WALLET_ADDRESS
getTransactionsForAddress is a Quicknode extension to the Solana JSON-RPC API, so it is not part of the default RPC surface in Solana Kit (@solana/kit). Kit supports this directly: createJsonRpcApi builds a client for any custom method, with the same transport and response handling as the standard API. Create a shared client in gtfa.ts that the rest of the guide imports:
import { createDefaultRpcTransport, createJsonRpcApi, createRpc } from "@solana/kit";
// ---- Request types ----------------------------------------------------
type Comparison<T> = { gte?: T; gt?: T; lte?: T; lt?: T };
export type GtfaFilters = {
slot?: Comparison<number>;
blockTime?: Comparison<number> & { eq?: number }; // Unix seconds
signature?: Comparison<string>; // positional, base-58 signature
status?: "any" | "succeeded" | "failed"; // default: any
tokenAccounts?: "none" | "balanceChanged" | "all"; // default: none
};
export type GtfaConfig = {
transactionDetails?: "signatures" | "full"; // default: signatures
sortOrder?: "asc" | "desc"; // default: desc
limit?: number; // 1 to 1000, default 1000
paginationToken?: string;
commitment?: "confirmed" | "finalized";
minContextSlot?: number;
encoding?: "json" | "jsonParsed" | "base58" | "base64"; // default: json, full mode only
maxSupportedTransactionVersion?: number; // full mode only
filters?: GtfaFilters;
};
// ---- Response types ---------------------------------------------------
export type GtfaItem = {
slot: number;
transactionIndex: number;
blockTime: number | null;
// Present in signatures mode:
signature?: string;
err?: object | null;
memo?: string | null;
confirmationStatus?: "confirmed" | "finalized";
// Present in full mode:
transaction?: any;
meta?: any;
version?: "legacy" | number;
};
export type GtfaResponse = {
data: GtfaItem[];
paginationToken: string | null;
};
type GtfaApi = {
getTransactionsForAddress(address: string, config?: GtfaConfig): GtfaResponse;
};
// ---- Client -------------------------------------------------------------
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name} in .env`);
return value;
}
const endpoint = requireEnv("QN_ENDPOINT");
export const walletAddress = requireEnv("WALLET_ADDRESS");
const api = createJsonRpcApi<GtfaApi>({
responseTransformer: (response: any) => {
// Surface JSON-RPC errors instead of returning undefined
if (response.error) {
throw new Error(`RPC error ${response.error.code}: ${response.error.message}`);
}
return response.result;
},
});
const transport = createDefaultRpcTransport({ url: endpoint });
export const gtfaRpc = createRpc({ api, transport });
Those types mirror the method's full parameter set, which is worth having in one place:
| Parameter | Values | Default |
|---|---|---|
transactionDetails | signatures, full | signatures |
sortOrder | asc, desc | desc |
limit | 1 to 1000 (higher values are capped) | 1000 |
paginationToken | Cursor from a prior response, formatted slot:transactionIndex | None |
commitment | confirmed, finalized (nothing lower) | None |
minContextSlot | Minimum slot at which to evaluate the request | None |
encoding | json, jsonParsed, base58, base64 (full mode only) | json |
maxSupportedTransactionVersion | Integer, full mode only. Omit and only legacy transactions return | None |
filters.slot | gte, gt, lte, lt | None |
filters.blockTime | gte, gt, lte, lt, eq (Unix seconds) | None |
filters.signature | gte, gt, lte, lt (positional, base-58) | None |
filters.status | any, succeeded, failed | any |
filters.tokenAccounts | none, balanceChanged, all | none |
This helper does three things defines request and response types that mirror the method reference, reads your endpoint and target wallet from .env, and exports a ready-to-use gtfaRpc client.
Make a First Call
Start with the method's defaults to see the baseline response shape. Create first-call.ts:
import { gtfaRpc, walletAddress } from "./gtfa.js";
const result = await gtfaRpc
.getTransactionsForAddress(walletAddress, { limit: 10 })
.send();
for (const tx of result.data) {
const status = tx.err === null ? "ok" : "FAILED";
console.log(`${tx.slot}:${tx.transactionIndex} ${status} ${tx.signature}`);
}
console.log(`\npaginationToken: ${result.paginationToken}`);
Run it:
npx tsx --env-file=.env first-call.ts
Signatures, slots, and the pagination token will differ for your own wallet.
Expected results:
440303193:1866 ok 3xTVgD66gwavvNfMeS6PeQKJYEpSgpSk7DKhQD8oGSGW5vts89xM6mCDckkQZftrQXXyxptBJ2YvbcAb4CFVbEAW
440303193:1865 ok 5o7p4sFu29mHqdKfPdgzf8kTGJGZ1vRHnJVDMmF1MsPKZBiuKJnJSY4ukoemHAXs58PhMjTZgRfvD4QUmo8EPxF4
...
paginationToken: 440301811:203
Each item carries the same fields getSignaturesForAddress returns (signature, slot, err, memo, blockTime, confirmationStatus) plus transactionIndex, the transaction's position inside its block. Paired with the slot, slot:transactionIndex gives every transaction an exact position in history, which is also the format of the paginationToken at the bottom of the response.
These results only contain transactions that reference the wallet address directly, so token transfers in and out of the wallet's token accounts are missing. The next section shows how to retrieve them.
Include Token Account Activity
The filters.tokenAccounts option controls whether transactions touching the wallet's owned token accounts are part of the result. It has three settings:
none(default): only transactions that reference the address itselfbalanceChanged: also include transactions that changed a balance in a token account the address ownsall: also include every transaction that touched any token account the address owns, even without a balance change
For a wallet history feed, balanceChanged is the setting you want. It adds deposits, withdrawals, and swaps involving the wallet's tokens, and excludes transactions that touched a token account without moving a balance, such as an account creation or a delegate approval.
Create token-accounts.ts to see the difference on your wallet:
import { gtfaRpc, walletAddress } from "./gtfa.js";
// Bound both queries to the same 30-day window so the counts are comparable.
// Without a shared window, each query returns its own page of up to 1,000
// results covering a different slot range, and the numbers mean nothing.
const THIRTY_DAYS_AGO = Math.floor(Date.now() / 1000) - 30 * 86400;
async function countWindow(tokenAccounts: "none" | "balanceChanged") {
const result = await gtfaRpc
.getTransactionsForAddress(walletAddress, {
limit: 1000,
filters: {
tokenAccounts,
blockTime: { gte: THIRTY_DAYS_AGO },
},
})
.send();
return result.data.length;
}
const walletOnly = await countWindow("none");
const withTokens = await countWindow("balanceChanged");
console.log(`Last 30 days, wallet address only: ${walletOnly} transactions`);
console.log(`Last 30 days, with token accounts: ${withTokens} transactions`);
Run it:
npx tsx --env-file=.env token-accounts.ts
Expected results:
Last 30 days, wallet address only: 38 transactions
Last 30 days, with token accounts: 212 transactions
For any wallet that holds tokens, the second number is larger, often several times larger. Every transaction in that difference is one a getSignaturesForAddress-based history silently drops.
Both queries use the same 30-day window on purpose. Comparing two unbounded pages would compare different slot ranges, and on a busy wallet both would simply return the 1,000-result maximum. If your wallet returns 1,000 for both, narrow the window until it comes in under the cap.
tokenAccounts defaults to none. If you copy a minimal example into a wallet history feature and never set this filter, the feature will look correct and be wrong. Set balanceChanged for anything user-facing.
Fetch Full Transactions
So far the responses contain signatures. To render a history feed you need what each transaction did, and the old answer was one getTransaction call per signature. Setting transactionDetails: "full" returns the complete transaction and meta objects inline instead.
Create full-history.ts:
import { gtfaRpc, walletAddress } from "./gtfa.js";
const result = await gtfaRpc
.getTransactionsForAddress(walletAddress, {
transactionDetails: "full",
maxSupportedTransactionVersion: 0, // required for v0 transactions
encoding: "jsonParsed",
limit: 25,
filters: { tokenAccounts: "balanceChanged" },
})
.send();
for (const tx of result.data) {
const signature = tx.transaction.signatures[0]; // moved: no top-level field in full mode
const status = tx.meta.err === null ? "ok" : "FAILED"; // moved: err lives in meta
const fee = tx.meta.fee;
const cu = tx.meta.computeUnitsConsumed;
console.log(`${tx.slot}:${tx.transactionIndex} ${status} v=${tx.version} fee=${fee} cu=${cu} ${signature}`);
}
Run it:
npx tsx --env-file=.env full-history.ts
Expected results:
440303193:1866 ok v=0 fee=5000 cu=42315 3xTVgD66gwavvNfMeS6PeQKJYEpSgpSk7DKhQD8oGSGW5vts89xM6mCDckkQZftrQXXyxptBJ2YvbcAb4CFVbEAW
440302871:412 ok v=0 fee=5000 cu=118204 5o7p4sFu29mHqdKfPdgzf8kTGJGZ1vRHnJVDMmF1MsPKZBiuKJnJSY4ukoemHAXs58PhMjTZgRfvD4QUmo8EPxF4
440301544:88 FAILED v=0 fee=5000 cu=9821 2vNJ8bXsPk4mQhRtL6yWcE3zAdFgHnJkMpQrStUvWxYz1aB2cD3eF4gH5iJ6kL7mN8oP9qR1sT2uV3wX4yZ5aB6c
...
Each item now carries transaction (accounts and instructions, parsed because of encoding: "jsonParsed"), meta (fee, pre/post balances, pre/post token balances, inner instructions, log messages, and compute units), and version. That is everything a history row, a balance-change calculation, or a debugging session needs, with zero follow-up calls.
The two modes do not return the same fields, and two fields move rather than disappear:
| Field | transactionDetails: signatures | transactionDetails: full |
|---|---|---|
slot | Yes | Yes |
transactionIndex | Yes | Yes |
blockTime | Yes | Yes |
signature | Yes | Moves to transaction.signatures[0] (json and jsonParsed encodings) |
err | Yes | Moves to meta.err |
memo | Yes | Not returned |
confirmationStatus | Yes | Not returned |
transaction | Not returned | Yes |
meta | Not returned | Yes |
version | Not returned | Yes |
Full mode has two behaviors that will cost you debugging time if you miss them:
If you omit maxSupportedTransactionVersion, the method returns an error whenever it encounters a versioned transaction. Version 0 transactions have accounted for the majority of Solana Mainnet activity since address lookup tables shipped in late 2022, so set it to 0 in every full-mode request. For background on the versioned transactions, see How to Use Versioned Transactions on Solana.
Page Through Complete History
A single page holds up to 1,000 results. Active wallets have more history than that, which is what the paginationToken is for: pass the token from one response into the next request, and stop when it comes back null. An async generator wraps that loop once so every later script can reuse it. Add this to the bottom of gtfa.ts:
// ---- Pagination helper --------------------------------------------------
export async function* walkTransactionsForAddress(
address: string,
config: Omit<GtfaConfig, "paginationToken"> = {},
startToken?: string,
) {
let paginationToken: string | null | undefined = startToken;
do {
const page: GtfaResponse = await gtfaRpc
.getTransactionsForAddress(address, { ...config, paginationToken })
.send();
yield page; // caller gets data plus the token, useful for checkpointing
paginationToken = page.paginationToken;
} while (paginationToken);
}
The generator yields whole pages rather than individual transactions so callers can persist page.paginationToken as a checkpoint, which the backfill recipe below relies on. Fetching a wallet's entire history now looks like this, in all-history.ts:
import { walkTransactionsForAddress, walletAddress } from "./gtfa.js";
let total = 0;
for await (const page of walkTransactionsForAddress(walletAddress, {
limit: 1000,
filters: { tokenAccounts: "balanceChanged" },
})) {
total += page.data.length;
console.log(`+${page.data.length} (total ${total}) next=${page.paginationToken}`);
}
console.log(`\nComplete history: ${total} transactions`);
Run it:
npx tsx --env-file=.env all-history.ts
Expected results:
+1000 (total 1000) next=440301811:203
+1000 (total 2000) next=439877402:112
+412 (total 2412) next=null
Complete history: 2412 transactions
The paginationToken encodes a position within one specific query. Reusing it with different filters, a different sort order, or a different address produces meaningless results. Change any part of the query and start from a fresh request.
Filter Recipes
The recipes below combine everything used so far into four production jobs that were previously impractical, each as a short variation on the helpers you already have.
Resume an Indexer Backfill from a Checkpoint
getSignaturesForAddress walks backward only, so replaying an account's history forward into a database meant paging to the very beginning and buffering everything in memory, with a crash sending you back to the start. Ascending sort plus a persisted cursor turns the same job into a flat-memory loop that survives restarts:
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
import { walkTransactionsForAddress, walletAddress } from "./gtfa.js";
const CHECKPOINT_FILE = "checkpoint.txt";
const startToken = existsSync(CHECKPOINT_FILE)
? readFileSync(CHECKPOINT_FILE, "utf8").trim()
: undefined;
for await (const page of walkTransactionsForAddress(
walletAddress,
{
sortOrder: "asc", // oldest first: forward replay
transactionDetails: "full",
maxSupportedTransactionVersion: 0,
limit: 1000,
filters: { tokenAccounts: "balanceChanged" },
},
startToken,
)) {
// ... write page.data to your database here ...
console.log(`Indexed ${page.data.length} transactions`);
if (page.paginationToken) {
writeFileSync(CHECKPOINT_FILE, page.paginationToken); // resume point
}
}
// Clear the checkpoint so a rerun starts fresh instead of replaying the last page
if (existsSync(CHECKPOINT_FILE)) unlinkSync(CHECKPOINT_FILE);
console.log("Backfill complete");
Run it:
npx tsx --env-file=.env backfill.ts
Expected results:
Indexed 1000 transactions
Indexed 1000 transactions
Indexed 412 transactions
Backfill complete
Generate a Fixed-Window Statement
Monthly statements, accounting exports, and activity reports need everything inside a date range and nothing outside it. filters.blockTime bounds the query server-side, so the cost tracks the window's contents rather than how far in the past it sits:
import { walkTransactionsForAddress, walletAddress } from "./gtfa.js";
const MARCH_1_2026 = 1772323200; // Unix seconds, UTC
const APRIL_1_2026 = 1775001600;
let count = 0;
let failed = 0;
for await (const page of walkTransactionsForAddress(walletAddress, {
sortOrder: "asc",
limit: 1000,
filters: {
blockTime: { gte: MARCH_1_2026, lt: APRIL_1_2026 },
tokenAccounts: "balanceChanged",
},
})) {
count += page.data.length;
failed += page.data.filter((tx) => tx.err !== null).length;
}
console.log(`March 2026: ${count} transactions (${failed} failed)`);
Run it:
npx tsx --env-file=.env statement.ts
Expected results:
March 2026: 184 transactions (6 failed)
Audit a Slot Range
Incident reviews and dispute resolutions need everything an account did between two specific slots. The legacy cursor is signature-based, so answering that used to start with a search for a signature near the boundary slot. filters.slot bounds it directly. Substitute a slot from your own first-call.ts output for the range below.
import { walkTransactionsForAddress, walletAddress } from "./gtfa.js";
// Replace these with a slot from your own first-call.ts output. The lower
// bound below is roughly a day of Solana slots before the upper bound.
const AUDIT_END_SLOT = 440303193;
const AUDIT_START_SLOT = AUDIT_END_SLOT - 216000;
for await (const page of walkTransactionsForAddress(walletAddress, {
sortOrder: "asc",
limit: 1000,
filters: {
slot: { gte: AUDIT_START_SLOT, lte: AUDIT_END_SLOT },
tokenAccounts: "balanceChanged", // an audit that drops token transfers is not an audit
},
})) {
for (const tx of page.data) {
console.log(`${tx.slot}:${tx.transactionIndex} ${tx.signature}`);
}
}
Run it:
npx tsx --env-file=.env slot-audit.ts
Expected results:
440087312:204 4KcTn8sVqYbGpLm2WxRdF6hJ3zAeNuPvQtSrUwXyZ1aB2cD3eF4gH5iJ6kL7mN8oP9qR1sT2uV3wX4yZ5aB6c
440091887:1533 3xTVgD66gwavvNfMeS6PeQKJYEpSgpSk7DKhQD8oGSGW5vts89xM6mCDckkQZftrQXXyxptBJ2YvbcAb4CFVbEAW
440091887:1534 5o7p4sFu29mHqdKfPdgzf8kTGJGZ1vRHnJVDMmF1MsPKZBiuKJnJSY4ukoemHAXs58PhMjTZgRfvD4QUmo8EPxF4
...
The transactionIndex on each result means the output is a total ordering. Two transactions in the same slot are listed in their execution order, which matters when an audit hinges on which came first.
Investigate Failed Transactions
When a bot or a program-owned account starts failing, the transactions you need are buried among thousands of successful ones. Filtering by status and requesting full details returns just the failures, logs attached:
import { walkTransactionsForAddress, walletAddress } from "./gtfa.js";
const DAY_AGO = Math.floor(Date.now() / 1000) - 86400;
for await (const page of walkTransactionsForAddress(walletAddress, {
transactionDetails: "full",
maxSupportedTransactionVersion: 0,
limit: 100,
filters: {
status: "failed",
blockTime: { gte: DAY_AGO },
tokenAccounts: "balanceChanged", // catch failures in the address's token accounts too
},
})) {
for (const tx of page.data) {
// logMessages is null when a validator has log collection off or truncated
const logs = tx.meta.logMessages ?? [];
console.log(`\n${tx.transaction.signatures[0]}`);
console.log(` error: ${JSON.stringify(tx.meta.err)}`);
console.log(` logs: ${logs.slice(-3).join("\n ") || "none returned"}`);
}
}
Run it:
npx tsx --env-file=.env failures.ts
Expected results:
4KcTn8sVqYbGpLm2WxRdF6hJ3zAeNuPvQtSrUwXyZ1aB2cD3eF4gH5iJ6kL7mN8oP9qR1sT2uV3wX4yZ5aB6c
error: {"InstructionError":[3,{"Custom":6001}]}
logs: Program log: Error: slippage tolerance exceeded
Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 84210 of 200000 compute units
Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 failed: custom program error: 0x1771
...
The last few log messages usually contain the program error behind each failure, which is what you need to diagnose an overnight outage. How to Track Solana Transaction History with Logs goes deeper on reading and filtering log output.
When to Use Streams or Solana gRPC Instead
getTransactionsForAddress is built for history: everything an address has already done, filtered and ordered on the server. It is a pull-based method, so it is the wrong tool for reacting to new activity the moment it happens. For that end of the timeline, Quicknode has two options:
- Quicknode Streams delivers filtered onchain data to a webhook, S3 bucket, or other destination as it lands, so nothing polls for new transactions. Reach for it when you want new activity pushed to your infrastructure.
- Solana gRPC streams accounts, transactions, and blocks over a persistent connection, and it is the lowest-latency option Quicknode offers. Trading systems and liquidation bots that need updates the moment they happen belong here.
Indexers can use use getTransactionsForAddress to backfill history (the checkpoint recipe above), and Streams or Solana gRPC to keep it current from there.
Wrapping Up
The gap getTransactionsForAddress closes here is about correctness. A history feed built on getSignaturesForAddress alone silently drops token transfers that no amount of extra calls will recover. The filter recipes are small enough to drop into real systems as-is, and the checkpoint pattern in the backfill recipe is the foundation of a production indexer. Point the scripts at your own wallets, wire the backfill into a database, and add Streams or Solana gRPC when you need the feed to stay current.
If you're stuck, have questions, or just want to talk about what you're building, drop us a line on Discord or Twitter!
Frequently Asked Questions
What is getTransactionsForAddress on Solana?
getTransactionsForAddress is a Quicknode RPC method that returns the transaction history for a Solana address in a single call, with optional server-side filtering by slot, block time, signature position, status, and token account activity. It replaces the traditional pattern of combining getTokenAccountsByOwner, getSignaturesForAddress, and per-signature getTransaction calls.
How is getTransactionsForAddress different from getSignaturesForAddress?
getSignaturesForAddress returns only signatures for transactions that directly reference the address, walks backward only, and offers no filtering. getTransactionsForAddress can include activity from token accounts the address owns and return full transaction payloads inline. It also filters by slot, time, and status on the server, sorts in either direction, and paginates with a cursor it returns to you.
Why are token transfers missing from my results?
The tokenAccounts filter defaults to none, which returns only transactions that reference the address itself. Set filters.tokenAccounts to balanceChanged to include transactions that changed a balance in a token account the address owns, which is the setting most wallet history features want.
Why does the method return an error about transaction versions?
In full mode, transactions above the maxSupportedTransactionVersion produce an error. Omitting the parameter supports only legacy transactions, and version 0 accounts for the majority of current Mainnet activity. Set maxSupportedTransactionVersion to 0 in every request that uses transactionDetails full.
Can I use getTransactionsForAddress for real-time monitoring?
getTransactionsForAddress is designed for historical queries, not live delivery. For real-time data, use Quicknode Streams for reliable push delivery of filtered onchain data, or Solana gRPC for the lowest-latency streaming of accounts, transactions, and blocks. A common architecture backfills history with getTransactionsForAddress and stays current with Streams or Solana gRPC.
How do I get a Solana wallet's full transaction history?
Call getTransactionsForAddress with the wallet address and set filters.tokenAccounts to balanceChanged so token account activity is included. Set transactionDetails to full for decoded transactions, then pass the returned paginationToken back on each subsequent request until it comes back null.
