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.

Top comments (0)