Start with one complete request
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.
pnpm add @triton-one/yellowstone-grpcimport Client, {
CommitmentLevel,
type SubscribeRequest,
} from "@triton-one/yellowstone-grpc";
const endpoint = process.env.RPCEDGE_GRPC_ENDPOINT ?? "https://grpc.rpcedge.com:443";
const apiKey = process.env.RPCEDGE_API_KEY;
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 OR and AND rules
The rules become easier to reason about when each named filter is treated as a predicate.
Account filters
Inside one named account filter:
- Pubkeys in
accountare OR choices. - Pubkeys in
ownerare OR choices. - The
account,owner, andfiltersfields are combined with AND. - Every item inside
filtersmust match. Thememcmpanddatasizepredicates 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 A OR account B)
AND (owner X OR owner Y)
AND memcmp at offset 0
AND data size equals NNamed 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.
Transaction filters
Inside one named transaction filter:
accountIncludematches when any listed account is used.accountRequiredmatches only when all listed accounts are used.accountExcluderejects 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:
| Protobuf field | TypeScript field | Meaning |
|---|---|---|
account_include | accountInclude | Any listed account may match |
account_required | accountRequired | Every listed account must match |
account_exclude | accountExclude | Any listed account rejects the transaction |
accounts_data_slice | accountsDataSlice | Return selected byte ranges for account updates |
filter_by_commitment | filterByCommitment | Restrict slot updates to the request commitment |
Cookbook 1: subscribe to exact accounts
The account field fits known addresses, such as a set of pools, vaults, user positions, or wallet-owned
state accounts.
import { emptyRequest } from "./yellowstone-request";
const POOL_A = "ReplaceWithPoolAccountA";
const POOL_B = "ReplaceWithPoolAccountB";
const request = emptyRequest();
request.accounts.pools = {
account: [POOL_A, POOL_B],
owner: [],
filters: [],
};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.
Cookbook 2: subscribe by owner program
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.
import { emptyRequest } from "./yellowstone-request";
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
const request = emptyRequest();
request.accounts.tokenAccounts = {
account: [],
owner: [TOKEN_PROGRAM],
filters: [],
};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.
Cookbook 3: combine owner, memcmp, and data size
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.
import { emptyRequest } from "./yellowstone-request";
const PROGRAM_ID = "ReplaceWithProgramId";
const BASE58_DISCRIMINATOR = "ReplaceWithBase58EncodedBytes";
const EXPECTED_ACCOUNT_SIZE = "256";
const request = emptyRequest();
request.accounts.positions = {
account: [],
owner: [PROGRAM_ID],
filters: [
{
memcmp: {
offset: "0",
base58: BASE58_DISCRIMINATOR,
},
},
{ datasize: EXPECTED_ACCOUNT_SIZE },
],
};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.
Cookbook 4: use account data slices
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.
import { emptyRequest } from "./yellowstone-request";
const PROGRAM_ID = "ReplaceWithProgramId";
const request = emptyRequest();
request.accounts.positions = {
account: [],
owner: [PROGRAM_ID],
filters: [],
};
request.accountsDataSlice = [
{ offset: "0", length: "8" },
{ offset: "40", length: "16" },
];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.
Cookbook 5: include any watched account
Choose accountInclude for wallet monitoring or any workload interested in a transaction that mentions
one of several programs or accounts.
import { emptyRequest } from "./yellowstone-request";
const WATCHED_WALLET = "ReplaceWithWalletPubkey";
const SECOND_WALLET = "ReplaceWithSecondWalletPubkey";
const request = emptyRequest();
request.transactions.watchedWallets = {
vote: false,
failed: false,
signature: undefined,
accountInclude: [WATCHED_WALLET, SECOND_WALLET],
accountExclude: [],
accountRequired: [],
};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.
Cookbook 6: require accounts and exclude noise
Set accountRequired to demand every listed account. Add accountExclude to reject known routing,
program, or account patterns irrelevant to the consumer.
import { emptyRequest } from "./yellowstone-request";
const DEX_PROGRAM = "ReplaceWithDexProgramId";
const TARGET_MARKET = "ReplaceWithMarketAccount";
const EXCLUDED_ACCOUNT = "ReplaceWithExcludedAccount";
const request = emptyRequest();
request.transactions.marketActivity = {
vote: false,
failed: false,
signature: undefined,
accountInclude: [],
accountRequired: [DEX_PROGRAM, TARGET_MARKET],
accountExclude: [EXCLUDED_ACCOUNT],
};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.
Cookbook 7: choose vote and failed behavior explicitly
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.
Cookbook 8: subscribe to slot updates
Slot updates provide a lightweight chain clock for lag measurement, health checks, and checkpointing.
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.
Open the stream and handle writes
Writing the request is asynchronous. Treat a write error, stream error, end, or close as a connection failure that enters the same recovery path.
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 | null | undefined) => {
if (error) reject(error);
else resolve();
});
});
stream.on("data", (update: SubscribeUpdate) => {
void onUpdate(update).catch((error) => {
console.error("yellowstone update failed", error);
});
});
stream.on("error", (error) => console.error("yellowstone stream error", error));
stream.on("end", () => console.warn("yellowstone stream ended"));
stream.on("close", () => console.warn("yellowstone stream closed"));
return stream;
}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.
Backpressure is an application problem
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:
gRPC stream
-> validate update
-> bounded in-memory queue
-> fixed worker pool
-> idempotent state write
-> checkpointTrack 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.
Reconnect without pretending the stream is a database
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.
The Solana streaming decision matrix covers the broader choice between polling, WebSockets, gRPC, and earlier signal classes. For a protocol-level overview, read Yellowstone gRPC vs standard RPC.
Need a filtered Solana stream?
rpc edge provides Yellowstone-compatible gRPC alongside Solana RPC and transaction infrastructure. Choose a plan for your bot or application workload.
A production review checklist
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.