π gRPC Delivery
A Decoded Shred Stream can be consumed over gRPC instead of UDP: an outbound, connection-oriented stream that adds server-side account filtering and an ordered HTTP/2 delivery on top of the same sub-millisecond-latency transactions β no publicly reachable UDP endpoint required. Your dashboard shows the gRPC endpoint and the access token for each stream.
- Endpoint β
<host>:50051(one port for every flow). - Transport β gRPC over HTTP/2, plaintext h2c (no TLS). This is a deliberate latency choice: there is no TLS handshake to pay for. Isolate the link at the network layer β private network, VPN, or peering β never expose it to the open internet.
- Routes β
shredstream.com.DecodedShredStreamService/SubscribeDecodedTransactions, or the drop-in compatibleshreder_binary.ShrederBinaryService/SubscribeBinaryTransactions. Both are strictly equivalent (same protobuf messages, byte-for-byte identical content) β a client coming from a Shreder / Raiden Pulse endpoint only has to change the address.
π Authentication
Every call must carry your token in the gRPC metadata, in either of these two forms (both are accepted):
authorization: Bearer <TOKEN> x-token: <TOKEN>
A missing or invalid token is rejected with a uniform UNAUTHENTICATED status and the message authentication refused. Check your token.
Remember: one connection per token. Opening a second stream with the same token evicts the oldest one.
π§ Subscription Model
The route is bidirectional streaming (stream request β stream response):
- Open the stream and send at least one request carrying a map of named filters:
{ "<name>": <filter>, β¦ }. - The server delivers only the transactions that match at least one filter. Each response is tagged with the name(s) of the filter(s) it satisfied (the
filtersfield) β so you can route several strategies over a single stream. - You may send a new map at any time: it replaces the previous one hot, with no reconnection and no gap in the flow.
Edge cases:
- Empty map (
{}) β nothing is delivered (you subscribe to filters, not to a firehose). - Empty named filter (
{ "all": {} }) β every transaction passes, tagged"all". - Once your filters are sent, you have nothing more to send: the stream keeps delivering with those filters until you send new ones or close the connection.
π― Account Filters
Each named filter is three lists of base58 public keys, combined with logical AND:
| Field | Meaning |
|---|---|
account_include | the transaction must touch at least one of these accounts (empty = no constraint) |
account_exclude | the transaction must touch none of these accounts |
account_required | the transaction must touch all of these accounts |
For transactions, "touch" is evaluated on the transaction's static account keys (signers included). Addresses resolved through Address Lookup Tables (ALTs) are not present in the transaction envelope and are therefore not filterable β filter on static keys only.
π¦ The Payload
Each response carries the transaction as raw bytes plus its slot:
message SubscribeUpdateBinaryTransaction {BinaryTransaction transaction = 1;uint64 slot = 2; // Solana slot}message BinaryTransaction {repeated bytes signatures = 1; // signatures (64 bytes each)bytes binary_transaction = 3; // VersionedTransaction, bincode, VERBATIM}
binary_transaction is the standard Solana wire format β a bincode-serialized VersionedTransaction, untouched. Deserialize it with any Solana SDK and feed it to your existing parser, exactly as with the UDP mode. Vote transactions are excluded by default.
π» Official Client
The official decoded-shredstream clients speak this route for you β connection, token metadata, filter map, reconnection with the current filters re-sent, and the transaction with its slot and signatures. Same package for UDP and gRPC.
// npm install decoded-shredstreamimport { DecodedShredStream, FilterAll } from "decoded-shredstream";const client = await DecodedShredStream.grpc({endpoint: "<host>:50051",token: process.env.DECODED_SHREDSTREAM_TOKEN!,filters: { all: FilterAll }, // or: { "watched-wallet": { include: [wallet] } }});for await (const tx of client.transactions()) {console.log(`slot=${tx.slot} sig=${tx.signature.toBase58()} matched=${JSON.stringify(tx.filters)}`);}
# pip install decoded-shredstreamfrom decoded_shredstream import Client, Filter, GrpcConfigwith Client.grpc(GrpcConfig(endpoint="<host>:50051", token="<TOKEN>",filters={"all": Filter()})) as client:for update in client:print(update.slot, len(update.data), list(update.filters))
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-threaduse decoded_shredstream::{Filter, GrpcClient, GrpcConfig};let mut client = GrpcClient::connect(GrpcConfig::new("<host>:50051", "<TOKEN>").filter("all", Filter::all()),).await?;while let Some(update) = client.next_update().await {let update = update?;println!("slot={} sig={} matched={:?}", update.slot(), update.signature(), update.filters());}
// go get github.com/shredstream/decoded-shredstream-goclient, err := decodedshredstream.NewGRPC(decodedshredstream.GRPCConfig{Endpoint: "<host>:50051",Token: "<TOKEN>",Filters: decodedshredstream.Filters{"all": decodedshredstream.FilterAll()},})if err != nil { log.Fatal(err) }defer client.Close()err = client.Run(ctx, func(u *decodedshredstream.TransactionUpdate) {sig, _ := u.Signature()fmt.Println(u.Slot, sig, u.Filters)})
Filters can be replaced on a live stream (updateFilters / update_filters) with no reconnection. Recoverable interruptions never reach you β the client reconnects and re-sends the current filter map; only a refused token, a session closed by the server or a rejected filter map end the stream, through an error you handle once.
π» Standard gRPC Clients (generated stubs)
For teams that prefer their own gRPC stack: the contract is decoded.proto (our route, shredstream.com.DecodedShredStreamService), which imports shreder_binary.proto for its messages β both files ship with every official client. Generating stubs from shreder_binary.proto alone gives you the drop-in compatible route used below.
Rust (tonic)
use std::collections::HashMap;use shreder_binary::shreder_binary_service_client::ShrederBinaryServiceClient;use shreder_binary::{SubscribeBinaryTransactionsRequest, SubscribeRequestFilterBinaryTransactions};use solana_transaction::versioned::VersionedTransaction;use tonic::Request;let mut client = ShrederBinaryServiceClient::connect("http://<host>:50051").await?;let filters = HashMap::from([("pumpfun".to_string(),SubscribeRequestFilterBinaryTransactions {account_include: vec!["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".into()],account_exclude: vec![],account_required: vec![],},)]);// Static filters: emit one request, then half-close (the iterator ends).let outbound = tokio_stream::iter(vec![SubscribeBinaryTransactionsRequest { transactions: filters }]);let mut req = Request::new(outbound);req.metadata_mut().insert("authorization", "Bearer <TOKEN>".parse()?);let mut stream = client.subscribe_binary_transactions(req).await?.into_inner();while let Some(resp) = stream.message().await? {if let Some(tx) = resp.transaction.and_then(|u| u.transaction) {let vtx: VersionedTransaction = bincode::deserialize(&tx.binary_transaction)?;// β route on resp.filters, process vtx β¦}}
Our equivalent route is
DecodedShredStreamServiceClient::subscribe_decoded_transactions, with exactly the same request/response types. To update filters mid-stream, replacetokio_stream::iterwith a channel you keep open andsenda new request on it β no reconnection required.
TypeScript (@grpc/grpc-js)
import { Metadata } from "@grpc/grpc-js";// β¦ client generated from shreder_binary.proto β¦const meta = new Metadata();meta.set("x-token", "<TOKEN>");const call = client.subscribeBinaryTransactions(meta);call.write({transactions: { pumpfun: { accountInclude: ["6EF8rrecβ¦"], accountExclude: [], accountRequired: [] } },});// call.end(); // half-close if the filters never changecall.on("data", (resp) => {const raw = resp.transaction?.transaction?.binaryTransaction; // Buffer// deserialize a VersionedTransaction client-side β¦});call.on("error", (e) => { /* DATA_LOSS / UNAUTHENTICATED β reconnect / log */ });
Python (grpcio)
import grpcimport shreder_binary_pb2 as pb, shreder_binary_pb2_grpc as rpcchan = grpc.insecure_channel("<host>:50051")stub = rpc.ShrederBinaryServiceStub(chan)def requests():yield pb.SubscribeBinaryTransactionsRequest(transactions={"pumpfun": pb.SubscribeRequestFilterBinaryTransactions(account_include=["6EF8rrecβ¦"])})md = (("x-token", "<TOKEN>"),)for resp in stub.SubscribeBinaryTransactions(requests(), metadata=md):raw = resp.transaction.transaction.binary_transaction # bincode bytes# deserialize a VersionedTransaction β¦
Quick test with grpcurl
grpcurl -plaintext -proto shreder_binary.proto \-H 'x-token: <TOKEN>' \-d '{"transactions":{"all":{}}}' \<host>:50051 shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions
π§― Error Handling & Reconnection
| gRPC status | Meaning | What to do |
|---|---|---|
UNAUTHENTICATED | token missing / invalid / wrong flow | fix the token or the flow β do not retry blindly |
INVALID_ARGUMENT | bad base58, too many pubkeys, too many named filters | fix the offending filter |
DATA_LOSS | your consumer is too slow β the server's send queue overflowed and the stream was closed | reconnect and resend your filter map |
PERMISSION_DENIED | disconnected/revoked by the operator | do not loop; contact the operator |
The service is real-time only β there is no replay or backfill. On DATA_LOSS or a transport drop, reconnect with exponential backoff (and a little jitter), then resend your filter map; a short data gap during the reconnect is expected. Because of the one-connection-per-token rule, make sure only one reconnection loop runs per token.
To avoid DATA_LOSS in the first place, drain the stream without blocking: hand heavy processing (including bincode deserialization) off to a queue or worker pool so your receive loop never stalls. gRPC over HTTP/2 guarantees the order and integrity of everything emitted β the only possible loss is the "client too slow" drop, and it is always signalled explicitly by DATA_LOSS.
βοΈ gRPC or UDP?
| gRPC | UDP | |
|---|---|---|
| Latency | Low β h2c, no TLS handshake | Lowest β binary datagrams, no connection |
| Delivery | Ordered, reliable; "client too slow" signalled by DATA_LOSS | Best-effort push; loss possible, detected via seq gaps |
| Filtering | Server-side account filters (static keys) | None β filter client-side after receiving |
| Reachability | Outbound connection β works behind NAT/firewalls | Requires a publicly reachable UDP endpoint |
| Payload | VersionedTransaction (bincode) | VersionedTransaction (bincode), in a 16-byte envelope |
Choose gRPC when you want server-side filtering, ordered delivery, and NAT-friendly connectivity. Choose UDP for the absolute lowest latency when you have a reachable endpoint and monitor loss yourself. The payload is identical either way.
β‘οΈ Next Steps
- Receiving Decoded Transactions β the UDP envelope, offsets, gap detection, and a minimal decoder.
- Decoded Shred Stream β positioning, latency, and delivery modes.