🔌 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/SubscribeDecodedTransactionsoder die drop-in-kompatible Routeshreder_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):
- Ă–ffnen Sie den Stream und senden Sie mindestens eine Anfrage mit einer Map benannter Filter:
{ "<name>": <filter>, … }. - 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. - 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:
| Feld | Bedeutung |
|---|---|
account_include | die Transaktion muss mindestens einen dieser Accounts berühren (leer = keine Einschränkung) |
account_exclude | die Transaktion darf keinen dieser Accounts berĂĽhren |
account_required | die 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:
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.
// 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)})
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)
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 Sietokio_stream::iterdurch einen Kanal, den Sie offen halten und auf dem Sie eine neue Anfrage persendschicken — ohne Reconnect.
TypeScript (@grpc/grpc-js)
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 änderncall.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)
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# eine VersionedTransaction deserialisieren …
Schnelltest mit grpcurl
grpcurl -plaintext -proto shreder_binary.proto \-H 'x-token: <TOKEN>' \-d '{"transactions":{"all":{}}}' \<host>:50051 shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions
đź§Ż Fehlerbehandlung & Reconnect
| gRPC-Status | Bedeutung | Was tun |
|---|---|---|
UNAUTHENTICATED | Token fehlt / ungültig / falscher Flow | Token oder Flow korrigieren — nicht blind erneut versuchen |
INVALID_ARGUMENT | ungĂĽltiges base58, zu viele Pubkeys, zu viele benannte Filter | den fehlerhaften Filter korrigieren |
DATA_LOSS | Ihr Consumer ist zu langsam — die Sende-Queue des Servers ist übergelaufen, der Stream wurde geschlossen | neu verbinden und Ihre Filter-Map erneut senden |
PERMISSION_DENIED | vom Betreiber getrennt/widerrufen | nicht 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?
| gRPC | UDP | |
|---|---|---|
| Latenz | Niedrig — h2c, kein TLS-Handshake | Am niedrigsten — binäre Datagramme, keine Verbindung |
| Zustellung | Geordnet, zuverlässig; „Client zu langsam" durch DATA_LOSS signalisiert | Best-Effort-Push; Verlust möglich, erkannt über seq-Lücken |
| Filterung | Account-Filter serverseitig (statische Keys) | Keine — clientseitig nach dem Empfang filtern |
| Erreichbarkeit | Ausgehende Verbindung — funktioniert hinter NAT/Firewalls | Erfordert einen öffentlich erreichbaren UDP-Endpunkt |
| Payload | VersionedTransaction (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
- Dekodierte Transaktionen empfangen — der UDP-Umschlag, die Offsets, Verlusterkennung und ein minimaler Decoder.
- Decoded Shred Stream — Einordnung, Latenz und Auslieferungsmodi.