πŸ”Œ 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 compatible shreder_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):

  1. Open the stream and send at least one request carrying a map of named filters: { "<name>": <filter>, … }.
  2. 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 filters field) β€” so you can route several strategies over a single stream.
  3. 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:

FieldMeaning
account_includethe transaction must touch at least one of these accounts (empty = no constraint)
account_excludethe transaction must touch none of these accounts
account_requiredthe 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:

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

ts
// npm install decoded-shredstream
import { 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)}`);
}
python
# pip install decoded-shredstream
from decoded_shredstream import Client, Filter, GrpcConfig
with 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))
rust
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-thread
use 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());
}
Gogo
// go get github.com/shredstream/decoded-shredstream-go
client, 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)

rust
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, replace tokio_stream::iter with a channel you keep open and send a new request on it β€” no reconnection required.

TypeScript (@grpc/grpc-js)

ts
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 change
call.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)

python
import grpc
import shreder_binary_pb2 as pb, shreder_binary_pb2_grpc as rpc
chan = 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

bash
grpcurl -plaintext -proto shreder_binary.proto \
-H 'x-token: <TOKEN>' \
-d '{"transactions":{"all":{}}}' \
<host>:50051 shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions

🧯 Error Handling & Reconnection

gRPC statusMeaningWhat to do
UNAUTHENTICATEDtoken missing / invalid / wrong flowfix the token or the flow β€” do not retry blindly
INVALID_ARGUMENTbad base58, too many pubkeys, too many named filtersfix the offending filter
DATA_LOSSyour consumer is too slow β€” the server's send queue overflowed and the stream was closedreconnect and resend your filter map
PERMISSION_DENIEDdisconnected/revoked by the operatordo 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?

gRPCUDP
LatencyLow β€” h2c, no TLS handshakeLowest β€” binary datagrams, no connection
DeliveryOrdered, reliable; "client too slow" signalled by DATA_LOSSBest-effort push; loss possible, detected via seq gaps
FilteringServer-side account filters (static keys)None β€” filter client-side after receiving
ReachabilityOutbound connection β€” works behind NAT/firewallsRequires a publicly reachable UDP endpoint
PayloadVersionedTransaction (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

gRPC Delivery β€” Docs | ShredStream.com