Yellowstone gRPC usage
Connect, authenticate, subscribe, and reconnect to Corvus Solana streams with the @triton-one/yellowstone-grpc TypeScript client.
npm install @triton-one/yellowstone-grpcexport CORVUS_GRPC_ENDPOINT='http://<region>.corvus-labs.io:10101'Your endpoint is in the dashboard. Keep the http:// scheme; that's what makes this client pick insecure channel credentials (transport security).
Connect and subscribe to slots
import Client, { CommitmentLevel } from '@triton-one/yellowstone-grpc';
const endpoint = process.env.CORVUS_GRPC_ENDPOINT!;
const token = process.env.CORVUS_TOKEN || undefined;
const client = new Client(endpoint, token, {
grpcMaxDecodingMessageSize: 64 * 1024 * 1024,
});
const stream = await client.subscribe();
stream.on('data', (update) => {
if (update.slot) {
console.log('slot', update.slot.slot, 'status', update.slot.status);
}
if (update.ping) {
stream.write({ ping: { id: 1 } });
}
});
stream.on('error', (error) => {
console.error('Yellowstone stream failed', error.code, error.details);
});
stream.write({
slots: {
progress: {
filterByCommitment: true,
interslotUpdates: false,
},
},
commitment: CommitmentLevel.PROCESSED,
});- The second constructor argument is your
x-token. Only dedicated deployments have one; on a shared endpoint passundefined, and the registered egress IP is the credential. progressis a filter name you picked yourself. It comes back on matching updates, so one name per logical consumer pays off.grpcMaxDecodingMessageSizeis raised above the client default here, because large account and block payloads can go past it. Channel options in client v5 are camelCase; the old grpc-js style keys ('grpc.max_receive_message_length') are silently ignored.error.detailscarries the structured reason naming whichever limit or filter got rejected, so log it next toerror.code; Errors lists them.
Subscribe to account updates
stream.write({
accounts: {
clock: {
account: ['SysvarC1ock11111111111111111111111111111111'],
owner: [],
filters: [],
},
},
accountsDataSlice: [{ offset: 0, length: 64 }],
commitment: CommitmentLevel.PROCESSED,
});accountsDataSlice returns only the bytes you named.
Subscribe to transactions
stream.write({
transactions: {
activity: {
vote: false,
failed: false,
accountInclude: ['<PUBKEY>'],
accountExclude: [],
accountRequired: [],
},
},
commitment: CommitmentLevel.PROCESSED,
});If you only need confirmation state rather than the full payload, use transactionsStatus instead.
Each write replaces everything
A SubscribeRequest is the whole desired state, not a delta. The account subscription above cancels the slot subscription unless you write both in the same message. The one exception is a ping-only write ({ ping: { id } }): it carries no filters, so it leaves your active configuration alone.
Reconnect after a failure
On error or end, drop the stream and channel, open a new client, and write the complete filter set in one request; nothing is carried over. Then close the gap, one of two ways.
Replay it. Track the last slot you processed and pass it as fromSlot. The server re-sends everything it retains from that slot onward, in order, then continues live on the same stream:
stream.write({
transactions: {
activity: {
vote: false,
failed: false,
accountInclude: ['<PUBKEY>'],
accountExclude: [],
accountRequired: [],
},
},
commitment: CommitmentLevel.PROCESSED,
fromSlot: String(lastProcessedSlot),
});Backfill it. Subscribe without fromSlot, compare the first slot you receive against your persisted checkpoint, and reconcile the gap through JSON-RPC or your own store.
Replay's contract:
- Processed commitment only. Any other
commitmentcombined withfromSlotis rejected:INVALID_ARGUMENT(3),from_slot replay only supports processed commitment. - Everything replays except full blocks. Combining
fromSlotwith ablocksfilter is rejected:INVALID_ARGUMENT(3),blocks are not possible to replay.blocksMetareplays fine. - Replay is inclusive. It starts at
fromSlotitself, so you'll see updates from that slot you already handled; dedupe on your side. - The window is bounded. History is a fixed-size buffer evicted oldest-first, so its depth in slots moves with network traffic. A
fromSlotolder than what's retained is rejected:INVALID_ARGUMENT(3),from_slot is older than the available replay window. - An empty backend replays nothing. Right after a restart there may be no history at all; the subscribe succeeds and the stream simply starts live. Replay narrows the gap, it doesn't replace checkpointing.
- Reading too slowly mid-replay ends the stream with
DATA_LOSS(15), messagelagged. Reconnect and ask again from your checkpoint.
Rate-limit the reconnects. Cancelling streams in quick succession ends the connection with an HTTP/2 GOAWAY carrying ENHANCE_YOUR_CALM, which isn't a gRPC status at all, so a handler matching on error.code never sees it. Rewriting the filter configuration too often gets you RESOURCE_EXHAUSTED with reason subscribe_rate_limit.
Non-retryable errors
| Status | Meaning | Fix |
|---|---|---|
UNAUTHENTICATED (16) | Wrong or missing x-token, or unregistered egress IP | Correct credentials, then open a new connection |
PERMISSION_DENIED (7) | Your resource does not include the requested access | Check product access on Discord |
INVALID_ARGUMENT (3) | Malformed request or filter | Fix the request; retrying it unchanged fails identically |
One you won't meet here: the PPS gate that closes a connection with PERMISSION_DENIED and the detail prefix PPS limit requirement not met belongs to the aRPC transport, so a client on port 10101 never hits it. See aRPC limits if you consume both. Gateway & streaming errors has every status and reason string.