π₯ 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.
| Offset | Size | Field | Value / semantics |
|---|---|---|---|
| 0 | 2 | magic | 0x5AE7 (u16 LE) β anything else β reject the datagram |
| 2 | 1 | version | 2 β reject any unknown version |
| 3 | 1 | msg_type | 2 = DecodedTx (1 = reserved) |
| 4 | 1 | flags | Reserved, 0 |
| 5 | 1 | frag_index | Fragment index, 0-based β always 0 for decoded transactions |
| 6 | 1 | frag_count | Fragment count β always 1 for decoded transactions (never fragmented) |
| 7 | 1 | _pad | 0 |
| 8 | 8 | seq | u64 LE, monotonically increasing per product stream |
π§Ύ The Payload (msg_type = 2)
Immediately after the header:
| Field | Size | Semantics |
|---|---|---|
slot | 8 | u64 LE β the slot the transaction belongs to |
transaction | rest of the datagram | Standard 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:
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)
import socket, structsock = 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 dashboardexpected = Nonewhile True:dgram = sock.recv(2048)if len(dgram) < 24:continuemagic, version, msg_type = struct.unpack_from("<HBB", dgram, 0)if magic != 0x5AE7 or version != 2:continue # reject unknown magic/versionif 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.
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-threaduse 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:
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.