πŸ“₯ Receiving Decoded Transactions

A Decoded Shred Stream is pushed over UDP to the IP and port you configure on the stream detail page β€” no connection, no handshake, no retransmission. Each datagram is at most 1,408 bytes and carries one complete transaction: a 16-byte binary header, the slot, and the transaction in standard Solana wire format.


πŸ“¦ The Envelope Header (16 bytes)

Every datagram starts with the same fixed header. All integers are little-endian.

OffsetSizeFieldValue / semantics
02magic0x5AE7 (u16 LE) β€” anything else β‡’ reject the datagram
21version2 β€” reject any unknown version
31msg_type2 = DecodedTx (1 = reserved)
41flagsReserved, 0
51frag_indexFragment index, 0-based β€” always 0 for decoded transactions
61frag_countFragment count β€” always 1 for decoded transactions (never fragmented)
71_pad0
88sequ64 LE, monotonically increasing per product stream

🧾 The Payload (msg_type = 2)

Immediately after the header:

FieldSizeSemantics
slot8u64 LE β€” the slot the transaction belongs to
transactionrest of the datagramStandard Solana wire format (bincode-serialized VersionedTransaction)

The transaction bytes are exactly what any Solana SDK expects β€” feed them to your existing parser unchanged. Decoded transactions are never fragmented: one datagram is always one whole transaction. Vote transactions are excluded by default.


πŸ“‰ Detecting Loss with seq

UDP does not retransmit. Every datagram carries a seq that increases by exactly 1 within your stream. A gap in seq means that many datagrams were lost:

text
seq received = 1042, previous = 1039 β†’ 2 datagrams lost

Track the last seq you saw and alert on gaps β€” sustained loss usually means your receive buffer is too small (below) or an on-path bottleneck.


πŸ”§ Buffer Sizing

The stream is bursty β€” a busy slot can deliver many transactions back-to-back. Give the socket a generous receive buffer (SO_RCVBUF, several megabytes) so short bursts are absorbed instead of dropped by the kernel, and drain the socket from a dedicated, fast loop.


πŸ’» Minimal Decoder (Python)

python
import socket, struct
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)
sock.bind(("0.0.0.0", 5000)) # the port you configured on the dashboard
expected = None
while True:
dgram = sock.recv(2048)
if len(dgram) < 24:
continue
magic, version, msg_type = struct.unpack_from("<HBB", dgram, 0)
if magic != 0x5AE7 or version != 2:
continue # reject unknown magic/version
if msg_type != 2:
continue # not a DecodedTx datagram
(seq,) = struct.unpack_from("<Q", dgram, 8)
if expected is not None and seq != expected:
print(f"gap: {seq - expected} datagram(s) lost")
expected = seq + 1
(slot,) = struct.unpack_from("<Q", dgram, 16)
tx_bytes = dgram[24:] # standard wire format β€” feed to any Solana tx parser

πŸ¦€ Official Clients and the codec Module

You do not have to write the decoder above: the official decoded-shredstream clients (Rust, Go, JavaScript/TypeScript, Python) bind the port, parse the envelope, detect seq gaps and hand you each transaction with its slot and signatures β€” over UDP and gRPC, with the same API.

rust
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-thread
use decoded_shredstream::{UdpClient, UdpConfig};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut client = UdpClient::bind(UdpConfig { port: 8002, ..Default::default() })?;
while let Some(update) = client.next_update().await {
println!("slot={} sig={} {}B", update.slot(), update.signature(), update.bytes().len());
}
Ok(())
}

The Rust crate also exposes the framing as the codec module (constants FRAME_MAGIC = 0x5AE7, FRAME_VERSION = 2, FRAME_HEADER_LEN = 16, MAX_DATAGRAM = 1408, parse_header, StreamDecoder) so you can decode datagrams read from a socket you manage yourself β€” the Go client exposes the same through ParseHeader / StreamDecoder:

rust
use std::net::UdpSocket;
use decoded_shredstream::codec::{Push, StreamDecoder, MAX_DATAGRAM};
fn decode_loop(socket: &UdpSocket) -> std::io::Result<()> {
let mut decoder = StreamDecoder::new();
let mut buf = [0u8; MAX_DATAGRAM + 64];
loop {
let n = socket.recv(&mut buf)?;
if let Push::Update(update) = decoder.push(&buf[..n]) {
println!("slot={} sig={}", update.slot(), update.signature());
}
}
}

Install: npm install decoded-shredstream Β· pip install decoded-shredstream Β· cargo add decoded-shredstream Β· go get github.com/shredstream/decoded-shredstream-go.


➑️ Next Steps

  • Decoded Shred Stream β€” positioning, latency, and delivery modes.
  • gRPC Delivery β€” the same transactions over an ordered, connection-oriented stream with server-side account filtering.
Receiving Decoded Transactions β€” Docs | ShredStream.com