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 two 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 (UDP) / DedicatedProvisioned per subscriptionRaw 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 and Amsterdam. Every account can start a free 1-hour trial once, 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:

1syntax = "proto3";
2
3package shredstream;
4
5service ShredstreamProxy {
6  rpc SubscribeEntries(SubscribeEntriesRequest) returns (stream Entry);
7}
8
9message SubscribeEntriesRequest {}
10
11message Entry {
12  uint64 slot = 1;
13  // bincode-serialized Vec<solana_entry::entry::Entry>
14  bytes entries = 2;
15}

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:

1pub struct Entry {
2    pub num_hashes: u64,
3    pub hash: Hash,                          // [u8; 32]
4    pub transactions: Vec<VersionedTransaction>,
5}

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

1use tonic::{Request, transport::Channel, metadata::MetadataValue};
2use shredstream_proxy_client::ShredstreamProxyClient;
3use solana_entry::entry::Entry as LedgerEntry;
4
5#[tokio::main]
6async fn main() -> Result<(), Box<dyn std::error::Error>> {
7    // Plaintext h2c — the proxy does not terminate TLS, so use http://
8    let channel = Channel::from_static("http://88.216.132.50:9090")
9        .connect()
10        .await?;
11
12    let mut client = ShredstreamProxyClient::new(channel)
13        .max_decoding_message_size(64 * 1024 * 1024);
14
15    let mut request = Request::new(SubscribeEntriesRequest {});
16    request.metadata_mut().insert(
17        "x-api-key",
18        MetadataValue::from_str("ak_YOUR_KEY")?,
19    );
20
21    let mut stream = client.subscribe_entries(request).await?.into_inner();
22
23    while let Some(msg) = stream.message().await? {
24        let entries: Vec<LedgerEntry> = bincode::deserialize(&msg.entries)?;
25        for entry in &entries {
26            for tx in &entry.transactions {
27                // tx is a solana_sdk::transaction::VersionedTransaction
28                println!("slot={} sig={}", msg.slot, tx.signatures[0]);
29            }
30        }
31    }
32
33    Ok(())
34}

Node.js / TypeScript

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

1const grpc = require('@grpc/grpc-js');
2const protoLoader = require('@grpc/proto-loader');
3
4const packageDef = protoLoader.loadSync('./shredstream.proto');
5const proto = grpc.loadPackageDefinition(packageDef);
6
7// Plaintext h2c — no TLS on this endpoint.
8const client = new proto.shredstream.ShredstreamProxy(
9  '88.216.132.50:9090',
10  grpc.credentials.createInsecure(),
11  { 'grpc.max_receive_message_length': 64 * 1024 * 1024 }
12);
13
14const metadata = new grpc.Metadata();
15metadata.add('x-api-key', 'ak_YOUR_KEY');
16
17const stream = client.subscribeEntries({}, metadata);
18
19stream.on('data', (msg) => {
20  // msg.slot    -> string | number
21  // msg.entries -> Buffer, bincode Vec<solana_entry::entry::Entry>
22  // Decode it with a bincode reader to reach the VersionedTransactions.
23  console.log(`slot=${msg.slot} entries_bytes=${msg.entries.length}`);
24});
25
26// Slots can be skipped and the stream can drop — reconnect with backoff.
27stream.on('error', (err) => console.error('stream error:', err));
28stream.on('end', () => console.log('stream ended'));

Python

1import grpc
2import shredstream_pb2
3import shredstream_pb2_grpc
4
5# Plaintext h2c — no TLS on this endpoint.
6channel = grpc.insecure_channel(
7    "88.216.132.50:9090",
8    options=[("grpc.max_receive_message_length", 64 * 1024 * 1024)],
9)
10stub = shredstream_pb2_grpc.ShredstreamProxyStub(channel)
11
12metadata = [("x-api-key", "ak_YOUR_KEY")]
13
14for msg in stub.SubscribeEntries(shredstream_pb2.SubscribeEntriesRequest(), metadata=metadata):
15    # msg.entries is bincode-serialized Vec<solana_entry::entry::Entry>;
16    # decode it to reach the transactions inside each entry.
17    print(f"slot={msg.slot} entries_bytes={len(msg.entries)}")

grpcurl smoke test

1grpcurl -plaintext \
2  -proto shredstream.proto \
3  -H 'x-api-key: ak_YOUR_KEY' \
4  -d '{}' \
5  88.216.132.50:9090 shredstream.ShredstreamProxy/SubscribeEntries

Direct (UDP) and Dedicated

Direct UDP and the Dedicated cluster forward the raw shred stream — data shreds and coding shreds, exactly as they come off Turbine. Nothing is decoded for you, which is the point: you deshred in your own process on your own schedule.

Connection details

Endpoint IP and portIssued with your subscription, shown in the console
AuthIP whitelisting — register your source IPs on the subscription

Packets arrive unsolicited from the streamer, so bind and listen; there is no handshake.

Rust (UDP listener)

Deserialize with solana-ledger's Shred.

1use std::net::UdpSocket;
2use solana_ledger::shred::Shred;
3
4fn main() -> std::io::Result<()> {
5    // Bind the port issued with your subscription
6    let socket = UdpSocket::bind("0.0.0.0:8001")?;
7
8    let mut buf = [0u8; 65535];
9    println!("Listening for shreds...");
10
11    loop {
12        let (amt, _src) = socket.recv_from(&mut buf)?;
13        match Shred::new_from_serialized_shred(buf[..amt].to_vec()) {
14            Ok(shred) => println!("slot={} index={}", shred.slot(), shred.index()),
15            Err(_) => continue, // coding shred or partial packet
16        }
17    }
18}

Node.js (UDP listener)

1const dgram = require('dgram');
2const server = dgram.createSocket('udp4');
3
4server.on('error', (err) => {
5  console.log(`server error:\n${err.stack}`);
6  server.close();
7});
8
9server.on('message', (msg, rinfo) => {
10  console.log(`Received ${msg.length} bytes from ${rinfo.address}:${rinfo.port}`);
11  // Raw shred bytes. Deshredding in JS needs custom logic or WASM bindings
12  // to the Solana Rust crates — most teams do this step in Rust.
13});
14
15server.on('listening', () => {
16  const address = server.address();
17  console.log(`server listening ${address.address}:${address.port}`);
18});
19
20// Bind the port issued with your subscription
21server.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.