Solana WebSocket examples
Copy-paste WebSocket subscription examples for slot, account, program, and signature updates, plus a transaction confirmation flow: sendTransaction, signatureSubscribe, getSignatureStatuses fallback.
// updated 2026-09-29
Copy-paste working examples for connecting to Supanode WebSocket.
Prerequisites
- An active Supanode Bundle subscription or free trial. Don't have one? Start one in the portal.
- Your access token, issued in the portal - see Authentication.
- The WebSocket endpoint URL (default:
wss://fra.sol.supanode.xyz:8900).
The token goes on the handshake, either as an x-token header or as an api-key query parameter.
Use the query parameter when your client cannot set handshake headers. @solana/web3.js is the common case - its wsEndpoint takes a URL and nothing else, so append ?api-key=... to it. Anything that lets you set headers should send x-token instead, since a query string ends up in logs.
Subscribe to slot updates
The lightest possible WebSocket subscription - useful as a smoke test.
import { Connection } from "@solana/web3.js";
const token = process.env.SUPANODE_TOKEN!;
const connection = new Connection("https://fra.sol.supanode.xyz:8899", {
httpHeaders: { "x-token": token },
// web3.js cannot set headers on the WS handshake — pass the token in the URL.
wsEndpoint: `wss://fra.sol.supanode.xyz:8900/?api-key=${token}`,
});
const subscriptionId = connection.onSlotChange((slotInfo) => {
console.log("New slot:", slotInfo.slot);
});
// To unsubscribe later:
// await connection.removeSlotChangeListener(subscriptionId);
Subscribe to an account
Stream updates for a single account.
import { Connection, PublicKey } from "@solana/web3.js";
const token = process.env.SUPANODE_TOKEN!;
const connection = new Connection("https://fra.sol.supanode.xyz:8899", {
httpHeaders: { "x-token": token },
// web3.js cannot set headers on the WS handshake — pass the token in the URL.
wsEndpoint: `wss://fra.sol.supanode.xyz:8900/?api-key=${token}`,
});
const accountAddress = new PublicKey("YOUR_ACCOUNT_PUBKEY");
const subscriptionId = connection.onAccountChange(
accountAddress,
(accountInfo) => {
console.log("Balance:", accountInfo.lamports);
console.log("Data:", accountInfo.data.toString("base64"));
},
"confirmed"
);
Subscribe to a program (with required filter)
programSubscribe requires either a dataSize or memcmp filter on shared plans. Whole-program subscriptions are blocked - see Restrictions.
import { Connection, PublicKey } from "@solana/web3.js";
const token = process.env.SUPANODE_TOKEN!;
const connection = new Connection("https://fra.sol.supanode.xyz:8899", {
httpHeaders: { "x-token": token },
// web3.js cannot set headers on the WS handshake — pass the token in the URL.
wsEndpoint: `wss://fra.sol.supanode.xyz:8900/?api-key=${token}`,
});
const programId = new PublicKey("YOUR_PROGRAM_ID");
const subscriptionId = connection.onProgramAccountChange(
programId,
(keyedAccountInfo) => {
console.log("Account:", keyedAccountInfo.accountId.toBase58());
console.log("Lamports:", keyedAccountInfo.accountInfo.lamports);
},
"confirmed",
[{ dataSize: 165 }] // required filter
);
Subscribe to a transaction signature
signatureSubscribe sends one notification when the transaction reaches the requested commitment, then the server removes the subscription. No unsubscribe call is needed.
If the transaction is already confirmed when you subscribe, the notification still arrives, usually within one slot.
import { Connection } from "@solana/web3.js";
const token = process.env.SUPANODE_TOKEN!;
const connection = new Connection("https://fra.sol.supanode.xyz:8899", {
httpHeaders: { "x-token": token },
// web3.js cannot set headers on the WS handshake - pass the token in the URL.
wsEndpoint: `wss://fra.sol.supanode.xyz:8900/?api-key=${token}`,
});
const signature = "YOUR_TX_SIGNATURE";
// One-shot: fires once, then the server removes the subscription.
connection.onSignature(
signature,
(result, context) => {
if (result.err) console.error("Failed in slot", context.slot, result.err);
else console.log("Confirmed in slot", context.slot);
},
"confirmed"
);
Confirm a sent transaction
A reliable confirmation flow uses both transports:
- 1Send
sendTransactionover HTTP RPC. It returns the signature as soon as the node accepts the transaction. - 2Subscribe
signatureSubscribeover WebSocket. This is the fast path: on our shared nodes theconfirmednotification arrived 0.35-0.45 seconds after the transaction was processed. - 3Fall back to polling
If no notification arrives within a timeout (or the socket drops), poll
getSignatureStatusesevery 2 seconds. Stop when the status isconfirmed, or when the block height passeslastValidBlockHeight- after that the transaction can no longer land and is safe to rebuild with a fresh blockhash and resend.
import {
Connection, Keypair, SystemProgram, Transaction, TransactionSignature,
} from "@solana/web3.js";
const token = process.env.SUPANODE_TOKEN!;
const connection = new Connection("https://fra.sol.supanode.xyz:8899", {
httpHeaders: { "x-token": token },
// web3.js cannot set headers on the WS handshake - pass the token in the URL.
wsEndpoint: `wss://fra.sol.supanode.xyz:8900/?api-key=${token}`,
});
type Result = "confirmed" | "failed" | "expired";
// 2. Wait for signatureNotification. Resolve null on timeout so polling takes over.
function waitForSignature(signature: TransactionSignature, timeoutMs: number) {
return new Promise((resolve: (result: Result | null) => void) => {
const timer = setTimeout(() => {
connection.removeSignatureListener(subId).catch(() => {});
resolve(null);
}, timeoutMs);
const subId = connection.onSignature(
signature,
(result) => {
clearTimeout(timer);
resolve(result.err ? "failed" : "confirmed");
},
"confirmed"
);
});
}
// 3. Fallback: poll getSignatureStatuses until confirmed or the blockhash expires.
async function pollSignature(signature: TransactionSignature, lastValidBlockHeight: number) {
while (true) {
const { value: [status] } = await connection.getSignatureStatuses([signature]);
if (status?.err) return "failed";
if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
return "confirmed";
}
if ((await connection.getBlockHeight("confirmed")) > lastValidBlockHeight) return "expired";
await new Promise((r) => setTimeout(r, 2000));
}
}
async function sendAndConfirm(tx: Transaction, payer: Keypair) {
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
tx.recentBlockhash = blockhash;
tx.feePayer = payer.publicKey;
tx.sign(payer);
// 1. Send.
const signature = await connection.sendRawTransaction(tx.serialize(), { maxRetries: 5 });
const result =
(await waitForSignature(signature, 15_000)) ??
(await pollSignature(signature, lastValidBlockHeight));
return { signature, result };
}
// Usage: a 1-lamport transfer to yourself.
const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.PAYER_SECRET!)));
const tx = new Transaction().add(
SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: payer.publicKey, lamports: 1 })
);
console.log(await sendAndConfirm(tx, payer));
Budget. sendTransaction counts against your plan's TPS. The subscribe call, getSignatureStatuses and getBlockHeight count against RPS at 1 unit each, so the polling loop above costs 1 RPS while it runs. When you track many transactions, batch up to 256 signatures into one getSignatureStatuses call. See RPC limits.
Production tips
-
Reconnect with exponential backoff. WebSocket connections can drop. Implement retry starting at 1 second, capped at 30 seconds.
-
Re-establish subscriptions on reconnect. Subscription IDs are connection-scoped. After reconnect, all subscriptions are gone - resubscribe to what you need.
-
Send a ping every 30-60 seconds. Idle timeout is 10 minutes, but a periodic ping keeps the connection healthy and detects half-open sockets.
-
Watch your subscription budget. Total subs per plan is a hard cap. See Limits.
-
Switch to gRPC for high throughput. If you need many account / transaction streams at once, gRPC is the right tool.