Transaction Introspection
Decode a confirmed transaction and parse its instructions
Introduction
When you fetch a confirmed transaction with getTransaction, the RPC hands you a compiled, wire-shaped payload: account references are numeric indices, instruction data is encoded as a string, and the instructions emitted by cross-program invocations live in a separate meta.innerInstructions field. Codama auto-generated (e.g.@solana-program/*) clients — identifyXyzProgramInstruction, parseMyProgramInstruction, and other helpers — want Kit Instruction objects with resolved AccountMetas and raw bytes.
The @solana/transaction-introspection package bridges that gap. It decodes a getTransaction response into a CompiledTransactionMessage, resolves every account index against the static keys plus the addresses loaded from address lookup tables, normalizes the inner CPI instructions, and returns instructions in the exact form the @solana-program/* clients can identify and parse directly. It supports legacy, v0, and v1 transactions.
This guide picks up where Deserializing transactions leaves off.
Installation
These utilities are included within the @solana/kit library but you may also install them using their standalone package.
Decoding an RPC response
decodeTransactionFromRpcResponse turns a getTransaction response into a DecodedRpcTransaction: the compiledMessage (always carrying the recent blockhash in lifetimeToken), the loadedAddresses pulled from meta, and — for base64 and base58 encodings only — a re-encodable transaction.
Prefer encoding: 'base64' when bandwidth allows — it is the most compact, the wire bytes round-trip cleanly through the Kit codecs, and the return type statically guarantees a non-optional transaction. encoding: 'base58' behaves the same way, and encoding: 'json' is accepted too, though it omits transaction because the server has already decompiled the wire format.
encoding: 'jsonParsed' is not supported. Its instructions arrive pre-parsed by the server
and lack the raw bytes the @solana-program/* parseXInstruction clients need, so passing one
throws SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION.
To receive a v0 or v1 transaction at all, you must set maxSupportedTransactionVersion on
the getTransaction call. Without it the server rejects anything past legacy.
Walking every instruction
walkInstructions is the main API. It returns every instruction in a confirmed transaction as an array of TracedInstructions, in the order an explorer displays them: each outer instruction followed immediately by the inner instructions its CPIs produced. Because each entry is itself a resolved Kit Instruction, you can pass it straight to isInstructionForProgram and to the auto-generated identifyXInstruction / parseXInstruction helpers.
Here we tally every USDC transfer in a transaction — outer instructions and inner CPI alike — using the @solana-program/token client to parse each TransferChecked and filter by mint:
The CPI transfers — those a router or DEX program made on your behalf — are caught alongside the top-level ones precisely because walkInstructions interleaves the inner instructions from meta.innerInstructions.
Each entry carries a trace property typed as InstructionTrace, a discriminated union that records where the instruction sits:
{ kind: 'outer', index }— a top-level instruction in the compiled message.{ kind: 'inner', outerIndex, innerIndex, stackHeight? }— an instruction emitted via cross-program invocation.stackHeightis the CPI depth, included only when the RPC reports it.
The same pattern works with any codama-generated @solana-program/* client. Swap in @solana-program/system to tally lamports moved by every TransferSol, outer or inner:
Pass meta?.loadedAddresses (or the loadedAddresses returned by
decodeTransactionFromRpcResponse) for v0 transactions that load accounts from address lookup
tables. This package never makes an RPC call to resolve lookup tables itself — it uses the
addresses the validator already recorded in meta. Without them, only the static accounts are
available to resolve indices, and any instruction referencing a looked-up account index throws
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE.
Omitting meta entirely returns only the outer instructions.
Resolving instructions without walking
If you do not need the interleaved outer-and-inner ordering, the lower-level helpers expose each piece on its own.
getInstructionsFromCompiledTransactionMessage returns just the outer instructions as ResolvedInstructions. getInnerInstructionsFromMeta returns just the inner instructions (decoding their base58 data and resolving indices against a supplied AccountMeta list). getAccountMetasFromCompiledTransactionMessage builds the ordered AccountMeta list both rely on.
The account metas are returned in the runtime's resolution order — writable signers, readonly signers, writable non-signers, readonly non-signers, then the ALT-loaded writable and readonly addresses — so inner-instruction indices line up against the very same list. If you only need the flat ordered address list, map over the result with accountMetas.map((m) => m.address).
Notes
- Version support.
legacy,v0, andv1compiled messages all resolve identically — account indices, inner instructions, and ALT-loaded addresses behave the same across versions. jsonParsedis rejected. Usebase64,base58, orjson. Any unrecognized response shape throwsSOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE.TracedInstructionis aResolvedInstruction. No separate filter helper is needed — narrow withisInstructionWithAccounts/isInstructionWithDataand hand the result straight to the@solana-program/*clients.