Your Position Ledger Versus What the Chain Says

A bot's internal record drifts from on-chain reality in specific, predictable ways. Where the gaps come from and how to reconcile them.

BoltTx Team··9 min read
solanaposition-trackingreconciliationaccountingtrading-bottransaction-landing

Your bot thinks it holds 1,000 tokens. The chain says 987. Neither number is a bug in the usual sense — they diverged for reasons that were correct at every individual step.

★The chain is the only authority. Your ledger is a cache of it, and every cache drifts.★

Where the Drift Comes From

Cause Direction
★Recorded intent, not outcome★ ★Ledger too high★
★Transfer fee withheld★ ★Ledger too high★
Slippage — filled worse than expected Ledger too high
★Reverted transaction counted★ ★Ledger too high★
Unlogged manual transaction Ledger too low
★Retry that landed after you gave up★ ★Ledger too low★

★Notice that most causes push the same direction.★ A bot recording what it intended rather than what happened accumulates an optimistic position, which is the worse error — you attempt to sell tokens you do not have.

Record Outcomes, Not Intentions

// ★Wrong: the signature means the RPC accepted bytes.★
const sig = await connection.sendRawTransaction(raw);
positions.add(mint, expectedAmount);

// ★Right: read what actually moved.★
const outcome = await resolve(sig, lastValidBlockHeight);
if (outcome.status !== "success") return;

const tx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
const delta = tokenDelta(tx.meta, myTokenAccount);   // ★from pre/post balances★
positions.add(mint, delta);

★preTokenBalances and postTokenBalances are the authoritative record of what changed★, and they account for everything automatically — slippage, transfer fees, and anything a CPI did internally.

Using them removes four of the six drift causes at once, because you stop predicting and start reading.

If your accounting is correct and transactions still miss, a free BoltTx key is one line to test the submission path.

The Three-Outcome Rule Applies Here Too

switch (outcome.status) {
  case "success":  applyDelta(readDeltaFromMeta(sig)); break;
  case "reverted": recordFeeOnly(sig);                 break;   // ★no position change★
  case "expired":  recordNothing();                    break;   // ★never executed★
}

★A reverted transaction landed and cost a fee but changed no position.★ Counting it as a fill is the most common single source of drift, and it compounds silently across many trades.

An expired transaction is the opposite trap — it never executed, so recording it as a failure is correct, but only if you confirmed expiry rather than assuming it from a timeout.

Reconcile Against the Chain on a Schedule

async function reconcile(connection, wallet) {
  const [classic, t22] = await Promise.all([
    connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_PROGRAM_ID }),
    connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_2022_PROGRAM_ID }),
  ]);

  const onChain = new Map();
  for (const { account } of [...classic.value, ...t22.value]) {
    const info = account.data.parsed.info;
    onChain.set(info.mint, BigInt(info.tokenAmount.amount));   // ★string, exact★
  }

  for (const [mint, believed] of ledger) {
    const actual = onChain.get(mint) ?? 0n;
    if (actual !== believed) await flagDiscrepancy(mint, believed, actual);
  }
}

★Two details make this correct rather than approximately correct.★

Query both token programs. A single-program query silently omits Token-2022 balances, which reads as a missing position rather than an incomplete query.

Compare in raw units as bigint. Using uiAmount introduces float error above 2^53, which produces phantom discrepancies on large balances and hides real ones.

Which Direction the Gap Points

★The sign of the discrepancy tells you where to look, which is more useful than the magnitude.★

Ledger higher than chain: you recorded something that did not happen. Look for reverted transactions counted as fills, or intent recorded before outcome.

Ledger lower than chain: something happened that you did not record. ★Look for a retry that landed after you gave up, or a transaction signed by your key that your bot did not initiate.★

That second case deserves an alert, not a correction. A position you did not create has one benign explanation — a late retry — and one that is not, and the reconciliation is where you find out which.

Reconcile SOL Separately

Native SOL needs its own treatment, because fees come out of the same balance:

const lamports = await connection.getBalance(wallet);
const expected = lastKnown - feesSpent + inflows - outflows;

★A SOL discrepancy that matches your fee spend is not a discrepancy★ — it is your accounting failing to subtract fees. Track fees as a separate line so trading movements and fee movements are distinguishable.

Also remember wrapped SOL is a token account, not part of this balance. A bot that wraps and unwraps needs both tracked, and needs to know they do not move together.

What to Do With a Discrepancy

★Never silently overwrite your ledger with the chain value.★ That fixes the number and destroys the evidence of why it was wrong.

The sequence that preserves information:

1. Record both values and the timestamp. The history of discrepancies is how you find a systematic cause.

2. Attempt attribution. Fetch recent signatures and look for a transaction your ledger does not know about.

3. Alert if unattributable. ★A gap you cannot explain is a different problem from a gap you can.★

4. Then correct, with the chain as the authority, and a record of the correction.

5. Halt trading on that position if the gap is large. Trading against a position you cannot verify is how a small accounting error becomes a large one.

What Landing Looks Like

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

★Predictable landing reduces the ambiguous window where a transaction's fate is unknown.★ Most accounting drift starts with a transaction whose outcome was unclear long enough that someone recorded a guess.

Where BoltTx Fits

We handle submission. Position tracking and reconciliation stay entirely in your code.

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.

You sign locally. We never hold funds, never sign, and never modify transaction contents — ★which is also why a transaction in your wallet's history that you did not initiate cannot have come from us.★ 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 connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

Why does my bot's position differ from the chain? Usually because it recorded intent rather than outcome. Slippage, transfer fees, and reverted transactions all make the actual result differ from what was expected.

How do I record what a transaction actually did? Read preTokenBalances and postTokenBalances from the transaction meta. They reflect the real result including CPI effects, slippage, and any fee withheld during transfer.

Should I update my ledger when I get a signature? No. A signature means the RPC accepted the bytes. Update only after resolving to a terminal state and reading the actual balance change.

How do I handle reverted transactions in accounting? Record the fee and no position change. Counting a revert as a fill is the single most common source of drift, and it compounds silently.

How often should I reconcile against the chain? Frequently enough that a discrepancy is caught in minutes rather than days. Anything trading continuously benefits from a check every few minutes.

Why does my reconciliation miss some tokens? Probably because you query only one token program. Token-2022 balances require a separate query, and their absence reads like a missing position.

Should I compare balances as numbers or strings? As bigint from the raw amount string. Using uiAmount introduces float error above 2^53, which creates phantom discrepancies and hides real ones.

What does it mean if my ledger is higher than the chain? You recorded something that did not happen. Look for reverted transactions counted as fills, or a position updated on submission rather than on outcome.

What does it mean if my ledger is lower than the chain? Something happened you did not record. Usually a retry that landed after you gave up, but it can also be a transaction signed by your key that your bot did not initiate.

Should I auto-correct discrepancies? Not silently. Record both values first, attempt attribution, alert if unexplainable, then correct with the chain as authority and keep a record of the correction.

When should a discrepancy stop trading? When it is large enough that trading against the position could compound the error. Trading against a balance you cannot verify turns a small problem into a large one.

Why does my SOL balance never match? Because fees come out of the same balance as trading. Track fee spend as a separate line, or every fee looks like an unexplained discrepancy.

Is wrapped SOL part of my SOL balance? No, it is a separate token account. They do not move together, so a bot that wraps and unwraps must track both independently.

How do transfer fees affect position tracking? The amount arriving is less than the amount sent, so a ledger recording the sent amount is permanently too high. Reading the post-balance handles it automatically.

Can I reconstruct positions from chain history alone? Mostly, using signatures and balance deltas, but only for transactions that landed. Anything about intent or attempts that expired exists only in your own logs.

What is the most valuable reconciliation alert? On-chain activity your ledger cannot explain. It has one benign cause and one that is not, and that is exactly the distinction worth waking someone for.

← Back to all posts