Access
Solana gRPC streaming is available as an add-on. Onboarding runs through the Uniblock team: provisioning is handled per customer, and the endpoint and credential are issued during setup.Contact sales
Onboarding for the Solana gRPC add-on runs through the Uniblock team.
Pricing
Solana gRPC streaming is billed on data transferred, at $50 USD per TB, with partial terabytes rounded up. Enterprise discounts are available. Replayed data is metered the same as live data, so a full-window catch-up bills like the equivalent live stream. Narrow filters andaccounts_data_slice are the levers that reduce transferred volume.
Connect
Endpoint
Authentication
The credential is a single bearer token carried in gRPC metadata underx-token. There is no mTLS, no basic auth, no signing step and no handshake exchange. Authorization is evaluated once, when the stream is established, and is not re-checked while a stream runs. The token is opaque, with no meaningful internal structure.
An IP allow-list is available and is configured during onboarding. It is empty by default, so a token authenticates from any address unless addresses are added.
TLS
TLS only, on 443, against a public CA chain. No client certificate and no mTLS. The system trust store is sufficient.Stubs and client libraries
Stubs are generated from the upstream Yellowstone repository at github.com/rpcpool/yellowstone-grpc. Uniblock distributes no.proto of its own.
geyser.proto declares package geyser and imports solana-storage.proto publicly, so generating from geyser.proto alone pulls in both. Stubs generated from the earlier v11 protos are wire-identical across all 26 streaming messages and remain compatible.
What streams
Five update kinds are produced: accounts, transactions, slots, entries and block metadata, atPROCESSED, CONFIRMED or FINALIZED. Each update is delivered once. Slot status transitions and block metadata each arrive a single time per slot.
Every update carries filters, the list of filter names that matched it, and created_at, a server-side google.protobuf.Timestamp. One update lists every filter name it matched, so a single message can satisfy more than one filter.
Methods
All eight RPCs are implemented.Subscribe is the bidirectional stream. The seven unary methods answer from the same data the stream is built on, so a client holding a stream reads a blockhash or a slot without opening a JSON-RPC connection.
Filters
Each filter map key is an independent entry. An update is delivered if any entry matches it fully, and within one entry every populated field must hold. Sub-filters belong to their own entry and never gate frames matched by a different entry. The consequence in practice: a subscription carrying thirty owner filters plus one narrowmemcmp filter receives everything the thirty owners produce, plus the memcmp matches. The memcmp does not narrow the other thirty. Constraints that must apply together belong in a single entry.
The map key is a label. It comes back in filters to tell streams apart and is never compared against chain data. The addresses do the matching.
Transaction filters
SubscribeRequestFilterTransactions:
An unset field is not a constraint, and an empty list is the same as unset. Leaving every field unset is a wildcard that streams every transaction, which is permitted and is how a firehose subscription is expressed.
account_include is an OR across addresses. account_required is the AND version, which suits pinning a program and pool pair. Vote transactions dominate the raw feed by volume, so vote: false and failed: false is the usual starting point.
Account filters
SubscribeRequestFilterAccounts:
Sub-filters, from
SubscribeRequestFilterAccountsFilter:
memcmp requires decoding account data and is the expensive one. owner or datasize as the first cut, with memcmp refining inside the same entry, costs less.
Slot, entry and block metadata filters
SubscribeRequestFilterSlots takes filter_by_commitment, which delivers only slot updates whose status matches the requested commitment, and interslot_updates, which controls whether the intermediate transitions arrive. SubscribeRequestFilterEntry and SubscribeRequestFilterBlocksMeta are empty messages, so a named entry subscribes to the whole kind.
Encoding
Pubkeys, owners, signatures and hashes are raw bytes on the wire. Filters are given as base58 strings. Forgetting to encode when logging and decode when filtering is the most common cause of a filter that matches nothing.Reducing payload size
accounts_data_slice is a repeated {offset, length} on the request rather than on a filter. When present, account updates are re-encoded so data carries only the requested ranges. For a subscription watching one field in a large account layout this is the difference between kilobytes and tens of bytes per update, and it is the cheapest bandwidth reduction available.
Cardinality limits
Checked when aSubscribeRequest arrives, on live and historical subscriptions alike. Exceeding one ends the stream with INVALID_ARGUMENT and a message naming the limit.
The 64 counts distinct named map keys rather than addresses, so one entry holding several thousand pubkeys is one entry. The 100,000 ceiling is theoretical, because a request that large exceeds the 4 MB message cap first.
Rejection rather than silent widening
An address that fails base58 decoding, a signature of the wrong length, alamports sub-filter with no comparator, a datasize given twice, an oversized memcmp payload and an unsupported token_account_state all end the subscription with INVALID_ARGUMENT and a message naming the offending field and value.
A filter entry that lost its only address constraint would be a wildcard over the whole kind, so the request fails rather than quietly turning a narrow subscription into a firehose.
A worked request
Non-vote, non-failed transactions touching two programs, plus slot updates:filters: ["pump-amm"], filters: ["raydium-v4"] or filters: ["slots"]. A transaction touching both programs lists both names.
Commitment and slot status
Commitment is set once per request through thecommitment field and applies to the whole subscription. Three levels are accepted: PROCESSED, CONFIRMED and FINALIZED.
Commitment promotion is buffered per slot rather than re-fetched, so a CONFIRMED or FINALIZED subscriber receives the same bytes the PROCESSED subscriber saw, promoted when the status transition arrives.
A slot’s fate is reported by SubscribeUpdateSlot.status:
Slots are never retracted and a replaced slot is not re-sent. A fork-aware client follows these transitions, treats anything below
SLOT_FINALIZED as provisional, and unwinds work built on a slot that reports SLOT_DEAD. interslot_updates on a slot filter delivers the intermediate transitions rather than only the commitment-level ones. Each status fires once per slot.
Historical replay
Replay is triggered by settingfrom_slot on a SubscribeRequest. It is field 11 of the stock upstream message, so no special client, no SDK and no separate endpoint is involved. The stream replays forward from that slot and tails into live on the same connection, with no second reconnect and no gap.
SubscribeReplayInfo returns first_available, the oldest slot currently held. The window drifts above the 48 hour floor because pruning runs on a schedule rather than instantly, so first_available is the value to branch on in code and 48 hours is the figure to plan against.
The 64 session limit applies per customer rather than across the platform. Beyond it, a new replay subscription receives grpc-status: 8 and a retry message. Live subscriptions are never affected by replay load.
Account updates are not retained. A from_slot subscription carrying an accounts filter returns nothing for the historical portion and begins delivering at the live edge. Transactions, entries, blocks and block metadata are retained.
Every filter applies to replayed data exactly as it applies to live data. Addresses, owners, data size, lamports, the vote and failed flags, memcmp sub-filters and exact signature matching all behave identically on either path.
A from_slot older than the retained window is rejected with grpc-status: 11 and a message naming the oldest slot still held. The available window is read out of that message rather than by retrying the same slot, because a reconnect loop that always sets the same from_slot after a rejection spins forever.
The 10x cap is a real constraint on catching up. A full day of history takes roughly two and a half hours to replay. A subscription that needs to be current quickly starts from a nearer from_slot.
Limits and client behavior
Message sizes
A
SubscribeRequest over 4 MB is rejected and the stream ends with a status rather than staying open.
Because outbound messages are not capped, the binding constraint is the client’s own decode limit. Most gRPC stacks default to 4 MB. A subscription covering large accounts or busy blocks raises max_decoding_message_size on the Yellowstone client builder rather than relying on that default.
Subscriptions and connections
OneSubscribeRequest per stream, replaceable in place: sending another replaces the active filter without reconnecting. A request carrying only ping is not treated as a filter update. There is no per-credential connection cap, and max_concurrent_streams is unset, so subscriptions multiplex freely over one connection.
Compression
Compression is not enabled and not negotiated. The server advertises no accepted or sent encodings, so every stream is uncompressed protobuf regardless of what a client offers ingrpc-accept-encoding.
Keepalives
ASubscribeUpdate carrying UpdateOneof::Ping arrives on every stream every 15 seconds, including idle ones, so a filtered subscription that matches nothing still survives NAT and load-balancer idle timeouts. It is a keepalive and does not count as data. Keepalives carry created_at like every other update, so staleness computed from that field needs no special case.
There is no server-side idle timeout. A stream lives until the client ends it, the connection breaks, or the client falls behind.
Ping and Pong also exist as a client-initiated pair: a SubscribeRequest carrying only ping with an id returns the same id as SubscribeUpdatePong.
Falling behind
Ordinary HTTP/2 flow control applies first. A slow reader stalls its own stream and affects neither other streams on its connection nor other subscribers. The stream closes only once the per-client queue fills and does not recover. Remedies, in the order they usually work: move processing off the thread that drains the stream, narrow the filter, applyaccounts_data_slice to cut payload size, read faster.
Resuming
There is no server-side cursor. Consumption is not tracked, so a reconnect starts live unless the request asks for something else. Resumption is the client’s decision, throughfrom_slot: track the last slot processed, set from_slot to it on reconnect, and the stream replays forward and tails into live on the same connection.
A client that does not need gap-free delivery reconnects with no from_slot. That path is cheaper and cannot fail on a retention boundary. from_slot resumption suits state reconciliation that genuinely cannot tolerate a hole.
Errors and retries
Rejections carry an HTTP status, a gRPC status code, anx-error-code header and a grpc-message. x-error-code is stable and unique per condition and is the field to branch on. The prose is not stable and is better displayed than matched. grpc-message is always <message> (code: <X_ERROR_CODE>).
UNAUTHENTICATED covers a missing token, an invalid one, an expired one and a disallowed source address under one code, so a report of it is checked against the IP allow-list as well as against the credential.
AUTH_RATE_LIMITED counts distinct invalid credentials from one address rather than failed attempts. A client stuck in a reconnect loop with one stale or revoked token retries it indefinitely without being blocked, and keeps receiving UNAUTHENTICATED. It is a configuration error to surface rather than a transient to retry.
Conditions raised on the streaming path carry a status and a message but no x-error-code:
Both
OUT_OF_RANGE wordings name the oldest retained slot, so parsing for retained from is more reliable than matching either string whole.
Backoff
Statuses 13 and 14 retry with exponential backoff starting at 100 ms, capped at 30 seconds, with jitter. That coversAUTH_UNAVAILABLE, AUTH_BUSY, NO_CAPACITY, UPSTREAMS_UNAVAILABLE and UPSTREAM_ERROR. Status 8 carrying the replay capacity message retries the same way, as does a bare connection failure.
Retrying 16 or 3 without changing the credential or the request repeats the same failure. On 11, the available window is read out of the message and the subscription resumes from there.