Documentation index

ShredStream Access

AllenHark reads Solana block data at the source — the shreds validators exchange over Turbine while a block is still being produced, before RPC confirmation. There are three delivery modes, and they hand you different things:

ModeEndpointWhat you receive
Shared (gRPC)88.216.132.50:9090Decoded ledger entries — the transactions are already parsed out of the shreds for you
Direct UDPPushed to the IPv4 and port you registerThe same decoded entries, as UDP datagrams
DedicatedPushed to the IPv4 and port you registerRaw shreds — data and coding shreds exactly as they come off Turbine

Shared gRPC is the right choice unless you have a reason to deshred yourself. Regions: Frankfurt, plus Amsterdam for Shared weekly and monthly plans and for Direct UDP; Dedicated is Frankfurt. Shared gRPC and Direct UDP each have a free 1-hour trial, once per account, from the console. See pricing for the plans.


Shared (gRPC)

Connection details

Endpoint88.216.132.50:9090
TransportPlaintext h2c — there is no TLS on this port. Use http://, createInsecure(), grpc.insecure_channel(...).
AuthgRPC metadata header x-api-key (x-token is also accepted)
Key formatak_<digits>, issued in the console
Access controlYour subscription's whitelisted IPs

The proto

Download it from /shredstream.proto. It is small:

Protobuf
syntax = "proto3";

package shredstream;

service ShredstreamProxy {
  rpc SubscribeEntries(SubscribeEntriesRequest) returns (stream Entry);
}

message SubscribeEntriesRequest {}

message Entry {
  uint64 slot = 1;
  // bincode-serialized Vec<solana_entry::entry::Entry>
  bytes entries = 2;
}

Two fields, that is all. slot is the slot the batch belongs to. entries is not a shred payload — it is a bincode-serialized Vec<solana_entry::entry::Entry>, where each ledger entry is:

Rust
pub struct Entry {
    pub num_hashes: u64,
    pub hash: Hash,                          // [u8; 32]
    pub transactions: Vec<VersionedTransaction>,
}

So the transactions you want are two steps in: deserialize entries, then walk each entry's transactions.

There is no index, shred_type or data field on this stream. Older snippets that read them compiled fine against a stale proto and then printed zeros — if you have one, replace it.

Behaviour to design around

  • No filters. SubscribeEntriesRequest is empty; you get the whole firehose and filter client-side.
  • Votes are included. Roughly two thirds of transactions on a healthy slot are votes. Drop them yourself if you don't want them.
  • In order, but slots can be skipped — do not assume a contiguous slot sequence.
  • Raise your max message size. The 4 MiB gRPC default is too small for busy slots; 64 MiB is a comfortable ceiling.
  • Reconnect with backoff. The stream can drop; treat a clean end the same as an error.

Rust

Rust
use tonic::{Request, transport::Channel, metadata::MetadataValue};
use shredstream_proxy_client::ShredstreamProxyClient;
use solana_entry::entry::Entry as LedgerEntry;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Plaintext h2c — the proxy does not terminate TLS, so use http://
    let channel = Channel::from_static("http://88.216.132.50:9090")
        .connect()
        .await?;

    let mut client = ShredstreamProxyClient::new(channel)
        .max_decoding_message_size(64 * 1024 * 1024);

    let mut request = Request::new(SubscribeEntriesRequest {});
    request.metadata_mut().insert(
        "x-api-key",
        MetadataValue::from_str("ak_YOUR_KEY")?,
    );

    let mut stream = client.subscribe_entries(request).await?.into_inner();

    while let Some(msg) = stream.message().await? {
        let entries: Vec<LedgerEntry> = bincode::deserialize(&msg.entries)?;
        for entry in &entries {
            for tx in &entry.transactions {
                // tx is a solana_sdk::transaction::VersionedTransaction
                println!("slot={} sig={}", msg.slot, tx.signatures[0]);
            }
        }
    }

    Ok(())
}

Node.js / TypeScript

The generated package is shredstream, so the client hangs off proto.shredstream.ShredstreamProxy — not proto.ShredstreamProxy.

JavaScript
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDef = protoLoader.loadSync('./shredstream.proto');
const proto = grpc.loadPackageDefinition(packageDef);

// Plaintext h2c — no TLS on this endpoint.
const client = new proto.shredstream.ShredstreamProxy(
  '88.216.132.50:9090',
  grpc.credentials.createInsecure(),
  { 'grpc.max_receive_message_length': 64 * 1024 * 1024 }
);

const metadata = new grpc.Metadata();
metadata.add('x-api-key', 'ak_YOUR_KEY');

const stream = client.subscribeEntries({}, metadata);

stream.on('data', (msg) => {
  // msg.slot    -> string | number
  // msg.entries -> Buffer, bincode Vec<solana_entry::entry::Entry>
  // Decode it with a bincode reader to reach the VersionedTransactions.
  console.log(`slot=${msg.slot} entries_bytes=${msg.entries.length}`);
});

// Slots can be skipped and the stream can drop — reconnect with backoff.
stream.on('error', (err) => console.error('stream error:', err));
stream.on('end', () => console.log('stream ended'));

Python

Python
import grpc
import shredstream_pb2
import shredstream_pb2_grpc

# Plaintext h2c — no TLS on this endpoint.
channel = grpc.insecure_channel(
    "88.216.132.50:9090",
    options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
)
stub = shredstream_pb2_grpc.ShredstreamProxyStub(channel)

metadata = [("x-api-key", "ak_YOUR_KEY")]

for msg in stub.SubscribeEntries(shredstream_pb2.SubscribeEntriesRequest(), metadata=metadata):
    # msg.entries is bincode-serialized Vec<solana_entry::entry::Entry>;
    # decode it to reach the transactions inside each entry.
    print(f"slot={msg.slot} entries_bytes={len(msg.entries)}")

grpcurl smoke test

Terminal
grpcurl -plaintext \
  -proto shredstream.proto \
  -H 'x-api-key: ak_YOUR_KEY' \
  -d '{}' \
  88.216.132.50:9090 shredstream.ShredstreamProxy/SubscribeEntries

Direct UDP and Dedicated

Both push UDP packets from 88.216.132.50 to the IPv4 address and port you register in the console. There is no client and no handshake: bind, listen, and allow inbound UDP from that address in your firewall.

Direct UDP: decoded entries

Direct UDP carries the same reconstructed entries as the shared gRPC stream, not raw shreds. Each message is an 8-byte little-endian slot followed by the bincode-encoded entries. A message larger than 65,000 bytes is split into fragments, each with a 16-byte header: "SHRD", message_id (u32 LE), fragment_index (u16 LE), total_fragments (u16 LE) and total_size (u32 LE); reassemble by message_id. A message that fits in one datagram has no header. Packets come from source port 9093, and their order is not guaranteed.

Dedicated: raw shreds

The Dedicated feed forwards the raw shred stream — data shreds and coding shreds, exactly as they come off Turbine, deduplicated by first arrival, from an ephemeral source port. Nothing is decoded for you, which is the point: you deshred in your own process on your own schedule. At checkout you choose the format: raw, slot_prefixed (an 8-byte slot in front of each shred) or grpc. The listeners below are for the raw format.

DestinationThe IPv4 address and port you register on the subscription
Source88.216.132.50

Rust (UDP listener)

Deserialize with solana-ledger's Shred.

Rust
use std::net::UdpSocket;
use solana_ledger::shred::Shred;

fn main() -> std::io::Result<()> {
    // Bind the port issued with your subscription
    let socket = UdpSocket::bind("0.0.0.0:8001")?;

    let mut buf = [0u8; 65535];
    println!("Listening for shreds...");

    loop {
        let (amt, _src) = socket.recv_from(&mut buf)?;
        match Shred::new_from_serialized_shred(buf[..amt].to_vec()) {
            Ok(shred) => println!("slot={} index={}", shred.slot(), shred.index()),
            Err(_) => continue, // coding shred or partial packet
        }
    }
}

Node.js (UDP listener)

JavaScript
const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.log(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`Received ${msg.length} bytes from ${rinfo.address}:${rinfo.port}`);
  // Raw shred bytes. Deshredding in JS needs custom logic or WASM bindings
  // to the Solana Rust crates — most teams do this step in Rust.
});

server.on('listening', () => {
  const address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

// Bind the port issued with your subscription
server.bind(8001);

Jito Shreds

If you are migrating off Jito's ShredStream and want to keep your existing shredstream-proxy binary and keypair, that is a separate product — see Jito Shreds.

Support

For assistance with shred deserialization or connection issues, please join our Discord.