Solana gRPC Streaming: How to Use Yellowstone to Stream Transactions, Accounts, and Blocks in Real Time

Written by
Ted Bloquet
March 30, 2026
5
min. read
A sleek, flowing graphic showing a connected Web3 ecosystem. Curved lines link different modules—RPCs, Wallet Operations, Notifications, Fee Estimation, and Crypto Price API, into one continuous stream

If you've been building on Solana and relying on JSON-RPC polling to fetch the latest state, you've probably already hit the ceiling. Polling works fine for lightweight use cases, but the moment you need fresh data at validator speed, the cracks start to show. You're either hammering endpoints, introducing unnecessary latency, or both.

This is exactly the problem that Yellowstone gRPC solves. And with Tatum's Solana gRPC support now live, you can tap into real-time Solana streaming without running your own validator infrastructure.

This guide walks through what Yellowstone gRPC is, how it differs from standard JSON-RPC, and how to start streaming transactions, accounts, blocks, and slots using Node.js and TypeScript against Tatum's Mainnet gRPC endpoint.

What Is Yellowstone gRPC and Why Should You Care?

Yellowstone is a Geyser plugin for the Solana validator. Geyser is an internal plugin interface that Solana validators expose to emit events as they process blocks. Account updates, slot changes, and transaction confirmations all fire through Geyser in real time, from inside the validator itself.

The Yellowstone plugin takes those events and exposes them over a gRPC server. Instead of your application asking "what happened?" on a schedule, the validator pushes data to you the moment it processes it.

This is a meaningful architectural shift. With polling via JSON-RPC, there are inherent gaps between requests. With Yellowstone gRPC, your application maintains a persistent stream and receives data as it becomes available, with latency measured in milliseconds rather than the roundtrip time of repeated HTTP calls.

The practical benefits are significant for anything latency-sensitive. Trading infrastructure, real-time indexers, portfolio trackers, MEV tooling, monitoring dashboards, and on-chain analytics platforms all benefit from receiving events at validator speed. Tatum's Yellowstone endpoint supports streaming of account updates, transactions, slots, blocks, block metadata, and more, all over a single persistent connection.

Tatum

Stream Solana Data in Real Time

Access Tatum's Solana Yellowstone gRPC endpoint and start streaming transactions, accounts, blocks, and slots instantly. No validator infrastructure required. Just connect and build.

Get Your Free API Key

Setting Up Your Environment

This guide uses Node.js with TypeScript and the official @triton-one/yellowstone-grpc client. You will need Node 18 or later.

Start by creating a project and installing the required dependencies:

Shell
mkdir tatum-grpc-test
cd tatum-grpc-test
npm init -y
npm install @triton-one/yellowstone-grpc
npm install -D tsx typescript @types/node

Using tsx to run TypeScript files directly is recommended over ts-node. It is faster and requires no additional tsconfig.json setup.

Next, download the Yellowstone proto files. These are required if you want to use grpcurl for smoke testing before writing any application code:

Shell
curl -o geyser.proto https://raw.githubusercontent.com/rpcpool/yellowstone-grpc/master/yellowstone-grpc-proto/proto/geyser.proto
curl -o solana-storage.proto https://raw.githubusercontent.com/rpcpool/yellowstone-grpc/master/yellowstone-grpc-proto/proto/solana-storage.proto

Your project folder should look like this before you start writing code:

Shell
tatum-grpc-test/
├── geyser.proto
├── solana-storage.proto
├── node_modules/
├── package.json
└── get-transactions.ts

Verifying Your Endpoint Before Writing Any Code

This step is worth taking seriously. If your connection or API key has an issue, no amount of application code will fix it. Verifying with grpcurl first means you can isolate problems before they become harder to debug inside a full streaming implementation.

The Tatum Mainnet gRPC endpoint is:

Shell
solana-mainnet-grpc.gateway.tatum.io:443

Authentication uses the x-token header. This is your standard Tatum API key, the same one you use for JSON-RPC, just sent under the header name that Yellowstone expects rather than x-api-key.

Run the following to call GetVersion and confirm your setup:

Shell
grpcurl \
  -proto geyser.proto \
  -import-path . \
  -H "x-token: YOUR_API_KEY" \
  solana-mainnet-grpc.gateway.tatum.io:443 \
  geyser.Geyser/GetVersion

If everything is working, you will get back a response that looks something like this:

JSON
{
  "version": "{\"version\":{\"package\":\"yellowstone-grpc-geyser\",\"version\":\"12.1.0+solana.3.1.10\",\"proto\":\"12.1.0+solana.3.1.10\",\"solana\":\"3.1.10\",\"git\":\"f0ae884\",\"rustc\":\"1.86.0\",\"buildts\":\"2026-03-12T09:16:09.772120954Z\"},\"extra\":{\"hostname\":\"relevant-marten\"}}"
}

That version response confirms your endpoint and API key are functional. Only move on to writing application code once this step succeeds.

Quick Start Checklist

Before opening a stream in your application, confirm the following:

You can call GetVersion successfully with grpcurl. Your API key has access to the endpoint. Your environment can maintain long-lived outbound TCP connections, which some corporate networks or restrictive cloud environments do not allow by default. You have a reconnect strategy for network interruptions. You are using the correct Mainnet gRPC endpoint and not accidentally pointing at a Devnet JSON-RPC URL.

Authenticating the Yellowstone gRPC Client

The client setup is straightforward. Pass the endpoint URL and your API key directly to the Client constructor:

TypeScript
import Client, {
  CommitmentLevel,
} from "@triton-one/yellowstone-grpc";
const ENDPOINT = "https://solana-mainnet-grpc.gateway.tatum.io";
const X_TOKEN = "YOUR_API_KEY";

The X_TOKEN value is passed as the x-token header automatically by the client library. You do not need to configure headers manually.

Streaming Transactions in Real Time

The following script opens a stream and subscribes to all finalized, non-vote, non-failed transactions. This is a good starting point for understanding the full subscription lifecycle before adding filters.

TypeScript
import Client, {
  CommitmentLevel,
  SubscribeRequest,
} from "@triton-one/yellowstone-grpc";
const ENDPOINT = "https://solana-mainnet-grpc.gateway.tatum.io";
const X_TOKEN = "YOUR_API_KEY";
async function main() {
  const client = new Client(ENDPOINT, X_TOKEN);
  const stream = await client.subscribe();
  stream.on("data", (message) => {
    if (message.transaction) {
      const slot = message.transaction.slot;
      const signature = Buffer.from(
        message.transaction.transaction.signature,
      ).toString("base64");
      console.log(tx received: slot=${slot} signature=${signature});
    }
  });
  stream.on("error", (error) => {
    console.error("stream error:", error);
  });
  stream.on("end", () => {
    console.log("stream closed");
  });
  const request: SubscribeRequest = {
    slots: {},
    accounts: {},
    transactions: {
      alltxs: {
        vote: false,
        failed: false,
      },
    },
    blocks: {},
    blocksMeta: {},
    accountsDataSlice: [],
    commitment: CommitmentLevel.FINALIZED,
    entry: {},
    transactionsStatus: {},
  };
  stream.write(request);
  console.log("subscription started");
}
main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Run it with:

Shell
npx tsx get-transactions.ts

The SubscribeRequest object controls what the stream sends you. Every field must be present for the request to be valid, even if you are leaving most of them as empty objects. The transactions field here uses a key of alltxs as the subscription label. That label is arbitrary and just needs to be a unique string within your request.

Subscribing to Specific Account Updates

When you need to watch state changes for a known account rather than the full transaction flow, use an account subscription. This is useful for monitoring token accounts, liquidity pools, program state, or any address where you care about balance or data changes.

TypeScript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";
const request = {
  slots: {
    slots: {}
  },
  accounts: {
    "wsol/usdc": {
      account: ["8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6"]
    }
  },
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  commitment: CommitmentLevel.CONFIRMED,
  entry: {},
  transactionsStatus: {}
};

The key inside accounts, in this case "wsol/usdc", is your subscription label. The account array contains the base58 encoded public keys you want to watch. You can include multiple addresses in the same subscription.

Streaming Slots for Liveness Monitoring

Slot subscriptions are useful for tracking validator progress, monitoring chain liveness, or building anything that needs to react to the passage of time on-chain rather than to specific transactions or accounts.

TypeScript
const request = {
  slots: {
    incoming_slots: {}
  },
  accounts: {},
  transactions: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  entry: {},
  transactionsStatus: {}
};

Note that this request omits the commitment field. Slot updates fire at the slot level regardless of commitment status, so this is appropriate for raw slot progression monitoring.

Streaming Full Blocks

For block-by-block ingestion pipelines, use a block subscription. This gives you the full block payload including all transactions within each block.

TypeScript
const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {
    blocks: {}
  },
  blocksMeta: {},
  accountsDataSlice: [],
  entry: {},
  transactionsStatus: {}
};

Full block streaming produces significantly more data per message than transaction or account subscriptions. Make sure your consumer can process messages fast enough to avoid falling behind, particularly on Mainnet where block times are roughly 400 milliseconds and each block can contain thousands of transactions.

Streaming Block Metadata Only

If your use case only needs block headers and metadata rather than full block payloads, the block metadata subscription gives you a much lighter data stream. This is the right choice for anything that cares about block timing, block hashes, or leader information without needing the transaction contents.

TypeScript
const request = {
  slots: {},
  accounts: {},
  transactions: {},
  blocks: {},
  blocksMeta: {
    blockmetadata: {}
  },
  accountsDataSlice: [],
  entry: {},
  transactionsStatus: {}
};

Troubleshooting Common Issues

Auth failures. Make sure you are using x-token as the header name. This is not the same as x-api-key. The value is your standard Tatum API key.

Wrong endpoint. The gRPC endpoint is solana-mainnet-grpc.gateway.tatum.io. This is entirely separate from the JSON-RPC endpoints. Do not point a gRPC client at a JSON-RPC URL.

No data received after subscribing. Verify your subscription filter is broad enough. If you are watching a specific account that is not actively receiving transactions, the stream will be quiet. Temporarily switch to all transactions to confirm the stream itself is working.

Frequent disconnects. Add retries with exponential backoff and jitter. Also check whether your network environment allows long-lived outbound connections.

Consumer falling behind. Solana is a high-throughput chain. If you are subscribing to all transactions without filtering, your consumer may struggle to process messages as fast as they arrive. Add buffering or a queue between your stream handler and downstream logic, and process messages asynchronously.

Testing and Validation

Beyond the grpcurl smoke test, Tatum maintains an open-source test suite specifically for validating Yellowstone endpoint behavior across reconnects, keepalive handling, resubscribe scenarios, and concurrency. If you are building production infrastructure and need confidence in edge case behavior, it is worth running through: tatumio/solana-grpc-test

You can also test subscriptions interactively using the Postman collection linked in the Tatum docs. Import the collection, set your host and API key as collection variables, and you can call GetVersion and any subscription method directly from Postman without writing code.

When to Use gRPC vs JSON-RPC

JSON-RPC is still the right tool for a lot of tasks. Reading account state on demand, querying historical transactions, fetching program accounts, and one-off lookups all work perfectly well over standard RPC. The polling overhead does not matter when you are making infrequent requests.

gRPC becomes the better choice when you need continuous data at low latency. If your application logic depends on reacting quickly to on-chain events, polling will always introduce artificial delay equal to your polling interval. Streaming eliminates that gap entirely.

The two approaches also compose well together. A common pattern is to use gRPC for real-time event ingestion and JSON-RPC for enriching those events with additional context that the stream does not include, such as fetching full transaction details or program account state triggered by a streaming notification.

Closing Thoughts

Yellowstone gRPC is a genuinely better model for consuming real-time Solana data, and having it available through a managed endpoint removes the operational burden of running and maintaining your own Geyser-enabled validator node. If you are building anything that depends on fresh chain state, the streaming approach is worth understanding deeply and integrating into your architecture.