A Solana swap transaction is a signed message containing an ordered list of instructions that execute atomically against a recent blockhash. The swap itself is usually not one of those signed instructions. A router program is invoked, and it calls a pool program through cross-program invocation, so the fills appear as inner instructions. The evidence of what was exchanged sits in the pre and post token balances.
That structure is why two people reading the same transaction can disagree about what happened. One reads the instruction list and sees a call to an aggregator. The other reads the metadata and sees two pool swaps, four token transfers and a fee. Both are looking at the same bytes. This note walks the layers in order, then reads one transaction end to end.
What a transaction contains
A transaction has two parts: an array of signatures and a message. The message carries a header, the ordered list of account addresses the transaction will touch, a recent blockhash, and the instruction list. Each instruction identifies its program by an index into that account list, names the accounts it reads or writes by index as well, and carries an opaque byte payload that only the target program knows how to decode.
The account list is the reason transactions have a size limit. The Solana documentation puts the ceiling for a legacy transaction at 1232 bytes, and every address occupies 32 bytes. A route touching several pools can therefore run out of space before it runs out of logic. Versioned transactions address this with address lookup tables, which store addresses on chain once and let a transaction reference them by a compact index.
Two properties of the message matter for anyone reading swaps. Execution is atomic: if any instruction fails, every state change in the transaction is discarded. And the recent blockhash gives the transaction a limited lifetime, so a swap that sits unprocessed for too long expires rather than executing late at a stale price. Both properties show up constantly in failure analysis.
What the message does not contain is any notion of a trade. There is no order type, no side field, no price. The chain has no concept of a swap at the protocol level. A swap is an emergent description of what a particular set of programs did to a particular set of balances, and every parser you use is reconstructing that description from raw execution.
Instructions, inner instructions and why the swap hides inside a CPI
The top-level instruction list is exactly what the sender signed, and it is usually short. A typical Solana swap contains one or two compute budget instructions, sometimes an instruction creating a token account for a mint the wallet has never held, and then a single instruction addressed to a router or aggregator program whose data field encodes the entire route. Nothing in that list looks like a trade.
The router instruction then executes, and while executing it calls other programs. Solana names this cross-program invocation, and the runtime records each nested call as an inner instruction in the transaction metadata, indexed against the top-level instruction that produced it. The pool program's swap handler appears there. So do the token program transfers the pool performs. Invocation depth is bounded, which is why routes are flat rather than deeply nested.
This is the single most consequential fact for anyone building or trusting a volume figure. An indexer that reads only the signed instruction list sees a router call and no swap at all. An indexer that walks inner instructions sees one pool swap per hop. A two-hop route produces two pool-level executions inside one user action, which is precisely the ambiguity described in the note on how DEX volume is counted.
Inner instructions also explain why a swap can touch accounts the signer never listed by name. Programs derive addresses deterministically and sign for them programmatically, so pool vaults, authority accounts and fee accounts enter the transaction through the route data rather than through the wallet interface. If you are reconciling a transaction against a wallet's intent, the signed list tells you intent and the inner instructions tell you consequence.
Token accounts and the transfers that constitute the fill
Native SOL balances live on the account itself, denominated in lamports, of which there are one billion to a SOL. SPL token balances do not work that way. Each token balance lives in a separate token account owned by the token program, holding a mint, an owner and an amount. The usual form is an associated token account, whose address is derived deterministically from the owner and the mint.
A swap therefore moves value by transferring between token accounts rather than by editing a balance field on a wallet. In the simplest single-pool case there are two transfers and four balance changes: the trader's input token account is debited and the pool's vault for that mint is credited, then the pool's vault for the other mint is debited and the trader's output token account is credited. The pool fee is retained inside that arithmetic.
The runtime records these as pre and post token balance arrays in the transaction metadata. Each entry carries an account index, the mint, the owner and an amount together with its decimal precision. This is the ground truth for the fill. Log messages are program-authored strings and can change format between program versions; balance arrays are produced by the runtime and describe the committed state transition.
One wrinkle catches most first-time readers. When the input or output side is native SOL, many routers wrap it: a temporary wrapped-SOL token account is created, funded, used, then closed inside the same transaction, returning the remaining lamports to the wallet. The token balance arrays may then show a wrapped-SOL account appearing and vanishing, and the honest reading of the SOL side comes from the lamport balance change on the wallet, net of the fee.
The compute budget, compute units and priority fees
Every instruction consumes compute units as it executes, and the runtime meters that consumption. If a transaction exceeds its budget it is aborted. Per the Solana documentation, an instruction receives a default allocation of 200,000 compute units and a single transaction may not exceed 1.4 million. A route through several pools does real work in each of them and can approach that ceiling.
The ComputeBudget program exists to adjust these defaults. One instruction sets the compute unit limit for the transaction, raising it above the default allocation when a complex route needs headroom, or lowering it when the sender knows the true consumption. A second instruction sets a compute unit price, denominated in micro-lamports per compute unit. Together they determine the priority fee attached to the transaction.
The base fee is separate and is charged per signature, at a fixed rate documented in the Solana fee documentation as 5,000 lamports. The priority fee is the requested compute unit limit multiplied by the compute unit price, converted from micro-lamports to lamports. That is why lowering an inflated limit reduces cost: the fee is computed on what you requested, not on what you consumed. The point compounds for anything sending many transactions in sequence, so a per-transaction compute limit is a specification worth checking on any tool, including a SOL volume bot, before the setting is multiplied by a few thousand sends.
Illustrative arithmetic
The numbers in the next paragraph are invented purely to show how the two compute budget settings combine into a fee. They measure nothing real, describe no observed transaction, and should not be read as typical, recommended or achievable values.
Suppose a transaction requests a limit of 300,000 compute units at a price of 10,000 micro-lamports per unit. The priority component is 300,000 × 10,000 = 3,000,000,000 micro-lamports, which is 3,000 lamports. Add one signature at 5,000 lamports and the total fee is 8,000 lamports. Halving the requested limit to 150,000 halves only the priority component, giving 6,500 lamports.
Fees are trivial next to price impact on any meaningful order size, and that ordering of concerns is worth keeping. A reader who optimises fees while ignoring depth is optimising the smaller term. The arithmetic of the larger term is worked through in the note on price impact on thin pairs, where the cost of a single fill is set by reserves rather than by anything in the compute budget.
Why swaps fail and what a failure costs
A failed Solana transaction reverts all of its state changes and still pays the fee. Nothing moved, but the network expended work deciding that nothing should move, and the fee compensates that. In the metadata the status carries an error rather than a null, and the balance arrays show no net change for the signer beyond the fee itself. This is why failures must never enter a volume total.
- Slippage tolerance exceededThe route was quoted against reserves that changed before the transaction landed, and the program refused to fill outside the minimum output the sender encoded.
- Blockhash expiredThe transaction was not processed within the lifetime granted by its recent blockhash, so the runtime rejected it rather than executing at a stale price.
- Compute budget exhaustedThe route consumed more compute units than the transaction requested, usually on a long multi-hop path submitted with default limits.
- Missing destination token accountThe wallet held no token account for the output mint and the transaction did not include an instruction to create one.
- Insufficient lamportsThe wallet could not cover the fee, the rent-exempt reserve for a new token account, or the wrapped SOL amount the route required.
- Stale route dataA pool referenced in the encoded route changed state, was drained, or no longer offered the path the router selected when it built the instruction.
- Account write contentionThe transaction competed for write access to a hot account and was dropped by the scheduler rather than executed.
Failures are data, not noise. The ratio of landed transactions to submitted transactions describes execution quality, and it varies enormously by pool, by route length and by network conditions. Keep it as its own metric with its own denominator. Folding attempts into a volume figure produces a number that measures effort rather than settlement, which is the error the note on the anatomy of Solana trading volume spends most of its length dismantling.
Reading one transaction in an explorer, step by step
The following sequence works on any public explorer that exposes inner instructions and balance arrays; Solscan is one common choice. The aim is not to admire the interface but to end up holding a number you derived yourself, rather than one a dashboard derived for you.
- Start from the signaturePaste the transaction signature into the explorer search field. The signature is the base58 encoding of the first signature in the transaction and resolves to exactly one transaction on one cluster. Confirm you are on mainnet before reading anything else.
- Confirm status, slot and block timeCheck that the status reads success rather than an error. Note the slot, which is the chain's own ordering index, and treat the block time as an estimated wall-clock stamp. Bucketing by block time and bucketing by slot disagree at window boundaries.
- Read the fee and the compute units consumedThe fee is displayed in lamports or SOL and includes the base signature fee plus any priority component. Compare units consumed against the limit the transaction requested; a large gap means the sender paid for headroom it never used.
- Open the top-level instruction listRead the signed instructions in order. You will typically find one or two compute budget instructions, occasionally a token account creation, and one instruction to a router or pool program carrying the encoded route. This list is intent, not outcome.
- Expand the inner instructionsOpen the nested calls recorded under each top-level instruction. Here you will find the pool program invocations and the token program transfers that constitute the fill. Count the pool invocations: that count is the number of hops the route actually took.
- Read the pre and post token balancesLocate the balance arrays in the metadata view. Match entries by owner and mint, then compute the change for the signing wallet on both sides of the trade. Ignore the pool vault entries for this purpose; they are the mirror of the wallet's own change.
- Derive the executed priceDivide the absolute change in the quote asset by the absolute change in the base asset for the signing wallet. That single ratio is the price the transaction executed at, inclusive of pool fees and price impact, and it is the only price figure in the whole record that nobody estimated for you.
Illustrative arithmetic
The balances below are invented to demonstrate the final division and nothing more. They are not taken from any transaction, they describe no token, and the resulting price is meaningful only as a worked relationship between two balance changes.
Suppose the signing wallet's SOL balance falls by 2.005 SOL across the transaction, of which 0.005 SOL is the fee, and its balance of the traded mint rises by 40,000 units. The trade side of the SOL change is 2.000 SOL. Dividing gives 0.00005 SOL per unit, or 20,000 units per SOL. Any quoted price that differs from this is a quote, not an execution.
Doing this once by hand changes how you read aggregated data afterwards. Every dashboard figure is the sum of thousands of these derivations, each performed by a parser making the same choices you just made manually: which balance change to attribute to the trade, how to treat the fee, whether to count hops or user actions. The choices are ordinary. The disagreements they produce are not.
An annotated field table
The table below lists the fields that carry the substance of a swap, where each one lives, what it legitimately tells you, and the misreading each one most often invites. It is written against the structure of the transaction record rather than any particular explorer's layout, so the labels may differ slightly from the interface in front of you.
| Field or section | Where it appears | What it tells you | Common misreading |
|---|---|---|---|
| Signature | Top of the transaction record | A unique identifier for this transaction on this cluster | Treated as a wallet or account identifier rather than a transaction one |
| Slot | Confirmation section | The chain's own ordering index for when the transaction landed | Read as a timestamp; slots are ordinal, not evenly spaced in wall-clock terms |
| Block time | Confirmation section | An estimated wall-clock stamp for the block | Assumed exact, then used to bucket activity into minute windows without tolerance |
| Status | Header of the record | Whether state changes were committed or reverted | A failed transaction counted as activity because it still appears in the account history |
| Fee | Header, denominated in lamports | Base signature fee plus any priority component, charged either way | Confused with the pool's trading fee, which is taken inside the swap arithmetic instead |
| Compute units consumed | Execution metadata | Actual metered work, comparable against the requested limit | Read as a cost; the fee is charged on the requested limit, not on consumption |
| Instructions list | Signed message | What the sender authorised, in order | Searched for a swap that is not there, leading to the conclusion that none occurred |
| Inner instructions | Runtime metadata | Nested program calls, including every pool invocation and token transfer | Each hop of one route counted as an independent user trade |
| Pre and post token balances | Runtime metadata | Exact SPL balance changes by owner and mint, with decimals | Raw amounts compared without applying the mint's decimal precision |
| Pre and post SOL balances | Runtime metadata | Lamport changes per account, including the fee payer | The fee left inside the trade side when deriving an executed price |
| Log messages | Execution metadata | Program-authored strings describing what each program believed it did | Parsed as authoritative; logs can be truncated or reformatted between versions |
Read in this order, a transaction stops being an opaque hash and becomes an auditable record. The signed list gives intent. The inner instructions give mechanism. The balance arrays give outcome. The fee and compute fields give cost. Nothing else in the record is required to state what happened, and anything a dashboard adds beyond these fields is interpretation layered on top of them.
What readers ask about swap data
Why does the explorer show no swap in the instruction list?
Because the swap is performed by a program that the signed instruction calls. The router invokes the pool program through cross-program invocation, and the runtime records those nested calls as inner instructions in the metadata. The signed list holds the router call; the fill lives one layer down.
How do I tell a one-hop swap from a routed one?
Count the distinct pool program invocations in the inner instructions. One invocation is a direct swap against a single pool. Two or more means the router split or chained the order, and the intermediate token was genuinely bought and sold inside the same transaction.
Why do the token amounts look absurdly large?
Because raw amounts are integers scaled by the mint's decimal precision, which is carried alongside the balance in the metadata. A mint with six decimals reports one whole token as 1,000,000. Applying the wrong decimal count is the most common source of wildly wrong derived prices.
Does a higher priority fee improve my fill?
It improves the chance of landing sooner, which indirectly reduces the risk that reserves move against you before execution. It does not change the price the pool quotes for your size. Depth sets the price; the fee only influences when you get to ask.
Can I reconstruct volume from transactions alone?
Yes, and that is the honest way to do it, but it requires walking inner instructions, applying decimals correctly, pricing each leg in a chosen quote asset, and deciding explicitly whether to count hops or user actions. The decisions are unavoidable; publishing them alongside the number is what makes it defensible.
What should I record when I archive a transaction?
Signature, slot, status, fee, the signer's balance deltas on both sides with decimals applied, the count of pool invocations, and the programs involved. That set is enough to re-derive the executed price later without trusting any intermediary's parsing of the same transaction.
Filed under Market structure. Corrections and method questions go to the desk; our sourcing rules are on the editorial policy page.