Corvus Labs
Yellowstone gRPC

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-grpc
export 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 pass undefined, and the registered egress IP is the credential.
  • progress is a filter name you picked yourself. It comes back on matching updates, so one name per logical consumer pays off.
  • grpcMaxDecodingMessageSize is 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.details carries the structured reason naming whichever limit or filter got rejected, so log it next to error.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 commitment combined with fromSlot is rejected: INVALID_ARGUMENT (3), from_slot replay only supports processed commitment.
  • Everything replays except full blocks. Combining fromSlot with a blocks filter is rejected: INVALID_ARGUMENT (3), blocks are not possible to replay. blocksMeta replays fine.
  • Replay is inclusive. It starts at fromSlot itself, 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 fromSlot older 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), message lagged. 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

StatusMeaningFix
UNAUTHENTICATED (16)Wrong or missing x-token, or unregistered egress IPCorrect credentials, then open a new connection
PERMISSION_DENIED (7)Your resource does not include the requested accessCheck product access on Discord
INVALID_ARGUMENT (3)Malformed request or filterFix 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.

On this page