Watchlist Push WebSocket — /ws/sol-watch
Streams real-time account-change events for the Solana pubkeys your tenant has registered on its watchlist.
/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(usingcallback_type: "ws") before you subscribe. TheWatchSubscribeframe references those entries by their returned UUIDs.
Parameters
Connection
AuthorizationheaderrequiredBearer $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)objectactionstringrequired"watchlist.subscribe".entriesstring[]required["*"] 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"watchlist.subscribed".acceptedstring[] (uuid)rejectedobject[]id (uuid) and reason ∈ unknown, not_owned, deleted.Lifecycle
- Open
/ws/sol-watchwith bearer authentication. - Send a
WatchSubscribeframe listing the entry UUIDs to stream — or["*"]to stream every entry your tenant owns. - Receive a
WatchSubscribeAckreporting theacceptedids the server is now streaming and anyrejectedids (with a reason). - Receive the
WatchEventstream (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 code | Meaning | When it happens |
|---|---|---|
4001 | unauthorized | Missing or invalid bearer token on the upgrade / auth frame. |
4003 | forbidden | Token lacks the sol.watchlist scope, or your tier is below business. |
4029 | rate_limited | Per-tier RPS (with burst) exceeded. |
4030 | trial_expired | Trial 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(withcallback_type: "ws"); ids that aren't owned by your tenant come back in theWatchSubscribeAck.rejectedarray rather than failing the whole subscription. - Re-subscribing replaces the set. A second
WatchSubscribeatomically 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 freshWatchSubscribe.- Heartbeats vs. events. Expect a
WatchHeartbeatroughly 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
10000watched pubkeys per tenant. Plan your watchlist registrations against that ceiling. - Related: errors, rate limits and tiers, authentication.