TriportRPC

Solana High-Throughput Stream — /ws/sol-stream

A single bidirectional WebSocket that carries the full Solana firehose — accounts, transactions, slots, blocks, and entries — driven by one replaceable filter set.

Solana— (gated by tier, not scope)pro tier minimum · concurrent streams capped per tier (pro = 8, business = 20); **not** RPS-limited

/ws/sol-stream is the high-throughput streaming surface for Solana. Unlike /ws/sol (JSON-RPC pub/sub with one subscription id per call), this channel carries a single, comprehensive filter set over one connection: you describe everything you want — which accounts, which transactions, which slots/blocks/entries — in one SubscribeRequest, and the server pushes back a stream of SubscribeUpdate frames.

Each SubscribeRequest atomically replaces the previously active filter set. There is no incremental add/remove and no per-filter unsubscribe: to change what you receive, send a new full request. An empty filter block (e.g. "transactions": {}) effectively unsubscribes that update kind.

Use this channel when you need volume and low latency across many account or transaction targets at once — indexers, mempool/landed-tx watchers, block followers. For a handful of individual account or program subscriptions, prefer the simpler JSON-RPC /ws/sol channel.

This is a pro-tier-and-up surface. Access is gated by the number of concurrent streams (open connections) your tier allows — max_streams — rather than by a requests-per-second bucket. There is no RPS limit and no daily quota on this channel.

Parameters

A SubscribeRequest payload is a JSON object whose top-level keys are filter blocks. Each named filter block (accounts, transactions, …) is a map from a caller-chosen filter name to a filter shape; the filter name is echoed back in each update's filters array so you can tell which filter produced a frame.

SubscribeRequest payloadobject
accountsmap<string, AccountsFilter>optional
Account-update filters.
slotsmap<string, SlotsFilter>optional
Slot-update filters.
transactionsmap<string, TransactionsFilter>optional
Transaction-update filters.
transactionsStatusmap<string, TransactionsFilter>optional
Lightweight transaction-status filters (status only, no full tx).
blocksmap<string, BlocksFilter>optional
Full-block filters.
blocksMetamap<string, object>optional
Block-metadata filters (header only, no transactions).
entrymap<string, object>optional
Entry (PoH tick) filters.
commitmentstringoptional
One of processed, confirmed, finalized. Default confirmed.
accountsDataSlicearray<{offset, length}>optional
Server-side slicing of account data to reduce payload size. Each entry needs offset ≥ 0 and length ≥ 0.
ping{id: int32}optional
Inline keepalive ping (see Keepalive).

Response

The server pushes SubscribeUpdate frames. Each frame populates exactly one of the 8 update keys (account, slot, transaction, transactionStatus, block, blockMeta, entry, pong) plus the envelope fields filters and createdAt. Narrow on key presence.

A transaction update:

An account update (with accountsDataSlice applied, so data is truncated):

{
  "account": {
    "slot": 295310490,
    "account": {
      "pubkey": "4Nd1m...A1bc",
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "lamports": 2039280,
      "executable": false,
      "rentEpoch": 361,
      "data": "AQAAAA==",
      "writeVersion": 184467440737,
      "txnSignature": "5j7s...n2Qd"
    }
  },
  "filters": ["client_filter_1"],
  "createdAt": "2026-05-29T12:34:57.011Z"
}
filtersstring[]
Filter names (your SubscribeRequest map keys) that produced this update.
createdAtstring (date-time)
Server-side emit timestamp.
accountUpdateAccount
Account update: slot + account (pubkey, owner, lamports, executable, rentEpoch, data base64, writeVersion, txnSignature).
slotUpdateSlot
slot, parent, status (processed/confirmed/finalized), deadError.
transactionUpdateTransaction
slot + transaction (signature, isVote, full transaction envelope, meta).
transactionStatusUpdateTransactionStatus
slot, signature, isVote, index, err.
blockUpdateBlock
slot, blockhash, parentSlot, blockTime, blockHeight, executedTransactionCount, plus optional transactions/accounts/entries/rewards.
blockMetaUpdateBlockMeta
Block header only — same as block without the embedded arrays.
entryUpdateEntry
slot, index, numHashes, hash, executedTransactionCount.
pong{id: int32}
Keepalive echo (see below).

Keepalive

To keep an idle connection alive, send a ping — either as a standalone Ping message or inline via the ping field of a SubscribeRequest:

{ "ping": { "id": 42 } }

The server replies with a pong update echoing the same id:

{ "pong": { "id": 42 }, "filters": [], "createdAt": "2026-05-29T12:35:10.000Z" }

Errors

Before closing, the server may send a WSErrorFrame (shared error envelope), then close with one of the canonical WebSocket close codes. The RFC-6455 close code is on the close frame itself, not in the JSON body.

Close codeerrorWhen it happens
4001unauthorizedMissing or invalid Authorization / api-key.
4003forbiddenTier too low (below pro), or you exceeded your max_streams concurrent-connection cap.
4029rate_limitedConnection-rate / abuse protection tripped.
4030trial_expiredTrial period ended; upgrade required.

Example error frame preceding a 4003 close:

{
  "error": "forbidden",
  "message": "tier 'basic' cannot open sol-stream; requires pro",
  "current_tier": "basic",
  "required_tier": "pro",
  "category": "sol_yellowstone",
  "upgrade_url": "https://triport.io/upgrade?tier=pro"
}

See the shared errors page for the full WSErrorFrame envelope and SDK error-class mapping.

Examples

JavaScript (fetch / ws)

import WebSocket from "ws";


const ws = new WebSocket("wss://ws.triport.io/ws/sol-stream", {
  headers: { Authorization: `Bearer ${process.env.TRIPORT_API_KEY}` },
});


ws.on("open", () => {
  ws.send(JSON.stringify({
    transactions: {
      client_filter_1: {
        vote: false,
        failed: false,
        accountInclude: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
      },
    },
    commitment: "confirmed",
  }));
});


ws.on("message", (raw) => {
  const u = JSON.parse(raw);
  if (u.transaction) console.log("tx", u.transaction.transaction.signature, "@ slot", u.transaction.slot);
  else if (u.pong) console.log("pong", u.pong.id);
});

TypeScript SDK (@triport/sdk)

import { Triport } from "@triport/sdk";


const client = new Triport({ apiKey: process.env.TRIPORT_API_KEY! });


const stream = client.solana.stream({
  transactions: {
    client_filter_1: {
      vote: false,
      failed: false,
      accountInclude: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
    },
  },
  commitment: "confirmed",
});


for await (const update of stream) {
  if (update.transaction) {
    console.log(update.transaction.transaction.signature, update.transaction.slot);
  }
}


// Atomically replace the filter set — start watching slots instead:
await stream.update({ slots: { my_slots: { filterByCommitment: true } }, commitment: "finalized" });

Python (triport-sdk)

import os
from triport import Triport


client = Triport(api_key=os.environ["TRIPORT_API_KEY"])


stream = client.solana.stream(
    transactions={
        "client_filter_1": {
            "vote": False,
            "failed": False,
            "accountInclude": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
        }
    },
    commitment="confirmed",
)


for update in stream:
    if "transaction" in update:
        tx = update["transaction"]
        print(tx["transaction"]["signature"], tx["slot"])

Notes

  • Atomic filter replacement. Each SubscribeRequest fully replaces the previous filter set. There is no per-filter unsubscribe — send a new request with the desired blocks, and use empty blocks ({}) to drop an update kind.
  • max_streams, not RPS. Your tier caps the number of concurrent open /ws/sol-stream connections (pro = 8, business = 20). Opening one beyond your cap closes the new connection with 4003 forbidden. There is no per-message RPS bucket and no daily quota on this channel.
  • accountsDataSlice trims the data field of account updates server-side — set offset/length to fetch only the bytes you parse, which can sharply cut bandwidth on large accounts.
  • Pick the right channel. For low-volume, individually-managed subscriptions, /ws/sol is simpler. For Ethereum/Polygon streams see /ws/eth and /ws/polygon. For curated account-change pushes see /ws/sol-watch.
  • Commitment defaults to confirmed; choose processed for lowest latency (may revert) or finalized for irreversibility.