> For the complete documentation index, see [llms.txt](https://docs.triton.one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.triton.one/solana/quickstart/streaming/stream-solana-with-grpc.md).

# Start streaming with Yellowstone gRPC

Connect to Dragon's Mouth, subscribe to a live stream, and handle accounts, transactions, slots, and blocks over Yellowstone gRPC.

Dragon's Mouth is Triton's Geyser-fed gRPC interface for streaming Solana data: account writes, transactions, slots, blocks, block metadata, and entries. You subscribe once and the node pushes strongly typed Protobuf updates the moment they arrive, bypassing the JSON-RPC HTTP layer. Use it for backend workloads that need the lowest possible latency and fresh, structured data: trading, indexing, analytics, and DeFi. gRPC is unsupported by web browsers, so Dragon's Mouth targets backend software; for browsers, use the [WebSocket API](/solana/real-time-streaming/whirligig-websockets.md).

## Prerequisites

* A Dragon's Mouth endpoint and an `x-token`.
* A gRPC client. Official sample clients exist for Rust, Go, and NodeJS/TypeScript. You can also test with the generic `grpcurl` client.

For NodeJS/TypeScript, install the client:

{% tabs %}
{% tab title="npm" %}

```bash
npm install --save @triton-one/yellowstone-grpc
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add @triton-one/yellowstone-grpc
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The underlying gRPC proto can change, so test with a client matching the current version of the interface. Protobuf files live in the [yellowstone-grpc repo](https://github.com/rpcpool/yellowstone-grpc/tree/master/yellowstone-grpc-proto/proto) or the [`yellowstone-grpc-proto`](https://crates.io/crates/yellowstone-grpc-proto) crate.
{% endhint %}

## Minimal subscriber

Connect, subscribe to one program's account updates plus slots, and log each update.

{% tabs %}
{% tab title="TypeScript" %}
{% code title="index.ts" lineNumbers="true" %}

```typescript
import Client, {
  CommitmentLevel,
  SubscribeRequest,
} from "@triton-one/yellowstone-grpc";

async function main() {
  // Connect to the gRPC endpoint
  const client = new Client("https://<your-endpoint>.mainnet.rpcpool.com", "your-x-token", {
    grpcMaxDecodingMessageSize: 64 * 1024 * 1024, // 64MiB
  });

  await client.connect();

  // Subscribe to events
  const stream = await client.subscribe();

  // Handle stream events
  const streamClosed = new Promise<void>((resolve, reject) => {
    stream.on("error", (error) => {
      reject(error);
      stream.end();
    });
    stream.on("end", resolve);
    stream.on("close", resolve);
  });

  // Process incoming data
  stream.on("data", (data) => {
    if (data.slot) {
      console.log(`Slot: ${data.slot.slot}, Status: ${data.slot.status}`);
    }
    if (data.account) {
      console.log(`Account update at slot: ${data.account.slot}`);
    }
  });

  // Build and send subscription request
  const request: SubscribeRequest = {
    accounts: {
      client: {
        account: [],
        owner: ["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"], // Raydium AMM
        filters: [],
      },
    },
    slots: {
      client: { filterByCommitment: true },
    },
    transactions: {},
    transactionsStatus: {},
    entry: {},
    blocks: {},
    blocksMeta: {},
    blockFooter: {},
    commitment: CommitmentLevel.PROCESSED,
    accountsDataSlice: [],
    ping: undefined,
  };

  await new Promise<void>((resolve, reject) => {
    stream.write(request, (err) => (err ? reject(err) : resolve()));
  });

  await streamClosed;
}

main().catch(console.error);
```

{% endcode %}
{% endtab %}

{% tab title="Rust" %}
{% code title="src/main.rs" lineNumbers="true" %}

```rust
use {
    futures::StreamExt,
    std::collections::HashMap,
    yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient},
    yellowstone_grpc_proto::prelude::{
        CommitmentLevel, SubscribeRequest, SubscribeRequestFilterSlots,
        SubscribeRequestFilterAccounts, subscribe_update::UpdateOneof,
    },
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Connect to the gRPC endpoint
    let mut client = GeyserGrpcClient::build_from_shared("https://<your-endpoint>.mainnet.rpcpool.com")?
        .x_token(Some("your-x-token".to_string()))?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .connect()
        .await?;

    // Build subscription request
    let mut slots = HashMap::new();
    slots.insert(
        "client".to_string(),
        SubscribeRequestFilterSlots {
            filter_by_commitment: Some(true),
            interslot_updates: Some(false),
        },
    );

    let mut accounts = HashMap::new();
    accounts.insert(
        "client".to_string(),
        SubscribeRequestFilterAccounts {
            account: vec![], // specific account pubkeys
            owner: vec!["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8".to_string()], // Raydium AMM
            filters: vec![],
            nonempty_txn_signature: None,
            ..Default::default()
        },
    );

    let request = SubscribeRequest {
        slots,
        accounts,
        commitment: Some(CommitmentLevel::Processed as i32),
        ..Default::default()
    };

    // Subscribe and handle updates
    let (mut _tx, mut stream) = client.subscribe_with_request(Some(request)).await?;

    while let Some(message) = stream.next().await {
        match message?.update_oneof {
            Some(UpdateOneof::Slot(slot)) => {
                println!("Slot: {}, Status: {}", slot.slot, slot.status);
            }
            Some(UpdateOneof::Account(account)) => {
                println!("Account update at slot: {}", account.slot);
            }
            _ => {}
        }
    }

    Ok(())
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

The subscribe stream is bi-directional: you receive updates on `data` and send requests by writing to the same stream. The NodeJS client is asynchronous, so run all calls inside an async function.

## Subscribe request shape

Every subscription is a single JSON request object. Each stream type is a map of client-assigned labels to filters; an empty map (`{}`) means that stream is off. Labels (for example `wsol/usdc`) tag the updates you receive, so you can mix accounts, programs, blocks, and slots in one request.

| Stream         | Request key                          | Empty value |
| -------------- | ------------------------------------ | ----------- |
| Account writes | `accounts`                           | `{}`        |
| Transactions   | `transactions`                       | `{}`        |
| Slots          | `slots`                              | `{}`        |
| Blocks         | `blocks`                             | `{}`        |
| Block metadata | `blocks_meta` (NodeJS: `blocksMeta`) | `{}`        |

The examples below show the raw gRPC JSON alongside the NodeJS form. Field names differ between them: gRPC uses snake\_case (`account_include`, `blocks_meta`, `accounts_data_slice`), NodeJS uses camelCase (`accountInclude`, `blocksMeta`, `accountsDataSlice`).

## Subscribe to accounts

Subscribe to a single account by pubkey on `confirmed` commitment. Here `wsol/usdc` is a client-assigned label.

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": { "slots": {} }, "accounts": { "wsol/usdc": { "account": ["8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6"] } }, "transactions": {}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": [], "commitment": 1}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const request = {
  "slots": {
    "slots": {}
  },
  "accounts": {
    "wsol/usdc": {
      "account": ["8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6"]
    }
  },
  "transactions": {},
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": [],
  "commitment": CommitmentLevel.CONFIRMED
};
```

{% endtab %}
{% endtabs %}

To subscribe to all accounts owned by a program, pass `owner`. This example streams Solend account writes on `processed`.

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": { "slots": {} }, "accounts": { "solend": {  "owner": ["So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"] } }, "transactions": {}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": [], "commitment": 0}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const request = {
  "slots": {
    "slots": {}
  },
  "accounts": {
    "solend": {
      "owner": ["So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"]
    }
  },
  "transactions": {},
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": [],
  "commitment": CommitmentLevel.PROCESSED
}
```

{% endtab %}
{% endtabs %}

Add `filters` and `accounts_data_slice` to narrow the match and trim the payload. This subscribes to USDC Tokenkeg accounts and returns only 40 bytes of account data instead of all 165 (`offset` 32 gives `owner` and `amount`).

{% tabs %}
{% tab title="gRPC" %}

```json
{
    "accounts": {
        "usdc": {
            "owner": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
            "filters": [{
                "token_account_state": true
            }, {
                "memcmp": {
                    "offset": 0,
                    "base58": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
                }
            }]
        }
    },
    "accounts_data_slice": [{ "offset": 32, "length": 40 }]
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const request = {
  "slots": {},
  "accounts": {
    "usdc": {
      "owner": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
      "filters": [{
          "tokenAccountState": true
      }, {
          "memcmp": {
              "offset": 0,
              "base58": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
          }
      }]
    }
  },
  "transactions": {},
  "blocks": {},
  "blocksMeta": {},
  "entry": {},
  "commitment": CommitmentLevel.CONFIRMED,
  "accountsDataSlice": [{ "offset": 32, "length": 40 }],
};
```

{% endtab %}
{% endtabs %}

## Subscribe to transactions

If every transaction field is empty, all transactions are broadcast. Otherwise fields work as logical `AND`, and values in arrays as logical `OR`. You can include or exclude vote transactions and failed transactions. Alpenglow blocks contain no vote transactions.

This streams all finalized non-vote, non-failed transactions:

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": { "slots": {} }, "accounts": {}, "transactions": { "alltxs": { "vote": false, "failed": false }}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": [], "commitment": 2}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const request = {
  "slots": {
    "slots": {}
  },
  "accounts": {},
  "transactions": {
    "alltxs": {
      "vote": false,
      "failed": false
    }
  },
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": [],
  "commitment": CommitmentLevel.FINALIZED
};
```

{% endtab %}
{% endtabs %}

Filter to transactions mentioning specific accounts with `account_include`, exclude with `account_exclude`, and combine the two. This streams non-vote transactions that mention one account but not another:

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": { "slots": {} }, "accounts": {}, "transactions": { "serum": { "vote": false, "account_include": [ "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" ], "account_exclude": [ "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT" ] }}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": []}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request = {
  "slots": {
    "slots": {}
  },
  "accounts": {},
  "transactions": {
    "serum": {
      "vote": false,
      "accountInclude": [
        "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
      ],
      "accountExclude": [
        "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT"
      ]
    }
  },
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": []
};
```

{% endtab %}
{% endtabs %}

To watch one transaction by signature, subscribe with `signature`. The update arrives once, at the subscription commitment (`processed` by default); set `commitment` for `confirmed` or `finalized`. Replace the sample signature with the signature of a transaction you are about to send.

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": {}, "accounts": {}, "transactions": { "sign": { "signature": "5rp2hL9b6kexex11Mugfs3vfU9GhieKruj4CkFFSnu52WLxiGn4VcLLwsB62XURhMmT1j4CZiXT6FFtYbXsLq2Zs"}}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": [], "commitment": 1}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const request = {
  "slots": {},
  "accounts": {},
  "transactions": {
    "sign": {
      // Replace with the signature of a transaction you are about to send
      "signature": "5rp2hL9b6kexex11Mugfs3vfU9GhieKruj4CkFFSnu52WLxiGn4VcLLwsB62XURhMmT1j4CZiXT6FFtYbXsLq2Zs"
    }
  },
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": [],
  "commitment": CommitmentLevel.CONFIRMED
};
```

{% endtab %}
{% endtabs %}

## Subscribe to slots

Slot notifications need only a label.

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": { "incoming_slots": {} }, "accounts": {}, "transactions": {}, "blocks": {}, "blocks_meta": {}, "accounts_data_slice": []}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request = {
  "slots": {
    "incoming_slots": {}
  },
  "accounts": {},
  "transactions": {},
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": []
};
```

{% endtab %}
{% endtabs %}

## Subscribe to blocks and block metadata

`blocks` returns each block as it is produced, including its transactions by default. Toggle `include_transactions` and `include_accounts`, or filter with `account_include` to receive only blocks where a given account is mentioned.

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": {}, "accounts": { }, "transactions": {}, "blocks": { "blocks": {"include_transactions": false, "include_accounts": true} }, "blocks_meta": {}, "accounts_data_slice": []}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request = {
  "slots": {},
  "accounts": {},
  "transactions": {},
  "blocks": {
    "blocks": {
      "includeTransactions": false,
      "includeAccounts": true
    }
  },
  "blocksMeta": {},
  "accountsDataSlice": []
};
```

{% endtab %}
{% endtabs %}

For block-completion notifications without the transactions, use `blocks_meta`:

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": {}, "accounts": {}, "transactions": {}, "blocks": {}, "blocks_meta": { "blockmetadata": {} }, "accounts_data_slice": []}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request = {
  "slots": {},
  "accounts": {},
  "transactions": {},
  "blocks": {},
  "blocksMeta": {
    "blockmetadata": {}
  },
  "accountsDataSlice": []
};
```

{% endtab %}
{% endtabs %}

## Modifying and unsubscribing

The Subscribe method is a bi-directional stream, so you can change your subscription by writing a new request. The new request entirely overwrites the previous one, so keep a local register of the full subscription config you want. To unsubscribe from everything while keeping the connection open, send empty maps for all streams:

{% tabs %}
{% tab title="gRPC" %}

```json
{"slots": {}, "accounts": {}, "transactions": {}, "blocks": {}, "blocks_meta": {}}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request = {
  "slots": {},
  "accounts": {},
  "transactions": {},
  "blocks": {},
  "blocksMeta": {},
  "accountsDataSlice": []
};
```

{% endtab %}
{% endtabs %}

## Managing commitment

Streams run on `processed` commitment by default. You can also request `confirmed` or `finalized`, in which case Dragon's Mouth buffers updates and releases them once they reach that level. For maximum performance, handle commitment client-side instead.

```protobuf
enum CommitmentLevel {
  PROCESSED = 0;
  CONFIRMED = 1;
  FINALIZED = 2;
}
```

Working at `processed` lets you process transactions as soon as they arrive and commit to them only once you know they are confirmed or finalized, so you can surface UI changes faster.

To manage `confirmed` and `finalized` client-side, buffer events by the slot attached to each transaction or account write, then subscribe to slot notifications. Release a slot's buffer when you receive its notification at the commitment level you want. All transaction and account-write notifications for a slot arrive before that slot's confirmed or finalized notification.

{% hint style="info" %}
The validator does not send a commitment status for every slot, so Dragon's Mouth fills in the gaps. When a slot reaches `confirmed` or `finalized`, the server also sends that status for each ancestor slot that has not reached it yet.
{% endhint %}

## Keep-alive pings

Some cloud providers (for example Cloudflare) close idle streams. Send periodic pings to keep the connection open; the server also sends a `ping` every 10 seconds, and answers each ping you send with a `pong`.

```typescript
const PING_INTERVAL_MILLISECONDS = 30000;

// Ping request
const pingRequest: SubscribeRequest = {
    ping: { id: 1 },
    accounts: {},
    accountsDataSlice: [],
    transactions: {},
    transactionsStatus: {},
    blocks: {},
    blocksMeta: {},
    entry: {},
    slots: {},
    blockFooter: {},
};

// Sending pings periodically
setInterval(async () => {
  await new Promise<void>((resolve, reject) => {
    stream.write(pingRequest, (err) => {
      if (err === null || err === undefined) {
        resolve();
      } else {
        reject(err);
      }
    });
  }).catch((reason) => {
    console.error(reason);
    throw reason;
  });
}, PING_INTERVAL_MILLISECONDS);

// Handling pong responses
// This goes in your `data` handler
stream.on("data", (data) => {
if (data.pong) {
    console.log("Received Pong response");
  }
});
```

## Reconnect and replay from a slot

Dragon's Mouth can replay recently buffered updates by setting `from_slot` on the `SubscribeRequest`, mainly to recover from short disconnections. The server first replays buffered updates from that slot, then continues streaming live on the same connection using the same filters and commitment level.

Replay logic:

1. Track the latest slot your application has processed.
2. On disconnect, reconnect and resubscribe with `from_slot` set to that slot.
3. If the requested slot is no longer available, fall back to a fresh live subscription or your own backfill path.

Replay covers only the server's retained replay window: about 1,000 slots (roughly 400 seconds) on shared mainnet endpoints and about 300 slots on shared devnet; on dedicated nodes it varies, so contact support. Replay starts at a slot boundary, so reconnecting from your last processed slot may yield duplicate updates from that slot: deduplicate client-side. To find the earliest replayable slot, call the unary `SubscribeReplayInfo` RPC and read `first_available`; if `from_slot` is older, the request fails and you should retry with a newer slot.

{% tabs %}
{% tab title="gRPC" %}

```json
{
  "slots": {
    "incoming_slots": {}
  },
  "accounts": {},
  "transactions": {
    "alltxs": {
      "vote": false,
      "failed": false
    }
  },
  "blocks": {},
  "blocks_meta": {},
  "accounts_data_slice": [],
  "from_slot": 382001234
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const request: SubscribeRequest = {
  slots: {
    incoming_slots: {}
  },
  accounts: {},
  transactions: {
    alltxs: {
      vote: false,
      failed: false,
      accountInclude: [],
      accountExclude: [],
      accountRequired: []
    }
  },
  transactionsStatus: {},
  entry: {},
  blocks: {},
  blocksMeta: {},
  blockFooter: {},
  accountsDataSlice: [],
  fromSlot: "382001234"
};
```

{% endtab %}
{% endtabs %}

Check replay availability before reconnecting:

```typescript
const info = await client.subscribeReplayInfo();
console.log(info.firstAvailable);
```

{% hint style="info" %}
For teams running their own Yellowstone gRPC server, replay is on by default: `replay_stored_slots` defaults to `150` when unset. The sample `config.json` sets it to `0`, which turns replay off.
{% endhint %}

### Let the client reconnect

Ordinary subscriptions don't reconnect: `subscribe`, `subscribe_with_request`, and `subscribe_once` in Rust, and `subscribe()` in TypeScript, end at the first disconnect. The client can reconnect for you: use `subscribe_with_reconnect` (`yellowstone-grpc-client` 17.0.0 and later) or `subscribeWithReconnect()` (`@triton-one/yellowstone-grpc` 10.0.0 and later). It works at any commitment level, and filter changes you send apply to every reconnect. The client must not request a startup snapshot. After a disconnect, the stream emits a `Disconnected` event that lists the banks you received only in part, then by default replays from the last confirmed slot, which can resend banks you already hold.

```rust
use {
    futures::StreamExt,
    yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient, ReconnectConfig, ReconnectEvent},
};

let mut client = GeyserGrpcClient::build_from_shared(endpoint)?
    .x_token(x_token)?
    .tls_config(ClientTlsConfig::new().with_native_roots())?
    // Optional: the retry budget and the replay policy for subscribe_with_reconnect
    .set_reconnect_config(ReconnectConfig::default())
    .connect()
    .await?;

let (_sink, mut stream) = client.subscribe_with_reconnect(Some(request)).await?;

while let Some(event) = stream.next().await {
    match event {
        Ok(ReconnectEvent::Update(update)) => {
            // Apply the update. A replay can resend banks you already hold whole.
            println!("{:?}", update.update_oneof);
        }
        Ok(ReconnectEvent::Disconnected(disconnected)) => {
            // Drop the state for these banks: you received only part of them.
            println!("discard {:?}", disconnected.partial_banks);
        }
        Err(status) => {
            // The stream has ended: open a new subscription.
            eprintln!("stream ended: {status}");
            break;
        }
    }
}
```

For the request rules, the replay policy, the retry budget, the event fields, and a TypeScript example, see [Reconnect with bank recovery](/solana/real-time-streaming/dragon-s-mouth-grpc.md#reconnect-with-bank-recovery).

## Excluded programs

Light Protocol / ZK Compression (`compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq`) is excluded from all Dragon's Mouth gRPC streams and from `getProgramAccounts`. Each update produces a \~10MB blob, several gigabits per second of data, which is impractical to include in standard streams.

***

<i class="fa-life-ring">:life-ring:</i> Contact support by clicking the chat icon in your [customer dashboard](https://customers.triton.one)\ <i class="fa-briefcase">:briefcase:</i> Sales questions? [Contact us](https://triton.one/contact)\ <i class="fa-sparkles">:sparkles:</i> AI agent? Read [llms.txt](https://docs.triton.one/llms.txt)\ <i class="fa-rss">:rss:</i> Follow updates: [Blog](https://blog.triton.one) · [X](https://x.com/triton_one) · [YouTube](https://www.youtube.com/@triton_one_ltd) · [Telegram](https://t.me/tritonone) · [GitHub](https://github.com/rpcpool)
