To decode a swap on Solana, read the transaction's balance metadata rather than its logs. The pre and post token balance arrays record what the runtime observed for every token account the transaction touched, and the lamport balance arrays do the same for native SOL. Diff those, group the result by owner, and you have the trade. Log lines and instruction arguments are supporting evidence, not the source.

This ordering matters because the alternatives look easier. Parsing a log string is three lines of code and works until a program author rewrites a message. Reading instruction arguments looks authoritative and gives you the requested amount rather than the filled one. Balance deltas are slightly more work and are the only source produced by the runtime itself.

Solana has no event log

On an EVM chain, a contract emits a typed event with an ABI-defined structure, and the node stores it as a first-class part of the receipt. Solana has nothing equivalent. Programs can print text into a per-transaction log buffer, and frameworks can serialise structures into that buffer, but the runtime neither defines nor validates the shape.

What the runtime does record is state change. For every account involved, it captures the lamport balance before and after, and for SPL token accounts it captures the token balance before and after with the mint, the owner and the decimals. That record exists whether or not any program logged anything, and it cannot be rewritten by a developer changing a string.

So the mental model to carry is accounting rather than messaging. You are not looking for an announcement that a swap happened; you are looking at a ledger of what moved and inferring the swap from it. That inversion is the single biggest step in writing a decoder that does not need rewriting every quarter.

Three sources, ranked by trust

SourceProduced byTrustUse it for
Pre and post balances in metadataThe runtimeHighest; cannot be shaped by program authorsEvery amount you will aggregate
Inner instructionsThe runtime, recording actual invocationsHigh; structure is reliable, meaning needs a layoutAttribution, venue identification, call structure
Anchor event payloadsThe program, via a serialised log or self-invocationMedium; typed but self-reported and truncatableSemantic detail such as pool ids and fee splits
Free-form log textThe program authorLowest; no contract, no versioningHuman diagnosis only

A decoder that works from the top of this table down degrades gracefully. If an Anchor layout is missing, you lose detail and keep the amounts. A decoder that works from the bottom up loses everything the first time a string changes, and it does so silently.

Reading pre and post token balances

The metadata attached to a fetched transaction contains two arrays describing SPL token positions. Each entry identifies the account by its index in the transaction's account list and carries the mint, the owner, the token program that owns the account, and the amount expressed both in base units and as a decimal convenience value.

Always compute from the base unit amount, never from the decimal convenience field. Base units are integers and are exactly what the ledger stores; the decimal representation is a rendering that invites floating point error into a figure that should be exact. Convert to human units at display time and never before.

The two arrays are not guaranteed to be the same length or to be aligned. An account created during the transaction has no pre entry, and an account closed during the transaction may have no post entry. Match entries by account index and treat a missing side as zero, which handles both cases without special logic.

Group by owner, not by account

A single wallet can hold several token accounts for the same mint, and a routed trade may touch more than one of them. Aggregating deltas by owner and mint before you interpret anything turns a confusing set of small movements into one clean position change, and it removes an entire class of double counting that is otherwise very hard to spot.

The native SOL side and wrapped SOL

Native SOL does not appear in the token balance arrays. It appears in two parallel lamport arrays indexed by the same account list, and the difference between them is the change in native balance for each account. Three details make this trickier than it looks.

First, the fee payer's change includes the transaction fee, which is recorded separately in the metadata. Subtract the fee before interpreting the fee payer's movement, otherwise every trade appears slightly larger or smaller on the SOL side than it was.

Second, rent matters. Creating a token account locks lamports for rent exemption and closing it releases them, so a transaction that opens and closes a temporary account moves native balance for reasons that have nothing to do with trading. Those movements are real and must not be counted as volume.

Third, most venues trade wrapped SOL rather than native SOL, using the wrapped mint So11111111111111111111111111111111111111112. Routers commonly create a wrapped account, use it, and close it within one transaction. The result is that the SOL side of the trade appears in the token arrays while the wrapping and unwrapping appear in the lamport arrays, describing the same value twice from two directions. Reconcile them deliberately.

Inner instructions and the call tree

The metadata records the cross-program invocations each top-level instruction produced, grouped by the index of the outer instruction. This is how you learn that a router at position two invoked an automated market maker, which invoked the token program twice to move both legs.

For attribution this structure is more useful than the amounts. It tells you which program actually executed the fill, which is what you need in order to say a trade happened on one venue rather than another. It also tells you whether a movement was a user's own instruction or a consequence of one, which is the difference between counting a user trade and counting a pool fill.

Versioned transactions add one requirement. Address lookup tables move some account keys out of the message, so the real account list is the static keys plus the writable and readonly addresses the metadata reports as loaded. A decoder that reads only the static keys will resolve indexes to the wrong accounts and produce confident nonsense rather than an error. The transaction format is documented in the getTransaction reference.

Anchor event data and discriminators

Programs built with Anchor use eight-byte discriminators to identify both instructions and events. An instruction discriminator is derived from a hash of the instruction name in a global namespace; an event discriminator is derived from a hash of the event name in an event namespace. In both cases the first eight bytes of the hash prefix the serialised payload.

Classic Anchor events are written into the log buffer as a base64 payload on a dedicated line. With the program's interface definition you can decode that into typed fields, which often include useful semantics such as the pool involved or how a fee was split. Newer code frequently emits through a self-invocation instead, placing the payload in the inner instructions rather than in the log, which is more robust and invisible to a log subscriber.

Use these payloads for colour, never for totals. They are self-reported by the program, they are subject to the same log truncation as everything else in that buffer, and they can be absent for reasons that have nothing to do with what happened. The Anchor repository documents the encoding if you need to implement it.

Program identifiers worth knowing

A decoder needs a map from program identifier to venue. The identifiers below are the widely published ones for common Solana venues. Verify every value against the project's own documentation before hard-coding it, because programs are deployed, migrated and superseded, and a stale identifier produces a silent zero rather than an error.

ProgramIdentifierRole
SPL TokenTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DAOwns classic token accounts and moves balances
Token-2022TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbToken accounts with extensions such as transfer fees
Raydium AMM v4675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8Constant product pools
Raydium CLMMCAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqKConcentrated liquidity pools
Orca WhirlpoolwhirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCcConcentrated liquidity pools
Meteora DLMMLBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxoDiscrete liquidity bins
Pump.fun6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6PBonding curve launches
Jupiter aggregator v6JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4Routes an order across venues

The aggregator row is the one that changes how you count. A routed order appears as one user instruction wrapping several venue fills, so the same transaction is legitimately one user trade and several pool trades. Decide which question you are answering before you write the aggregation, because the two totals are different and both are defensible.

Coverage is also where a decoder ages. A token that starts on a bonding curve and later trades in a constant product pool has moved venue, and a pipeline that only understands the launch program stops seeing it. The same reasoning applies to tooling: a Raydium volume bot and a curve-focused tool are operating on different programs with different account layouts, which is why venue coverage is a specification rather than a detail.

A worked decode, step by step

The values below are illustrative, chosen to show the arithmetic clearly. They describe no real transaction. Assume a wallet swaps wrapped SOL for a token with nine decimals through a single pool.

AccountMintPre, base unitsPost, base unitsDelta
Trader wSOL accountwrapped SOL5,000,000,0003,000,000,000-2,000,000,000
Pool wSOL vaultwrapped SOL800,000,000,000802,000,000,000+2,000,000,000
Trader token accounttarget token01,940,000,000+1,940,000,000
Pool token vaulttarget token500,000,000,000498,060,000,000-1,940,000,000

Reading this as accounting: the trader spent 2,000,000,000 base units of wrapped SOL, which at nine decimals is 2 SOL, and received 1,940,000,000 base units of the token. The pool vaults move by exactly the opposite amounts, which is the check that the decode is internally consistent. If the vault deltas do not mirror the trader deltas, you have missed an account or a fee destination.

Now the counting decision. If you record the SOL side, this transaction contributes 2 SOL of volume. If you record both sides valued in SOL, it contributes roughly 4 SOL. If a router had split this across two pools, you would see two vault pairs and would have to decide whether the user traded once or twice. None of those answers is wrong; only an unstated answer is.

Attribution rules and the deduplication key

Write these four rules down before you write the aggregation, because changing them later means reprocessing history.

  • Which side counts. Pick the quote side, usually the SOL or stable leg, and apply it everywhere. Mixed sides across venues make totals incomparable.
  • User trade or pool fill. Decide whether the countable unit is the user's order or each venue-level execution, and never mix them in one total.
  • Failed transactions. Excluded, always. They moved nothing, and including them makes activity look higher during exactly the periods when submissions are failing.
  • The key. Transaction signature plus instruction index, stored as a unique constraint so a replayed delivery or a backfill overlap cannot create a second row.

Coverage is worth publishing alongside these rules. Any pipeline, including a hosted multi-DEX Solana volume bot, is only as complete as the venue list it decodes, and a stated list is what lets a reader tell an absent number from an uncovered one.

Edge cases that break naive decoders

  • Token accounts created and closed within one transaction, so a balance exists on only one side of the diff.
  • Token-2022 transfer fees, where the amount received is deliberately less than the amount sent and neither figure alone describes the trade.
  • Multiple token accounts for the same mint and owner, which look like separate positions until you group by owner.
  • Address lookup tables, where indexes resolve to the wrong accounts unless loaded addresses are appended to the key list.
  • Aggregated routes, where the same value passes through several pools and can be counted once or several times.
  • Partially filled or reverted inner instructions inside a transaction that ultimately succeeded.
  • Fee and referral destinations receiving a share of the output, which must be attributed rather than silently added to the trader's fill.
  • Rent movements from account creation and closure, which change lamport balances without moving any traded value.

Each of these is a rule in a decoder rather than a bug, and each has to be discovered by reading real transactions. The productive habit is to keep a small library of signatures that exercise the awkward cases and to run the decoder against them on every change, so that a regression is caught by a fixture rather than by a stakeholder asking why a number moved.

With events decoded and keyed, the remaining work is arithmetic over time, which is the subject of building a live volume view. If the events are arriving but occasionally missing entirely, the problem is upstream and belongs to data gaps and backfill.

Questions this desk keeps getting

Does Solana have events like Ethereum logs?

Not as a protocol feature. Programs can write text to a log buffer and frameworks such as Anchor can serialise structured payloads into that buffer, but nothing in the runtime validates the contents and nothing guarantees the format across versions. The runtime does record balance changes for every account a transaction touched, and that record is the closest thing to a canonical event.

Why use balance deltas instead of the swap instruction arguments?

Because the arguments describe intent and the balances describe outcome. A swap instruction typically carries a requested amount and a minimum acceptable result, and the actual fill sits somewhere between them. Reading the arguments gives you what the user asked for; reading the balances gives you what happened, which is the only figure worth aggregating.

How do I handle wrapped SOL in a swap?

Treat the wrapped SOL token account as the SOL side of the trade and be ready for it to be created and closed inside the same transaction. When an account is closed, its remaining lamports return to the owner, so the native balance change and the token balance change describe two halves of one movement. Counting both without reconciling them double counts the trade.

What is the correct deduplication key for a swap?

Transaction signature plus the index of the instruction that produced the fill. The signature alone is too coarse, because one transaction can contain several legitimate fills, and any key that includes a timestamp or an arrival order is unstable across retries and backfills. The pair is stable, unique and reproducible from the transaction itself.

Do I need the program IDL to decode a swap?

Not for amounts, which come from the runtime-recorded balances regardless of the program involved. You need the IDL when you want the semantic detail a program chose to emit, such as a pool identifier or a fee breakdown carried in an Anchor event. Build the balance-based path first so that an unavailable or outdated IDL degrades detail rather than breaking the count.

How do address lookup tables affect decoding?

They move some account keys out of the transaction message and into tables referenced by it, so the account key list you need is the static keys plus the loaded writable and readonly addresses reported in the metadata. A decoder that only reads the static keys will resolve indexes to the wrong accounts, which produces confidently wrong attribution rather than an error.

Should I decode every venue myself?

Only the ones you will maintain. Each additional program means another instruction layout, another set of account roles, and another upgrade to notice. A pipeline that covers four venues accurately is more useful than one that covers twelve with three of them silently broken, and the honest move is to publish the covered list next to the number.