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.

install.sh
pnpm add @triton-one/yellowstone-grpc
yellowstone-request.ts
import 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 account are OR choices.
  • Pubkeys in owner are OR choices.
  • 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 0
AND 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.

Transaction filters

Inside one named transaction filter:

  • 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:

Protobuf fieldTypeScript fieldMeaning
account_includeaccountIncludeAny listed account may match
account_requiredaccountRequiredEvery listed account must match
account_excludeaccountExcludeAny listed account rejects the transaction
accounts_data_sliceaccountsDataSliceReturn selected byte ranges for account updates
filter_by_commitmentfilterByCommitmentRestrict 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.

exact-accounts.ts
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.

owner-filter.ts
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.

owner-memcmp-datasize.ts
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.

account-data-slice.ts
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.

transaction-account-include.ts
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.

transaction-required-excluded.ts
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 valuesResult
vote: false, failed: falseSuccessful, non-vote transactions
vote: true, failed: falseSuccessful vote transactions
vote: false, failed: trueFailed, non-vote transactions
vote: undefined, failed: undefinedDo 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.

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.

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.

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 | 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:

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.

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.

  1. Persist a checkpoint such as the latest completely applied slot and relevant signatures.
  2. On failure, stop accepting updates from the old stream.
  3. Reconnect with bounded exponential backoff and jitter.
  4. Rebuild or reconcile state through RPC or a durable store when the workload cannot tolerate gaps.
  5. Resubscribe with the same versioned filter configuration.
  6. Deduplicate by a workload-appropriate identity, such as signature plus slot or account plus write version.
  7. 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.

View plans and pricing ->

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.

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.