ShredPrism Quickstart
Connection settings, the shredstream protocol, and Rust and TypeScript consumers.
This page connects to ShredPrism and prints the entries of each slot.
Requirements
- The Nexus plan.
- Your ShredPrism connection details and API key, from My Account.
Connection settings
| Setting | Value |
|---|---|
| Host | The ShredPrism host for your region. See Endpoints. |
| Port | 55577 |
| Transport | gRPC over HTTP/2 with TLS. Plaintext connections are not accepted. |
| Authentication | Metadata header x-token: YOUR_API_KEY |
| Streams | One stream per key, per region |
Some tools show the endpoint as grpc://prism-newyork.constant-k.com:55577. Port 55577 speaks TLS only, so the client has to enable it: in most gRPC libraries that means an https:// address or TLS channel credentials.
This confirms the TLS handshake and HTTP/2 negotiation from your server:
openssl s_client -connect prism-newyork.constant-k.com:55577 \
-servername prism-newyork.constant-k.com -alpn h2 </dev/null 2>/dev/null \
| grep -E "ALPN protocol|Verify return code"The output holds ALPN protocol: h2 and Verify return code: 0 (ok).
Protocol
ShredPrism serves the Jito ShredStream interface, so a client generated from Jito's public protocol file works unchanged. See Compatibility and identity. There is no proxy to run locally, no Jito authentication keypair and no region flag.
| Proto package | shredstream (proto3) |
| Service | shredstream.ShredstreamProxy |
| Method | SubscribeEntries, server streaming |
| Request | SubscribeEntriesRequest, an empty message. There are no filters: you receive the whole feed. |
| Response stream | Entry, with a slot and an entries byte string |
| Full path | /shredstream.ShredstreamProxy/SubscribeEntries |
| API key | Metadata x-token |
The definition comes from shredstream.proto in jito-labs/mev-protos. That file imports others this call does not need, so a local copy holding only the streaming service is enough:
syntax = "proto3";
package shredstream;
service ShredstreamProxy {
rpc SubscribeEntries(SubscribeEntriesRequest) returns (stream Entry);
}
message SubscribeEntriesRequest {}
message Entry {
uint64 slot = 1;
bytes entries = 2;
}entries holds serialized entries: a list of proof-of-history entries, each carrying its own transactions. Entries has the byte layout, the decoders and the delivery properties of the feed.
bincode cannot decode this payload
Decoding needs wincode and solana-entry 4.2 or later, built with its agave-unstable-api feature. A version 1 transaction has no bincode representation, so sample code that calls bincode::deserialize into a list of entries is out of date.
Rust consumer
Dependencies
[dependencies]
anyhow = "1"
prost = "0.14"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# tls-ring provides the crypto backend; without it the client panics at connect.
tonic = { version = "0.14", features = ["tls-ring", "tls-native-roots"] }
tonic-prost = "0.14"
solana-entry = { version = "4.3", features = ["agave-unstable-api"] }
wincode = "0.6"
[build-dependencies]
tonic-prost-build = "0.14"Generated client
The proto above goes in proto/shredstream.proto, with this build script:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_prost_build::configure()
.build_server(false)
.compile_protos(&["proto/shredstream.proto"], &["proto"])?;
Ok(())
}The consumer
use {
solana_entry::entry::Entry,
tonic::{
metadata::MetadataValue,
transport::{Channel, ClientTlsConfig},
Request,
},
};
pub mod shredstream {
tonic::include_proto!("shredstream");
}
use shredstream::{shredstream_proxy_client::ShredstreamProxyClient, SubscribeEntriesRequest};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let key = std::env::var("CK_API_KEY")?;
let channel = Channel::from_static("https://prism-newyork.constant-k.com:55577")
.tls_config(ClientTlsConfig::new().with_native_roots())?
.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-token", MetadataValue::try_from(key.as_str())?);
let mut stream = client.subscribe_entries(request).await?.into_inner();
while let Some(slot_entry) = stream.message().await? {
// wincode, not bincode: entries can carry version 1 transactions
let entries: Vec<Entry> = wincode::deserialize(&slot_entry.entries)?;
let transactions: usize = entries.iter().map(|e| e.transactions.len()).sum();
println!(
"slot {} entries {} transactions {}",
slot_entry.slot,
entries.len(),
transactions
);
}
Ok(())
}Running it
export CK_API_KEY="YOUR_API_KEY"
cargo run --releaseTypeScript consumer
This reads the entry framing with DataView and hands each transaction to @solana/kit. TypeScript decoding explains why, and what the two branches in readEntries are for.
Project setup
mkdir prism-quickstart && cd prism-quickstart
npm init -y
npm pkg set type=module
npm install @grpc/grpc-js @grpc/proto-loader @solana/kitThe proto above goes in proto/shredstream.proto.
The consumer
import {
credentials,
loadPackageDefinition,
Metadata,
type ClientReadableStream,
} from "@grpc/grpc-js";
import { loadSync } from "@grpc/proto-loader";
import {
getCompiledTransactionMessageDecoder,
type CompiledTransactionMessage,
} from "@solana/kit";
const definition = loadSync("proto/shredstream.proto", {
keepCase: true,
longs: String, // slot arrives as a decimal string
defaults: true,
oneofs: true,
});
const proto = loadPackageDefinition(definition) as any;
const client = new proto.shredstream.ShredstreamProxy(
"prism-newyork.constant-k.com:55577", // or prism-frankfurt
credentials.createSsl(), // TLS is required
{ "grpc.max_receive_message_length": 64 * 1024 * 1024 },
);
const messageDecoder = getCompiledTransactionMessageDecoder();
type Entry = {
numHashes: number;
hash: Uint8Array;
transactions: CompiledTransactionMessage[];
};
function readEntries(payload: Uint8Array): Entry[] {
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
let offset = 0;
const readU64 = () => {
const value = Number(view.getBigUint64(offset, true));
offset += 8;
return value;
};
const entries: Entry[] = [];
const entryCount = readU64();
for (let e = 0; e < entryCount; e++) {
const numHashes = readU64();
const hash = payload.subarray(offset, offset + 32);
offset += 32;
const transactionCount = readU64();
const transactions: CompiledTransactionMessage[] = [];
for (let t = 0; t < transactionCount; t++) {
const isV1 = payload[offset] >= 0x80; // 0x81 opens a version 1 message
if (!isV1) offset += 1 + payload[offset] * 64; // signature count, then the signatures
const [message, next] = messageDecoder.read(payload, offset);
offset = isV1 ? next + message.header.numSignerAccounts * 64 : next;
transactions.push(message);
}
entries.push({ numHashes, hash, transactions });
}
return entries;
}
const metadata = new Metadata();
metadata.set("x-token", process.env.CK_API_KEY ?? "");
const stream: ClientReadableStream<{ slot: string; entries: Buffer }> =
client.subscribeEntries({}, metadata);
stream.on("data", (msg) => {
const entries = readEntries(msg.entries);
const transactions = entries.reduce((n, e) => n + e.transactions.length, 0);
console.log("slot", msg.slot, "entries", entries.length, "transactions", transactions);
});
stream.on("error", (err: Error) => console.error("stream error:", err.message));
stream.on("end", () => console.log("stream ended"));Running it
export CK_API_KEY="YOUR_API_KEY"
npx tsx stream.tsStream limit
A key may hold one ShredPrism stream in each region. A second stream on the same key, in the same region, is refused with gRPC status RESOURCE_EXHAUSTED (8) and the message Stream Limit Exceeded (Max 1). First in wins: the stream you already hold is never dropped to make room for a new one, and the second request is not queued behind it.
The slot is freed when the old request finishes, which is not always the moment your client gave up on it. A half-dead stream can hold the slot until the endpoint times it out, roughly five minutes after the last data. A reconnect that comes back with status 8 needs a backoff and a retry, not more connections.
Consumer requirements
- Fan-out inside your own infrastructure. Where several services need the feed, one consumer takes the stream and distributes the data to them.
- A close before every reconnect, so the new stream is not refused while the old one still counts.
- Backoff on reconnect. About 250 ms to start, doubling each time, capped at a few seconds.
- Proximity to the node, so the network does not eat the lead that shreds give.
- Everything in Delivery properties, which covers duplicates, slot order, incomplete slots and back-pressure.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
UNAUTHENTICATED (16), Missing or Invalid API Key | The x-token metadata is missing, misspelled or holds an unknown key. |
PERMISSION_DENIED (7), Prism requires Nexus Plan | The key is valid but the plan is below Nexus. |
RESOURCE_EXHAUSTED (8), Stream Limit Exceeded (Max 1) | A stream on this key is already open in this region, or an old one has not been released yet. Close it, or back off and retry. |
INTERNAL (13), Internal Server Error | Retry with backoff. If it persists, contact us. |
| Connection reset, or an HTTP/2 or protocol error right after connecting | The client is using plaintext. Turn on TLS. |
| The stream stops without an error after a quiet period | The endpoint ends a stream that has been silent for about five minutes. Reconnect. |
| Decoding fails, or transactions come out as nonsense | The client is using bincode. Decode with wincode and solana-entry 4.2 or later. |
| A decode returns zero entries | Valid. A payload can carry an empty entry list. |