Skip to main content
When you receive transaction data from Laserstream, there are two important things to look for:
  • Message → What the user wanted to do (their signed proposal)
  • Meta → What actually happened (the execution result)
The challenge: Raw transaction data comes as binary byte arrays like <Buffer 00 bf a0 e8...> instead of readable addresses and signatures. This guide shows you how to: Decode that binary data into human-readable format, extract meaningful information, and understand the complete transaction story from proposal to execution.

A live stream, no decoding

Run the minimal client below. The filter flags drop vote and failed transactions, and the accountInclude array limits results to activity that touches the Jupiter program ID.
Your console now shows a wrapper—filters, createdAt plus a transaction branch that hides two children:
  • transaction.transaction.transaction → the signed message
  • transaction.transaction.meta → the execution meta
Everything that looks like Uint8Array remains opaque for the moment. When you run the script with the decoding function, you’ll see the actual nested structure with readable addresses:

Decoding the binary data

Why decode? Raw Laserstream data contains signatures, account keys, and hashes as binary Uint8Array objects that are unreadable. You need to convert these to base58 strings to make sense of the transaction. The solution: Laserstream uses Yellowstone gRPC, which provides built-in decoding utilities. Instead of writing separate decoders for each field type, we use one recursive function that converts all binary data to human-readable format.
This approach leverages the built-in decoding while handling the binary fields that need manual conversion. The transaction structure is already parsed - you just need to convert the binary fields to human-readable format.

Understanding the transaction structure

Now that we can see the decoded data, let’s explore the two main parts of every Laserstream transaction update. Remember from our initial example that each transaction contains two key objects:
  • Message (Proposal)transaction.transaction.transaction → the signed message (user’s proposal)
  • Meta (Execution)transaction.transaction.meta → the execution metadata (validator’s response)
This two-part structure tells a complete story: what the user requested versus what actually happened. Let’s examine each part in detail.

The proposal: everything inside message

The user creates a message that specifies what, who and until when. Here’s how to decode each part:

Transaction Header

numRequiredSignatures tells the validator how many signatures to verify, while the two numReadonly* values label accounts the runtime can treat as read-only, enabling parallel execution.

Account Keys Dictionary

accountKeys is a plain list of public keys that acts as a lookup table. Every later integer in the transaction - programIdIndex, each element in an instruction’s accounts array - points back into this list by index, saving more than a kilobyte per message.

Protection Against Replay

recentBlockhash expires once it scrolls out of the last 150 block-hashes, roughly ninety seconds on mainnet.

Instructions: The Actual Commands

Each instruction contains three key parts:
  • Program ID (programIdIndex): Points to an address in the accountKeys array (e.g., index 10 = ComputeBudget111111111111111111111111111111)
  • Accounts (accounts): A base58-encoded string representing which account indexes this instruction touches
  • Data (data): The actual instruction data encoded as base58
Due to the convertBuffers function, accounts appears as base58 but actually contains account indices (e.g., "3vtmrQMafzDoG2CBz1iqgXPTnC" decodes to indices [21, 19, 12, 17, 2, 6, 1, 22]) This design means instead of repeating full 32-byte addresses, each instruction just references positions in the lookup table.

Signatures: Proof of Authorization

signatures contains the cryptographic signatures proving the required accounts authorized this transaction. The number of signatures must match header.numRequiredSignatures.

Address Table Lookups

If versioned is true, addressTableLookups appears with an on-chain table and two index lists. Lookup tables lift the hard cap on address count to dozens while keeping the packet under the 1,232-byte MTU.

How It All Connects: The Flow

Here’s what happens from first principles:
  1. Build the lookup table: accountKeys lists all addresses this transaction will touch
  2. Set the rules: header specifies how many signatures are required and which accounts are read-only
  3. Create the commands: Each instruction points to:
    • A program (via programIdIndexaccountKeys[index])
    • The accounts it needs (via accounts → multiple accountKeys[index] positions)
    • The instruction data (encoded in data)
  4. Add authorization: signatures proves the required accounts approved this transaction
  5. Set expiration: recentBlockhash ensures this transaction can’t be replayed later

The execution: everything inside meta

While the message shows what the user wanted to do, the meta shows what actually happened when validators executed the transaction.

Basic execution information

Success/Failure
  • err: null = success
  • err: {...} = failure with error details
  • fee = lamports charged for this transaction
Balance Changes
Balance arrays correspond to the accountKeys array by index:
  • Account 0: Lost 15000 lamports (fee payment)
  • Account 1: Gained 1461600 lamports (new account created)
  • Account 3: Gained 2001231920 lamports (program account)
Compute Usage
Shows how much compute budget was used (out of the requested amount).

Advanced execution details

Inner Instructions
Inner instructions are additional instructions that programs called during execution. They’re not part of the original transaction but were triggered by the main instructions. Log Messages
Log messages provide a chronological trace of program execution, showing which programs were called and any custom log messages they output. Token Balance Changes
Token balance changes show before/after states for SPL token accounts, including the human-readable amounts with proper decimal handling.

Practical decoding patterns

Here are common patterns for extracting useful information from decoded transactions:

Complete example: Jupiter swap decoder

Here’s a complete example that decodes Jupiter swap transactions and extracts meaningful information:
This example shows how to combine message decoding with meta analysis to extract business-relevant information from complex DeFi transactions.

Key takeaways

  • Two-part structure: Every transaction has a message (what was requested) and meta (what actually happened)
  • Binary decoding: Use bs58.encode() to convert binary fields to readable base58 strings
  • Account key lookups: Instructions reference accounts by index in the accountKeys array
  • Balance tracking: Compare preBalances and postBalances to see what changed
The key to understanding Solana transactions is recognizing that they’re designed for efficiency: instead of repeating addresses, they use lookup tables and indexes to minimize transaction size while maximizing information density.