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

SettingValue
HostThe ShredPrism host for your region. See Endpoints.
Port55577
TransportgRPC over HTTP/2 with TLS. Plaintext connections are not accepted.
AuthenticationMetadata header x-token: YOUR_API_KEY
StreamsOne 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 packageshredstream (proto3)
Serviceshredstream.ShredstreamProxy
MethodSubscribeEntries, server streaming
RequestSubscribeEntriesRequest, an empty message. There are no filters: you receive the whole feed.
Response streamEntry, with a slot and an entries byte string
Full path/shredstream.ShredstreamProxy/SubscribeEntries
API keyMetadata 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:

proto/shredstream.proto
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

Cargo.toml
[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:

build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_prost_build::configure()
        .build_server(false)
        .compile_protos(&["proto/shredstream.proto"], &["proto"])?;
    Ok(())
}

The consumer

src/main.rs
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 --release

TypeScript 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/kit

The proto above goes in proto/shredstream.proto.

The consumer

stream.ts
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.ts

Stream 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

SymptomCause and fix
UNAUTHENTICATED (16), Missing or Invalid API KeyThe x-token metadata is missing, misspelled or holds an unknown key.
PERMISSION_DENIED (7), Prism requires Nexus PlanThe 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 ErrorRetry with backoff. If it persists, contact us.
Connection reset, or an HTTP/2 or protocol error right after connectingThe client is using plaintext. Turn on TLS.
The stream stops without an error after a quiet periodThe endpoint ends a stream that has been silent for about five minutes. Reconnect.
Decoding fails, or transactions come out as nonsenseThe client is using bincode. Decode with wincode and solana-entry 4.2 or later.
A decode returns zero entriesValid. A payload can carry an empty entry list.

On this page