Looking Up Historical Solana Transactions

How far back an RPC actually goes, paginating with before and until, and why reconciliation requires storing your own records.

BoltTx Team··8 min read
solanahistorygetSignaturesForAddressreconciliationrpcaccounting

Every Solana transaction is permanently recorded. That does not mean your RPC will serve it to you.

★Standard RPC nodes prune old data. The chain remembers; the endpoint you are querying usually does not.★ Teams discover this at the worst time — when reconciling a month-end report and finding the earliest transactions return null.

Two Calls, Two Jobs

Find signatures for an address:

const sigs = await connection.getSignaturesForAddress(wallet, {
  limit: 1000,                    // ★maximum per call★
});

sigs.forEach((s) => {
  console.log(s.signature, s.slot, s.blockTime, s.err ? "failed" : "ok");
});

Fetch one transaction's detail:

const tx = await connection.getTransaction(signature, {
  maxSupportedTransactionVersion: 0,   // ★or versioned transactions return null★
});

★That option is not optional in practice.★ Without it, any v0 transaction comes back as null, which reads as "this transaction does not exist" rather than "your client did not declare it can parse this format."

Paginating Backwards

getSignaturesForAddress returns newest first, capped at 1000. To walk further back, page with before:

async function allSignatures(address, connection, stopAtSlot = 0) {
  const out = [];
  let before = undefined;

  while (true) {
    const page = await connection.getSignaturesForAddress(address, {
      limit: 1000,
      before,                        // ★signature, not slot★
    });
    if (!page.length) break;

    out.push(...page.filter((s) => s.slot >= stopAtSlot));
    const last = page[page.length - 1];
    if (last.slot < stopAtSlot) break;

    before = last.signature;         // ★continue from here★
    await sleep(200);                // ★this call is heavy★
  }
  return out;
}

before and until take signatures, not slots.★ Passing a slot number silently returns nothing useful, which is a confusing failure because the call succeeds.

until is the one worth using for incremental sync. Store the newest signature you have processed, pass it as until, and each run fetches only what is new rather than re-walking history.

If your history queries are fine and the problem is transactions landing, a free BoltTx key is one line to test the submission path.

How Far Back You Can Actually Go

This is the part that determines your architecture:

Node type Typical retention
Standard RPC ★recent slots only★
Extended-history provider months
Archive node ★full history★
Your own database ★whatever you stored★

★A standard endpoint holding only recent history is the default, not a defect.★ Storing the full chain is expensive, so most providers prune and offer deeper history as a separate tier.

The consequence for anything financial: you cannot rely on the RPC as your system of record. A tax report, a P&L, or a customer dispute six months later all need data you kept yourself.

// ★Store as you go, not when you need it.★
await db.transactions.insert({
  signature: sig,
  slot: outcome.slot,
  blockTime: outcome.blockTime,
  err: outcome.err,
  meta: extractWhatYouNeed(tx),
});

Reading Balance Changes From Meta

The most useful part of a historical transaction is not the instructions — it is what actually moved:

const pre  = tx.meta.preTokenBalances ?? [];
const post = tx.meta.postTokenBalances ?? [];

for (const p of post) {
  const before = pre.find((b) => b.accountIndex === p.accountIndex);
  const delta =
    BigInt(p.uiTokenAmount.amount) -
    BigInt(before?.uiTokenAmount.amount ?? "0");
  if (delta !== 0n) console.log(p.mint, delta);
}

preTokenBalances and postTokenBalances tell you what changed without decoding a single instruction.★ For reconciliation this is far more reliable than parsing instruction data, because it reflects the actual result including anything that happened inside a CPI.

For native SOL, preBalances and postBalances do the same by account index. Remember the fee comes out of the fee payer's delta, so the difference for accountKeys[0] includes it.

Failed Transactions Are In There Too

getSignaturesForAddress returns failed transactions alongside successful ones, and err distinguishes them:

const failed = sigs.filter((s) => s.err !== null);

★A failed transaction still landed and still consumed a base fee.★ Reconciliation that ignores them will not match the wallet balance, because the fees were spent on transactions that changed nothing else.

Note what is not in this list: transactions that never landed. Those left no record anywhere, which is precisely why your own logs matter — the chain cannot tell you about an attempt that expired.

Rate Limits and This Call

Both methods are expensive, and history backfills are a common way to exhaust a quota:

Page deliberately. A limit: 1000 call is one request; a thousand getTransaction calls to enrich it are a thousand more.

Fetch details only for what you need. The signature list already carries slot, block time, and error status. If that is enough, do not fetch the full transaction.

Use until for incremental runs. ★Re-walking the same history on every run is the single most common cause of a backfill hitting rate limits.★

Backfill separately from trading. A history job and a bot sharing an endpoint means the backfill can starve the sends.

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

★Only landed transactions have a history at all.★ An expired transaction leaves no on-chain trace, so the reliability of your submission path determines how much of your activity is even reconstructable later.

Where BoltTx Fits

We handle submission, not indexing or history. Keep whatever you use for lookups.

Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. Because the endpoints are independent, a history backfill cannot exhaust the path your transactions go out on.

You sign locally. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.

Get a free API key. No monthly fee:

const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

How far back can I query Solana transactions? On a standard RPC, only recent history — older data is pruned. Full history requires an archive node or a provider tier that offers extended retention.

Why does getTransaction return null for an old transaction? Either the node pruned it, or you omitted maxSupportedTransactionVersion: 0 and it is a versioned transaction. Check the second before assuming the first.

How do I paginate getSignaturesForAddress? Pass the last signature of the previous page as before. It returns newest first with a maximum of 1000 per call, so walking further back means repeated calls.

What is the difference between before and until? before walks backwards from a signature, while until stops at one. Use until with your last processed signature for incremental sync so you fetch only what is new.

Can I pass a slot number to before or until? No, both expect signatures. Passing a slot returns nothing useful while the call still succeeds, which makes it a confusing failure to diagnose.

How do I see what a transaction actually moved? Compare preTokenBalances with postTokenBalances from the meta, and preBalances with postBalances for native SOL. That reflects the real outcome including CPI effects.

Does the fee appear in the balance deltas? Yes, in the fee payer's native balance change. The difference for accountKeys[0] includes the fee, so subtract it when attributing the movement to a trade.

Are failed transactions returned in the signature list? Yes, with err set. They landed and consumed a base fee, so reconciliation that skips them will not match the wallet balance.

Can I find transactions that never landed? No. An expired transaction leaves no on-chain record anywhere, which is why your own submission logs are the only source for those attempts.

Should I rely on the RPC for accounting records? No. Standard nodes prune, so anything you need months later must be stored as it happens. Treat the RPC as a live source, not a system of record.

How do I backfill history without hitting rate limits? Page deliberately, fetch full transaction details only when the signature metadata is insufficient, and use until so each run covers only new activity.

What does the signature list include without fetching details? Signature, slot, block time, error status, and memo. If that answers your question, skip getTransaction entirely and save a request per signature.

Why does my reconciliation not match the wallet balance? Common causes are ignoring failed transactions that still paid fees, missing the fee in the fee payer's delta, or history the RPC pruned before you stored it.

Is blockTime always available? Usually, but it can be null for some older entries. Slot is always present and monotonic, so prefer it for ordering and use block time for display.

How many signatures can one call return? Up to 1000. Beyond that you must paginate with before, which makes deep history a sequence of requests rather than a single query.

Should history queries share an endpoint with my bot? Preferably not. A backfill is heavy and can exhaust a shared quota, which starves the sends at exactly the moment the bot needs them.

Back to all posts