🔌 gRPC-Zustellung

Ein Decoded Shred Stream kann statt über UDP auch über gRPC konsumiert werden: ein ausgehender, verbindungsorientierter Stream, der über dieselben Transaktionen mit Latenz im Submillisekundenbereich ein serverseitiges Account-Filtering und eine geordnete Auslieferung über HTTP/2 legt — ohne öffentlich erreichbaren UDP-Endpunkt. Ihr Dashboard zeigt für jeden Stream den gRPC-Endpunkt und das Zugriffstoken an.

  • Endpunkt — <host>:50051 (ein Port fĂĽr alle Flows).
  • Transport — gRPC ĂĽber HTTP/2, im Klartext h2c (kein TLS). Das ist eine bewusste Latenz-Entscheidung: Es gibt keinen TLS-Handshake zu bezahlen. Isolieren Sie die Verbindung auf Netzwerkebene — privates Netz, VPN oder Peering — und geben Sie sie niemals ins offene Internet frei.
  • Routen — shredstream.com.DecodedShredStreamService/SubscribeDecodedTransactions oder die drop-in-kompatible Route shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions. Beide sind strikt äquivalent (dieselben protobuf-Nachrichten, Byte fĂĽr Byte identischer Inhalt) — ein Client, der von einem Shreder- / Raiden-Pulse-Endpunkt kommt, muss nur die Adresse ändern.

🔑 Authentifizierung

Jeder Aufruf muss Ihr Token in den gRPC-Metadaten tragen, in einer der beiden folgenden Formen (beide werden akzeptiert):

authorization: Bearer <TOKEN>
x-token: <TOKEN>

Ein fehlendes oder ungĂĽltiges Token wird mit einem einheitlichen UNAUTHENTICATED-Status und der Nachricht authentication refused abgelehnt. PrĂĽfen Sie Ihr Token.

Denken Sie daran: eine Verbindung pro Token. Das Öffnen eines zweiten Streams mit demselben Token verdrängt den ältesten.


đź§­ Abonnement-Modell

Die Route ist bidirektionales Streaming (stream request → stream response):

  1. Öffnen Sie den Stream und senden Sie mindestens eine Anfrage mit einer Map benannter Filter: { "<name>": <filter>, … }.
  2. Der Server liefert nur die Transaktionen, die mindestens einen Filter erfüllen. Jede Antwort ist mit dem/den Namen des/der erfüllten Filter(s) getaggt (das Feld filters) — so können Sie mehrere Strategien über einen einzigen Stream routen.
  3. Sie können jederzeit eine neue Map senden: Sie ersetzt die vorherige im laufenden Betrieb, ohne Reconnect und ohne Lücke im Flow.

Randfälle:

  • Leere Map ({}) ⇒ es wird nichts geliefert (Sie abonnieren Filter, keinen Firehose).
  • Leerer benannter Filter ({ "all": {} }) ⇒ jede Transaktion passiert, getaggt mit "all".
  • Sobald Ihre Filter gesendet sind, mĂĽssen Sie nichts mehr senden: Der Stream liefert mit diesen Filtern weiter, bis Sie neue senden oder die Verbindung schlieĂźen.

🎯 Account-Filter

Jeder benannte Filter besteht aus drei Listen von base58-Public-Keys, kombiniert mit logischem UND:

FeldBedeutung
account_includedie Transaktion muss mindestens einen dieser Accounts berühren (leer = keine Einschränkung)
account_excludedie Transaktion darf keinen dieser Accounts berĂĽhren
account_requireddie Transaktion muss alle diese Accounts berĂĽhren

Für Transaktionen wird „berühren" auf den statischen Account-Keys der Transaktion ausgewertet (Signer eingeschlossen). Über Address Lookup Tables (ALTs) aufgelöste Adressen sind nicht im Transaktions-Umschlag enthalten und daher nicht filterbar — filtern Sie nur auf statischen Keys.


📦 Der Payload

Jede Antwort trägt die Transaktion als rohe Bytes zusammen mit ihrem Slot:

proto
message SubscribeUpdateBinaryTransaction {
BinaryTransaction transaction = 1;
uint64 slot = 2; // Solana-Slot
}
message BinaryTransaction {
repeated bytes signatures = 1; // Signaturen (je 64 Bytes)
bytes binary_transaction = 3; // VersionedTransaction, bincode, VERBATIM
}

binary_transaction ist das Standard-Solana-Wire-Format — eine bincode-serialisierte VersionedTransaction, unangetastet. Deserialisieren Sie sie mit einem beliebigen Solana-SDK und geben Sie sie an Ihren bestehenden Parser, genau wie im UDP-Modus. Vote-Transaktionen sind standardmäßig ausgeschlossen.


đź’» Offizieller Client

Die offiziellen decoded-shredstream-Clients sprechen diese Route für Sie — Verbindung, Token in den Metadaten, Filter-Map, Reconnect mit erneutem Senden der aktuellen Filter sowie die Transaktion mit ihrem Slot und ihren Signaturen. Dasselbe Paket für UDP und 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)
})

Filter können auf einem laufenden Stream ersetzt werden (updateFilters / update_filters), ohne Reconnect. Behebbare Unterbrechungen erreichen Sie nie — der Client verbindet sich neu und sendet die aktuelle Filter-Map erneut; nur ein abgelehntes Token, eine vom Server geschlossene Sitzung oder eine zurückgewiesene Filter-Map beenden den Stream, über einen Fehler, den Sie einmalig behandeln.


đź’» Standard-gRPC-Clients (generierte Stubs)

Für Teams, die ihren eigenen gRPC-Stack bevorzugen: Der Vertrag ist decoded.proto (unsere Route, shredstream.com.DecodedShredStreamService), die shreder_binary.proto für ihre Nachrichten importiert — beide Dateien liegen jedem offiziellen Client bei. Wer Stubs allein aus shreder_binary.proto generiert, erhält die unten verwendete drop-in-kompatible Route.

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![],
},
)]);
// Statische Filter: eine Anfrage senden, dann half-close (der Iterator endet).
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)?;
// → nach resp.filters routen, vtx verarbeiten …
}
}

Unsere äquivalente Route ist DecodedShredStreamServiceClient::subscribe_decoded_transactions, mit exakt denselben Request-/Response-Typen. Um Filter im laufenden Stream zu aktualisieren, ersetzen Sie tokio_stream::iter durch einen Kanal, den Sie offen halten und auf dem Sie eine neue Anfrage per send schicken — ohne Reconnect.

TypeScript (@grpc/grpc-js)

ts
import { Metadata } from "@grpc/grpc-js";
// … Client generiert aus 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, falls die Filter sich nie ändern
call.on("data", (resp) => {
const raw = resp.transaction?.transaction?.binaryTransaction; // Buffer
// eine VersionedTransaction clientseitig deserialisieren …
});
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
# eine VersionedTransaction deserialisieren …

Schnelltest mit grpcurl

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

đź§Ż Fehlerbehandlung & Reconnect

gRPC-StatusBedeutungWas tun
UNAUTHENTICATEDToken fehlt / ungültig / falscher FlowToken oder Flow korrigieren — nicht blind erneut versuchen
INVALID_ARGUMENTungĂĽltiges base58, zu viele Pubkeys, zu viele benannte Filterden fehlerhaften Filter korrigieren
DATA_LOSSIhr Consumer ist zu langsam — die Sende-Queue des Servers ist übergelaufen, der Stream wurde geschlossenneu verbinden und Ihre Filter-Map erneut senden
PERMISSION_DENIEDvom Betreiber getrennt/widerrufennicht in einer Schleife wiederholen; den Betreiber kontaktieren

Der Dienst ist ausschließlich Echtzeit — kein Replay, kein Backfill. Bei DATA_LOSS oder einem Transport-Abbruch verbinden Sie sich mit exponentiellem Backoff (und etwas Jitter) neu und senden Ihre Filter-Map erneut; eine kurze Datenlücke während des Reconnects ist normal. Wegen der Regel eine-Verbindung-pro-Token stellen Sie sicher, dass pro Token nur eine Reconnect-Schleife läuft.

Um DATA_LOSS von vornherein zu vermeiden, konsumieren Sie den Stream ohne zu blockieren: Lagern Sie schwere Verarbeitung (einschließlich der bincode-Deserialisierung) an eine Queue oder einen Worker-Pool aus, damit Ihre Empfangsschleife nie stockt. gRPC über HTTP/2 garantiert die Reihenfolge und Integrität von allem, was gesendet wird — der einzig mögliche Verlust ist der Drop „Client zu langsam", und er wird immer explizit durch DATA_LOSS signalisiert.


⚖️ gRPC oder UDP?

gRPCUDP
LatenzNiedrig — h2c, kein TLS-HandshakeAm niedrigsten — binäre Datagramme, keine Verbindung
ZustellungGeordnet, zuverlässig; „Client zu langsam" durch DATA_LOSS signalisiertBest-Effort-Push; Verlust möglich, erkannt über seq-Lücken
FilterungAccount-Filter serverseitig (statische Keys)Keine — clientseitig nach dem Empfang filtern
ErreichbarkeitAusgehende Verbindung — funktioniert hinter NAT/FirewallsErfordert einen öffentlich erreichbaren UDP-Endpunkt
PayloadVersionedTransaction (bincode)VersionedTransaction (bincode), in einem 16-Byte-Umschlag

Wählen Sie gRPC, wenn Sie serverseitiges Filtern, geordnete Zustellung und NAT-freundliche Konnektivität wollen. Wählen Sie UDP für die absolut niedrigste Latenz, wenn Sie einen erreichbaren Endpunkt haben und Verluste selbst überwachen. Der Payload ist in beiden Fällen identisch.


➡️ Nächste Schritte

gRPC-Zustellung — Docs | ShredStream.com