TriportRPC

Watchlist Push WebSocket — /ws/sol-watch

Streams real-time account-change events for the Solana pubkeys your tenant has registered on its watchlist.

Solanasol.watchlistBusiness tier; up to 10000 watched pubkeys per tenant. RPS-per-tier with burst — no daily quota.

/ws/sol-watch is a custom Triport push channel. Once you have registered one or more pubkeys on your watchlist through the REST control plane, this socket delivers an event every time the watched account's on-chain state actually changes. A server-side classifier filters out no-op churn (for example rent rebates that don't move the balance you care about), so you receive WatchEvent messages only for meaningful changes.

One socket serves one client connection and is scoped server-side to the tenant identified by the bearer token — you can never see another tenant's entries. You declare which watchlist entries to stream by sending a single WatchSubscribe frame after the socket opens; sending it again atomically replaces the active id-set. The connection stays open, emitting WatchEvent notifications, a 30-second idle WatchHeartbeat, and WatchEntryRevoked frames if entries are removed mid-stream, until you disconnect or every active entry has been revoked.

Prerequisite — register entries first. This channel only pushes events for entries that already exist. Create them via the REST control plane with POST /v1/sol/watchlist (using callback_type: "ws") before you subscribe. The WatchSubscribe frame references those entries by their returned UUIDs.

Parameters

Connection

Authorizationheaderrequired
Bearer $TRIPORT_API_KEY. May instead be sent as the first WS frame {"jsonrpc":"2.0","method":"auth","params":["$TRIPORT_API_KEY"]}.
WatchSubscribe frame (client → server)object
actionstringrequired
Must be the constant "watchlist.subscribe".
entriesstring[]required
Watchlist entry UUIDs to listen on (min 1). Use ["*"] to opt into all entries owned by the authenticated tenant.

Response

The server first acknowledges the subscription:

Then pushes WatchEvent frames as the watched accounts change:

{
  "action": "watchlist.event",
  "entry_id": "8f1d3a2c-9b4e-4c7a-bf21-1e0d5a6c7b90",
  "pubkey": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
  "slot": 287654321,
  "change_kind": "lamports",
  "before": { "lamports": 2039280, "owner": "11111111111111111111111111111111", "data_size": 0 },
  "after":  { "lamports": 5039280, "owner": "11111111111111111111111111111111", "data_size": 0 },
  "signature": "5h2k...9pQr",
  "timestamp": "2026-05-29T12:34:56Z"
}

When idle, a heartbeat arrives every 30 seconds:

{ "action": "watchlist.heartbeat", "ts": "2026-05-29T12:35:26Z", "active_entries": 1 }

If an entry stops streaming mid-connection (for example it was deleted via REST, or your tier was downgraded), a revocation frame is pushed:

{
  "action": "watchlist.revoked",
  "entry_id": "8f1d3a2c-9b4e-4c7a-bf21-1e0d5a6c7b90",
  "reason": "deleted"
}
actionstring
Constant "watchlist.subscribed".
acceptedstring[] (uuid)
Effective entry ids the server is now streaming.
rejectedobject[]
Ids excluded from the subscription. Each has id (uuid) and reasonunknown, not_owned, deleted.

Lifecycle

  1. Open /ws/sol-watch with bearer authentication.
  2. Send a WatchSubscribe frame listing the entry UUIDs to stream — or ["*"] to stream every entry your tenant owns.
  3. Receive a WatchSubscribeAck reporting the accepted ids the server is now streaming and any rejected ids (with a reason).
  4. Receive the WatchEvent stream (plus heartbeats and any revocations) until the connection closes.

Errors

Before closing the connection, the server may send a WSError frame, then close with one of the canonical WebSocket close codes:

Close codeMeaningWhen it happens
4001unauthorizedMissing or invalid bearer token on the upgrade / auth frame.
4003forbiddenToken lacks the sol.watchlist scope, or your tier is below business.
4029rate_limitedPer-tier RPS (with burst) exceeded.
4030trial_expiredTrial period has ended.

The application error frame carries error (one of unauthorized, forbidden, rate_limited, trial_expired, unknown), an optional message, and tier hints (current_tier, required_tier, upgrade_url, retry_after_sec, limit_rps). See errors for the full envelope and SDK error classes.

Examples

JavaScript (fetch / WebSocket)

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


ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    action: "watchlist.subscribe",
    entries: ["8f1d3a2c-9b4e-4c7a-bf21-1e0d5a6c7b90"],
  }));
});


ws.addEventListener("message", (ev) => {
  const msg = JSON.parse(ev.data);
  switch (msg.action) {
    case "watchlist.subscribed":
      console.log("streaming", msg.accepted, "rejected", msg.rejected);
      break;
    case "watchlist.event":
      console.log(`${msg.pubkey} ${msg.change_kind} @ slot ${msg.slot}`);
      break;
    case "watchlist.revoked":
      console.warn("entry revoked", msg.entry_id, msg.reason);
      break;
    case "watchlist.heartbeat":
      // idle keepalive
      break;
  }
});

TypeScript SDK (@triport/sdk)

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


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


const stream = client.solana.watchlist.subscribe({
  entries: ["8f1d3a2c-9b4e-4c7a-bf21-1e0d5a6c7b90"], // or ["*"]
});


stream.on("ack", ({ accepted, rejected }) => {
  console.log("streaming", accepted, "rejected", rejected);
});
stream.on("event", (e) => {
  console.log(`${e.pubkey} ${e.change_kind} @ slot ${e.slot}`, e.before, e.after);
});
stream.on("revoked", (r) => console.warn("revoked", r.entry_id, r.reason));

Python (triport-sdk)

import os
from triport import Triport


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


with client.solana.watchlist.subscribe(
    entries=["8f1d3a2c-9b4e-4c7a-bf21-1e0d5a6c7b90"],  # or ["*"]
) as stream:
    for msg in stream:
        if msg.action == "watchlist.subscribed":
            print("streaming", msg.accepted, "rejected", msg.rejected)
        elif msg.action == "watchlist.event":
            print(f"{msg.pubkey} {msg.change_kind} @ slot {msg.slot}")
        elif msg.action == "watchlist.revoked":
            print("revoked", msg.entry_id, msg.reason)

Notes

  • Register before you subscribe. Entries must exist via POST /v1/sol/watchlist (with callback_type: "ws"); ids that aren't owned by your tenant come back in the WatchSubscribeAck.rejected array rather than failing the whole subscription.
  • Re-subscribing replaces the set. A second WatchSubscribe atomically swaps the active id-set — it does not merge with the previous one.
  • ["*"] is dynamic at subscribe time. It resolves to the entries your tenant owns when the frame is processed; entries added later require a fresh WatchSubscribe.
  • Heartbeats vs. events. Expect a WatchHeartbeat roughly every 30 seconds when no changes occur; treat a long gap with no heartbeat as a dead connection and reconnect.
  • Capacity. The Business tier allows up to 10000 watched pubkeys per tenant. Plan your watchlist registrations against that ceiling.
  • Related: errors, rate limits and tiers, authentication.