Yellowstone gRPC Filters for Solana: A TypeScript Cookbook
Copyable Yellowstone gRPC filters for Solana accounts, owners, memcmp data, transactions, slots, and account data slices, with the production rules that keep streams narrow and recoverable.
Rrpc edge Team·Jul 22, 2026·13 min read
Frequently asked questions
How do Yellowstone gRPC account filters work?
Inside one named account filter, different fields are combined with AND. Values inside the account and owner arrays are combined with OR, while multiple memcmp and data-size filters must all match. Separate named filters act as independent subscriptions on the same stream.
What is the difference between accountInclude, accountRequired, and accountExclude?
accountInclude matches a transaction that mentions any listed account. accountRequired matches only when every listed account is present. accountExclude rejects a transaction when any listed account is present. When supplied together, all three conditions must pass.
Can Yellowstone gRPC filter failed and vote transactions?
Yes. Transaction filters expose optional vote and failed booleans. Setting both to false selects successful, non-vote transactions. Leaving either value undefined removes that property as a constraint.
How can I reduce Yellowstone gRPC bandwidth?
Filter on the server, avoid empty wildcard filters, use account data slices when only fixed byte ranges are required, and split unrelated workloads. Data slices reduce account payload bytes but do not change which accounts match.
How should a Yellowstone gRPC client reconnect?
Treat the stream as a live feed rather than durable storage. Persist a checkpoint, reconnect with bounded exponential backoff and jitter, rebuild state from RPC when required, resubscribe, deduplicate updates, and monitor queue depth so a slow consumer does not silently fall behind.
Yellowstone gRPC is a streaming interface built around Solana Geyser. A client opens one bidirectional
subscription, writes a SubscribeRequest, and receives matching updates. The protocol definition lives
in the official
geyser.proto,
while the official
rpcpool/yellowstone-grpc repository
documents the filter behavior. Triton One also maintains the upstream
Dragon's Mouth subscription documentation
with request examples and current protocol notes.
Install a compatible TypeScript client in your application, then create a complete request object. The
examples below use @triton-one/yellowstone-grpc, the client shown in the upstream project.
install.sh
pnpm add @triton-one/yellowstone-grpc
yellowstone-request.ts
import Client, { CommitmentLevel, type SubscribeRequest,
if (!apiKey) throw new Error("RPCEDGE_API_KEY is required");
export const client = new Client(endpoint, apiKey, {
grpcMaxDecodingMessageSize: 64 * 1024 * 1024,
});
export function emptyRequest(): SubscribeRequest {
return {
accounts: {},
slots: {},
transactions: {},
transactionsStatus: {},
blocks: {},
blocksMeta: {},
entry: {},
accountsDataSlice: [],
commitment: CommitmentLevel.PROCESSED,
ping: undefined,
};
}
An empty map means that update type is not requested. By contrast, an empty named filter, such as
accounts: { everything: { account: [], owner: [], filters: [] } }, can request every account update
if the server permits it. That is rarely a sensible starting point. Providers can also enforce filter
count, account count, and wildcard limits, so a request accepted by one endpoint may be rejected by
another.
The cheapest byte to process is the byte the server never sends.
The account, owner, and filters fields are combined with AND.
Every item inside filters must match. The memcmp and datasize predicates live here.
In plain language, the filter means account A or account B, owned by program X or program Y,
with every data predicate matching.
account-filter-logic.txt
(account A OR account B)AND (owner X OR owner Y)AND memcmp at offset 0AND data size equals N
Named filters in the accounts map are independent. Updates can match one or more names,
and the response carries the matching filter labels. Meaningful names make those labels useful for
downstream routing.
accountInclude matches when any listed account is used.
accountRequired matches only when all listed accounts are used.
accountExclude rejects a transaction when any listed account is used.
vote, failed, signature, and the three account conditions are combined with AND when present.
The official upstream README summarizes the same behavior and notes that an empty transaction filter
broadcasts all transactions when the server allows it. Empty wildcards can produce far more traffic
than expected.
The protobuf uses snake-case field names, while the generated TypeScript client uses camel case:
The two addresses are OR choices, so the subscription receives changes for either account. A bot should
track only markets it can act on. Applications with large lists should partition them by workload so one
hot group cannot starve every other consumer.
The owner field receives account changes for one or more programs. It starts from a program-owned
account set rather than a transaction log subscription.
Owner-based subscriptions can still be broad. The SPL Token program owns a large account population. Add a discriminator,
data size, or exact account list when the application does not need every update. A provider may also
reject a broad owner filter under its server policy.
memcmp compares bytes at a fixed offset in account data. It is useful for discriminators, embedded
pubkeys, status flags, and other fixed-layout fields. datasize restricts matches to accounts with one
exact data length.
The owner, memcmp, and size checks are all required. If you add a second memcmp, both byte comparisons
must match. Confirm offsets against the program's current account layout. Offsets copied from an older
IDL can create a healthy-looking stream that emits nothing.
The protobuf supports memcmp data as raw bytes, base58, or base64. The TypeScript client shape can vary
slightly between generated-client versions, so pin the package version and let its exported
SubscribeRequest type check the request.
Account updates include the full account data by default. When a consumer only needs a fixed header or
one fixed-width field, accountsDataSlice can reduce the returned payload.
Slices change the bytes returned for account updates without changing which accounts match. Their scope
covers every account subscription in the request, so consumers requiring incompatible payloads should
not share a stream. Put a full-body decoder on a different stream from a consumer that needs only a header.
The result contains successful, non-vote transactions that mention wallet A or wallet B. A mention
does not prove that either wallet signed. Inspect the transaction message and signer positions before
treating a mention as authorization or intent.
In a copy-trading workflow, the filter supplies detection input rather than strategy logic. The application still
needs program decoding, allowlists, sizing, slippage controls, and finality reconciliation. See the
copy-trading infrastructure guide for the wider loop.
Set accountRequired to demand every listed account. Add accountExclude to reject known routing,
program, or account patterns irrelevant to the consumer.
Both the program and market must be present, and the excluded account must be absent, for this filter to
accept a successful, non-vote transaction. Populating accountInclude would add another condition: one include value
would have to match in addition to every required value.
The vote and failed fields are optional booleans, not generic switches for the whole stream.
Filter values
Result
vote: false, failed: false
Successful, non-vote transactions
vote: true, failed: false
Successful vote transactions
vote: false, failed: true
Failed, non-vote transactions
vote: undefined, failed: undefined
Do not constrain either property
Failed transactions can be valuable for execution analytics and debugging. They are usually noise for a
consumer that only reacts to successful state transitions. Split these workloads rather than making the
strategy hot path branch over every failure.
Slot updates provide a lightweight chain clock for lag measurement, health checks, and checkpointing.
slot-filter.ts
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";import { emptyRequest } from "./yellowstone-request";const request = emptyRequest();request.commitment = CommitmentLevel.CONFIRMED;request.slots.clock = { filterByCommitment: true,};
With filterByCommitment: true, slot updates are restricted to the request's selected commitment. The
protocol also exposes interslot updates in current versions. Only enable event classes that your client
understands, and pin the client version because protocol fields evolve.
Commitment is part of the data contract. processed arrives earlier but can roll back. confirmed and
finalized trade later delivery for stronger cluster agreement. The
Solana commitment-level guide explains how to choose a level
for user interfaces, indexers, and bots.
Writing the request is asynchronous. Treat a write error, stream error, end, or close as a connection
failure that enters the same recovery path.
subscribe.ts
import type { SubscribeRequest, SubscribeUpdate } from "@triton-one/yellowstone-grpc";import { client } from "./yellowstone-request";export async function subscribe( request: SubscribeRequest, onUpdate: (update: SubscribeUpdate) => Promise<void>,) { await client.connect(); const stream = await client.subscribe(); await new Promise<void>((resolve, reject) => { stream.write(request, (error: Error |
The example supplies a transport skeleton, not a complete supervisor. Production code should route terminal events
to one reconnect controller, prevent concurrent reconnect attempts, and shut down the old stream before
opening another.
Server-side filters reduce input, but the client must still drain what it requested. A callback that
starts unlimited promises can exhaust memory while appearing responsive.
Place a bounded queue between the stream and application logic:
bounded-pipeline.txt
gRPC stream -> validate update -> bounded in-memory queue -> fixed worker pool -> idempotent state write -> checkpoint
Track queue depth, oldest queued update age, processing duration, decode failures, and the gap between the
latest observed slot and the cluster slot. Decide what overload means before it happens:
Disconnect and rebuild state when completeness matters.
Drop replaceable intermediate account states when only the latest value matters.
Never silently drop transaction events when downstream accounting assumes completeness.
Separate hot trading or application paths from archival and analytics consumers.
Continuous queue growth calls for a narrower filter, faster consumer, more
workers with safe ordering, or a durable buffer. A larger unbounded queue only delays the failure.
A live Yellowstone stream does not automatically give every application durable replay. Network loss,
process restarts, provider maintenance, and consumer overload can create gaps. Build recovery around the
workload's required truth.
Persist a checkpoint such as the latest completely applied slot and relevant signatures.
On failure, stop accepting updates from the old stream.
Reconnect with bounded exponential backoff and jitter.
Rebuild or reconcile state through RPC or a durable store when the workload cannot tolerate gaps.
Resubscribe with the same versioned filter configuration.
Deduplicate by a workload-appropriate identity, such as signature plus slot or account plus write
version.
Mark the consumer healthy only after it has caught up.
Current Yellowstone versions include from_slot in the protobuf, exposed as fromSlot by the
TypeScript client. A server with replay enabled lets a reconnecting client request buffered
updates from a recent slot and then continue into the live stream. The retention window is finite and
server-dependent. Query replay availability when the endpoint exposes it, handle a request older than
the retained window, and still deduplicate the checkpoint slot because replay starts on a slot boundary.
Do not assume replay is enabled without confirming it with the provider.
A subscription update replaces the previous subscription configuration. Keep the complete desired
request in local state and write the full request when adding or removing a filter. Sending only the new
filter can unintentionally unsubscribe the existing filters.
The upstream protocol includes ping messages for connections that would otherwise be closed by an idle
load balancer. Follow the client's current ping example rather than using pings as a substitute for a
reconnect supervisor. A connection can be alive while the consumer is still falling behind.
Before shipping a subscription, record these decisions in the pull request:
Assign every update type to a named filter.
Prove that no named filter becomes an accidental wildcard.
Document the OR and AND conditions.
Tie memcmp offsets, discriminators, and data sizes to a pinned layout version.
Confirm that each data slice includes every byte read by its decoder.
State the commitment level and rollback policy.
Set the maximum queue size and overload policy.
Preserve processing order wherever the workload requires it.
Define the persisted checkpoint.
Specify the gap-repair procedure after reconnect.
Select a stable deduplication identity.
Monitor metrics that distinguish a current consumer from a connected one.
The practical pattern is simple: subscribe narrowly, give filters useful labels, bound the consumer, and make
state repair explicit. The result is a stream an application can operate, rather than a demo that prints
updates.