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:
| Mode | Endpoint | What you receive |
|---|---|---|
| Shared (gRPC) | 88.216.132.50:9090 | Decoded ledger entries — the transactions are already parsed out of the shreds for you |
| Direct (UDP) / Dedicated | Provisioned per subscription | Raw 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
| Endpoint | 88.216.132.50:9090 |
| Transport | Plaintext h2c — there is no TLS on this port. Use http://, createInsecure(), grpc.insecure_channel(...). |
| Auth | gRPC metadata header x-api-key (x-token is also accepted) |
| Key format | ak_<digits>, issued in the console |
| Access control | Your 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_typeordatafield 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.
SubscribeEntriesRequestis 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
endthe 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/SubscribeEntriesDirect (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 port | Issued with your subscription, shown in the console |
| Auth | IP 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.