- Message → What the user wanted to do (their signed proposal)
- Meta → What actually happened (the execution result)
<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 accountsInclude array limits results to activity that touches the Jupiter program ID.filters
, createdAt
plus a transaction
branch that hides two children:
transaction.transaction.transaction
→ the signed messagetransaction.transaction.meta
→ the execution meta
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 binaryUint8Array
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.
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)
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
- Program ID (
programIdIndex
): Points to an address in theaccountKeys
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
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
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:- Build the lookup table:
accountKeys
lists all addresses this transaction will touch - Set the rules:
header
specifies how many signatures are required and which accounts are read-only - Create the commands: Each
instruction
points to:- A program (via
programIdIndex
→accountKeys[index]
) - The accounts it needs (via
accounts
→ multipleaccountKeys[index]
positions) - The instruction data (encoded in
data
)
- A program (via
- Add authorization:
signatures
proves the required accounts approved this transaction - 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/Failureerr: null
= successerr: {...}
= failure with error detailsfee
= lamports charged for this transaction
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)
Advanced execution details
Inner InstructionsPractical 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: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
andpostBalances
to see what changed