Documentation

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.

NOTE

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:

  1. 1
    Send

    sendTransaction over HTTP RPC. It returns the signature as soon as the node accepts the transaction.

  2. 2
    Subscribe

    signatureSubscribe over WebSocket. This is the fast path: on our shared nodes the confirmed notification arrived 0.35-0.45 seconds after the transaction was processed.

  3. 3
    Fall back to polling

    If no notification arrives within a timeout (or the socket drops), poll getSignatureStatuses every 2 seconds. Stop when the status is confirmed, or when the block height passes lastValidBlockHeight - 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));
NOTE

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

  1. Reconnect with exponential backoff. WebSocket connections can drop. Implement retry starting at 1 second, capped at 30 seconds.

  2. Re-establish subscriptions on reconnect. Subscription IDs are connection-scoped. After reconnect, all subscriptions are gone - resubscribe to what you need.

  3. 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.

  4. Watch your subscription budget. Total subs per plan is a hard cap. See Limits.

  5. Switch to gRPC for high throughput. If you need many account / transaction streams at once, gRPC is the right tool.

Where to go next

Limits
Connection caps and subscription totals.
Restrictions
Blocked subscriptions.
Free Trials
24-hour trial.
Switch to gRPC
For high-throughput streams.