cd /news/web3/building-a-live-solana-tps-meter-wit… · home topics web3 article
[ARTICLE · art-10034] src=dev.to ↗ pub= topic=web3 verified=true sentiment=· neutral

Building a Live Solana TPS Meter with OrbitFlare's TypeScript SDK

This article explains how to build a live transactions-per-second (TPS) meter for any specific Solana program using OrbitFlare's TypeScript SDK. The solution uses a Jetstream subscription to count transactions filtered by a target program, combined with a WebSocket slot subscription to verify the connection is still active when the program goes quiet. The entire tool runs in a terminal as a single TypeScript file under 100 lines of code, displaying real-time TPS rates and recent transaction signatures.

read4 min views18 publishedMay 22, 2026

Every Solana dashboard tells you the network's TPS. None of them tell you how busy the one program you care about is right now. Did Pumpfun cool off after a launch? Is Raydium spiking? Is the program your bot listens to still alive in the last 30 seconds? You're stuck picking between a smoothed-out aggregate or a paid dashboard that refreshes once a minute.

The smallest thing that answers the question directly is below. Point it at any Solana program, get a live transactions-per-second gauge in the terminal with a recent-signatures tape under it. One TypeScript file, less than 100 LoC, runs in a terminal, built on .

@orbitflare/sdk

jetstream tx rate: pumpfun (6EF8rrec...)
chain slot:        421442130

last 1s:     14 tx/s
last 10s:    9.7 tx/s avg

recent signatures:
   0.1s ago  slot 421442129  4bpPQBUHmobNJ4SS9ETLaMwY...
   0.4s ago  slot 421442128  Gsi3P32j6h9Yx3ce1gfHR5Pa...
   0.7s ago  slot 421442128  55AWdcey9N1WTYkmQDXPeKHD...
   ...

Full source: github.com/orbitflare/jetstream-tx-rate.

The honest meter problem #

The meter could be built with one Jetstream subscription: filter on the program, count transactions per second. That gets done in 50 lines. The problem only shows up the first time the program goes quiet for ten seconds. The meter reads zero. So does the signature tape. And now there's no way to know whether the program is actually idle or whether the SDK silently lost the connection three minutes ago.

The fix is one extra subscription on a second transport. WebSocket slotSubscribe()

ticks every ~400ms whether anything is happening or not, because the chain itself never stops.

The clients #

import { JetstreamClientBuilder } from '@orbitflare/sdk/jetstream';
import { WsClientBuilder } from '@orbitflare/sdk/ws';

const jet = new JetstreamClientBuilder()
  .url('http://jp.jetstream.orbitflare.com')
  .build();

const ws = await new WsClientBuilder()
  .url('ws://ams.rpc.orbitflare.com')
  .build();

That's it for setup. Jetstream needs no api key. WebSocket picks one up from ORBITFLARE_LICENSE_KEY

if set.

The two subscriptions #

WebSocket pipes the chain slot into a shared variable. One callback, no state machine:

let currentSlot = 0;
const slotSub = await ws.slotSubscribe();
slotSub.on((s) => {
  if (typeof s?.slot === 'number') currentSlot = s.slot;
});

Jetstream gets the transaction firehose, filtered server-side to just the program of interest. for await

drains it and stamps each arrival:

const stream = jet.subscribe({
  transactions: {
    target: {
      accountInclude: [],
      accountExclude: [],
      accountRequired: [program],
    },
  },
  accounts: {},
  ping: { id: 1 },
} as any);

const arrivals: number[] = [];
const recent: { slot: number; sig: string; at: number }[] = [];

for await (const u of stream) {
  const sig = u.transaction?.transaction?.signature;
  if (!sig) continue;
  const now = Date.now();
  arrivals.push(now);
  recent.unshift({ slot: Number(u.transaction!.slot), sig: bs58.encode(sig), at: now });
  if (recent.length > 10) recent.length = 10;
}

The accountRequired

filter is the load-bearing piece. It tells the Jetstream server to only ship transactions where the program is in the account list, so the bandwidth that reaches the laptop is already the data that matters. No client-side filtering, no wasted parsing.

Two pieces of state, both trivial. arrivals

is a list of millisecond timestamps the renderer reads to compute rates. recent

is a 10-element ring of the most recent signatures, kept in arrival order, displayed as a scrolling tape under the gauge.

The renderer #

Every second, the renderer does three things. First it drops any timestamp older than 10 seconds from arrivals

. Then it counts: how many timestamps in the last 1 second (that's the instantaneous rate), and the total count divided by 10 (that's the rolling 10-second average). Finally it clears the screen with an ANSI escape and prints the dashboard.

function render(): void {
  const now = Date.now();
  while (arrivals.length && now - arrivals[0]! > 10_000) arrivals.shift();
  const last1s = arrivals.filter((t) => now - t <= 1_000).length;
  const last10sAvg = arrivals.length / 10;

  process.stdout.write('\x1B[2J\x1B[H');
  console.log(`jetstream tx rate: ${label} (${program.slice(0, 8)}...)`);
  console.log(`chain slot:        ${currentSlot}\n`);
  console.log(`  last 1s:  ${String(last1s).padStart(5)} tx/s`);
  console.log(`  last 10s: ${last10sAvg.toFixed(1).padStart(5)} tx/s avg\n`);
  console.log('recent signatures:');
  for (const r of recent) {
    const age = Math.max(0, (now - r.at) / 1000).toFixed(1);
    console.log(`  ${age.padStart(4)}s ago  slot ${r.slot}  ${r.sig.slice(0, 24)}...`);
  }
}

setInterval(render, 1_000);
render();

Running it #

git clone https://github.com/orbitflare/jetstream-tx-rate.git
cd jetstream-tx-rate
npm install
npm start pumpfun       # or raydium, jupiter, or any raw program id

Signatures land within a second of the first run. Watching Pumpfun for five minutes shows it idling around 8-15 tx/s on a slow afternoon and spiking to 50-80 when a launch hits. Raydium runs lighter on average but every big swap is a visible jolt. Any program ID on Solana gets the same view of how busy it actually is, not how busy it averages out.

What this tiny snippet doesn't have to do #

Reconnects, regional failover, api-key scrubbing, ping/pong liveness, the protobuf wire format, the WebSocket re-subscribe dance after a drop. The SDK handles all of it the same way across both transports. None of it shows up in the source. What's left is a filter, a callback, and the rendering math.

That’s the trade. And a good one. Use the SDK’s plumbing, write the part that’s actually yours.

── more in #web3 4 stories · sorted by recency
── more on @solana 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-live-sola…] indexed:0 read:4min 2026-05-22 ·