DEV Community

OrbitFlare RPC
OrbitFlare RPC

Posted on

Jetstream V2 - The Ins and Outs

Banner
Jetstream has always had one job: get you Solana transactions before everyone else sees them. It sends back decoded shreds instead of waiting for Geyser, which means the data reaches your client while other feeds are still assembling blocks. That part hasn't changed, and it was never going to. What has changed is everything around it.

Jetstream V2 is a new protocol running alongside V1 on the same endpoints. Slightly better latency, same authentication, same regions. The difference is in what each message carries and what you can do with a stream once it's open. V1 hands you a decoded transaction and wishes you luck with the rest. V2 hands you the decoded transaction plus the answers to the questions you were about to ask, and it lets you reshape your subscription without ever dropping the connection.

Let's walk through what that actually means.

More data per message, fewer RPC calls

Here's the dirty secret of consuming any fast transaction stream: the stream is rarely your bottleneck. Your follow-up work is. You get a transaction in a few milliseconds, and then you spend an order of magnitude longer figuring out who paid the fee, what compute price it set, and which accounts it actually touches.

The worst offender is versioned transactions. A v0 transaction doesn't carry its full account list. It carries a reference to an on-chain address lookup table plus a set of indexes into it. On V1, learning which accounts the transaction really touches means a full RPC round trip to fetch that table, then resolving the indexes yourself. That's one extra request and response per transaction, sitting directly on your critical path, and it's far slower than the stream that delivered the transaction in the first place. You paid for shred-level latency and then handed most of it back to an RPC call.

V2 fixes this with enrichment. Turn it on and every transaction arrives with the resolved lookup table addresses already inline, in loaded_writable_addresses and loaded_readonly_addresses. No round trip, no index math, no lookup sitting between a transaction arriving and you acting on it. If a table can't be fully resolved, the transaction is flagged with alt_resolution_incomplete. In practice you'll rarely see the flag at all. Last we measured, the miss rate was in the 0.00x percent range, and it keeps shrinking as the resolver improves.

The rest of the enrichment fields cover the busywork you'd otherwise repeat on every single transaction:

  • fee_payer, so you stop deriving it from the account keys and header
  • compute_unit_price and compute_limit, so you stop parsing compute budget instructions
  • writable_accounts and program_ids, precomputed instead of walked
  • tx_size, measured for you

One note worth internalizing: compute_unit_price is the micro-lamports-per-compute-unit value from SetComputeUnitPrice, not the total priority fee. If you want the fee in lamports, it's compute_unit_price × compute_limit / 1e6.

Enrichment is opt-in and off by default, set per filter with include_enrichment: true. Turning it on adds no latency. The only cost is a modestly larger message, which is a trade most people will sign without reading the fine print.

Filters you can change while the stream is running

On V1, filters are a one-shot deal. You declare them in the message that opens the stream, and if you later want to watch something different, you tear the connection down and open a new one. For a trading system, that reconnect window is a blind spot at exactly the moment you decided something new was worth watching.

V2 makes the request side of the stream bidirectional. You add and remove filters on the live connection, and the stream never blinks. Each filter carries a filter_id that you choose, the server explicitly acknowledges every add and remove (with a reason when it rejects one), and every delivered transaction lists the filter IDs it matched. So when you're running a pump.fun filter and a Raydium filter on the same connection, you know exactly which one produced each transaction without re-deriving it from the account keys.

Slot events, sequence numbers, and heartbeats

V2 adds a second stream that V1 never had: SubscribeSlots. It emits an event when a slot goes ALIVE (first shred received), COMPLETE (last shred received), or DEAD (skipped or superseded), along with the slot's leader and parent. If you've ever inferred chain progress from the rhythm of your transaction flow, this is the cheap, honest replacement for that guesswork.

Then there are the two features nobody gets excited about until three in the morning, when they suddenly become the most important features in the protocol.

Every V2 message, transactions and slot events alike, carries a monotonic sequence number. If you see a gap, you dropped a message, and you know it immediately instead of discovering it in a reconciliation job next week. And when a transaction stream goes quiet, the server sends a Heartbeat every 60 seconds stamped with its own clock. That keeps the connection alive through load balancer idle timeouts and, more importantly, tells a quiet stream apart from a dead one. V1 gave you neither, and every serious V1 consumer ended up half-building both.

V1 vs V2, side by side

Here's the whole comparison in one picture:

V1 vs V2 comparision

Our SDKs already speak V2

Both OrbitFlare SDKs - Rust and TypeScript - ship V2 as a parallel module next to V1, with the same builder ergonomics across both. Nothing about your V1 code breaks; the V1 client stays where it always was, and V2 lives one module deeper.

In Rust that looks like this:

use orbitflare_sdk::jetstream::v2::{JetstreamClientBuilder, TransactionFilter};

let client = JetstreamClientBuilder::new()
    .url("http://fra.jetstream.orbitflare.com")
    .build()?;

let filter = TransactionFilter::new()
    .account_include(["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"])
    .include_enrichment(true)
    .with_id("pumpfun");

let mut stream = client.subscribe_transactions(vec![filter]);
Enter fullscreen mode Exit fullscreen mode

And because the stream stays open for business, changing your mind mid-flight is a method call, not a reconnect:

let handle = stream.handle();
handle.add_filters(vec![
    TransactionFilter::new()
        .account_include([RAYDIUM])
        .include_enrichment(true)
        .with_id("raydium"),
])?;
Enter fullscreen mode Exit fullscreen mode

Everything the SDK already did for your connection still works the same way on V2: retries, fallback URLs, keepalive, ping intervals, and picking up the endpoint from ORBITFLARE_JETSTREAM_URL. The SDK keeps the connection alive; you deal with the transactions.

The examples got the same upgrade

The jetstream-client-example repo now ships paired examples: a v1 binary on the classic fixed-filter client and a v2 binary that exercises the new protocol properly. The V2 examples aren't just the V1 code with a renamed import, either.

Both the Rust and TypeScript SDK repos carry their own jetstream_v2_basic examples as well, if you want the minimal version to crib from.

So, should you move?

For new integrations, yes, use V2. It's where enrichment, live filters, slot events, and the liveness signals live, and it's where every new capability will land. If you have a V1 client in production, there's no fire drill: V1 stays fully supported with no set retirement date, and since both versions share the same endpoints and the same authentication, migrating is a change in your client code rather than your infrastructure. You can even run both side by side on the same connection details while you cut over.

But the case for moving is easy to state. V2 takes an RPC round trip off your critical path, hands you precomputed answers instead of homework, never makes you reconnect to change what you're watching, and tells you immediately when something is missing or stalled. The stream was already fast. Now the rest of your pipeline gets to be fast too.

Resources

  1. Jetstream v2 docs
  2. v1 vs v2 comparison
  3. v2 protocol reference
  4. Client examples on GitHub
  5. orbitflare-sdk on crates.io | @orbitflare/sdk on npm

If you build on V2, or hit an edge that should be smoother, come tell us about it on Discord. We'd love to see what you ship.

Top comments (0)