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:
| 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 | Pushed to the IPv4 and port you register | The same decoded entries, as UDP datagrams |
| Dedicated | Pushed to the IPv4 and port you register | 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, 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
| 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:
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:
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_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
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.
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
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
grpcurl -plaintext \
-proto shredstream.proto \
-H 'x-api-key: ak_YOUR_KEY' \
-d '{}' \
88.216.132.50:9090 shredstream.ShredstreamProxy/SubscribeEntriesDirect 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.
| Destination | The IPv4 address and port you register on the subscription |
| Source | 88.216.132.50 |
Rust (UDP listener)
Deserialize with solana-ledger's Shred.
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)
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.
