The OrbitFlare SDK now speaks Go. It's the third language after Rust and TypeScript, and it ships the same surface as both: RPC, WebSocket, Yellowstone gRPC, and Jetstream clients, each in its own package, with the same builder pattern and the same connection plumbing handled for you. If you've used either of the other SDKs, you already know this one.
go get github.com/orbitflare/orbitflare-sdk-go
To show it off, we wanted to build something small that no other post on this blog has built. Every tutorial we've written so far watches transactions: a wallet ticker, a TPS meter, an indexer, a trade tracker. Nobody has watched the thing underneath all of them. Slots.
A slot is the network's heartbeat. Every 400 milliseconds or so, some validator has the leader role, produces a block, and hands off. Sometimes a leader fails to produce and the slot is skipped. Your transactions land inside this rhythm, and if you send them, the rhythm decides your fate: who the current leader is, how far away the next one is, and whether slots are completing cleanly or dying.
Jetstream v2 has a stream for exactly this, and the Go SDK ships it. SubscribeSlots emits an event when a slot goes ALIVE (first shred received), COMPLETE (last shred received), or DEAD (skipped or superseded). So the demo picked itself: a slot radar. One terminal screen showing the current slot and leader, the measured slot time, who's up next, and every skipped slot as it happens. Around 180 lines of Go, using two clients from the SDK.
slot radar · epoch 1011
slot 436945880 · leader Fd7b…69Nk
measured slot time: 443ms avg over last 30
next up:
slot 436945884 Awes…vpLM (in 4 slots, ~1.6s)
slot 436945888 GYx8…3YGQ (in 8 slots, ~3.2s)
slot 436945892 EdFU…sk6w (in 12 slots, ~4.8s)
slot 436945896 5pPR…HzSm (in 16 slots, ~6.4s)
last 60 slots: ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✗✗✗✓✓✓✓✓✓✓●
skipped this session: 4 (latest: 436945871)
The Clients
Two builders, one per client.
rpcClient, err := rpc.NewBuilder().
URL("http://ams.rpc.orbitflare.com").
APIKey(os.Getenv("ORBITFLARE_LICENSE_KEY")).
Build()
jet, err := jetstreamv2.NewBuilder().
URL("http://ams.jetstream.orbitflare.com").
Build()
Every client accepts multiple URLs if you want failover, and the first one is the primary. That's the whole setup.
The Schedule
The slot stream tells us what's happening right now, but "who leads next" lives in the leader schedule, which is an RPC question. Two calls give us the schedule:
raw, _ := rpcClient.Request(ctx, "getEpochInfo", []any{})
var epoch struct {
Epoch uint64 `json:"epoch"`
AbsoluteSlot uint64 `json:"absoluteSlot"`
SlotIndex uint64 `json:"slotIndex"`
}
json.Unmarshal(raw, &epoch)
epochStart := epoch.AbsoluteSlot - epoch.SlotIndex
raw, _ = rpcClient.Request(ctx, "getLeaderSchedule", []any{nil})
var schedule map[string][]uint64
json.Unmarshal(raw, &schedule)
leaderBySlot := make(map[uint64]string, 432_000)
for leader, idxs := range schedule {
for _, i := range idxs {
leaderBySlot[epochStart+i] = leader
}
}
There's one small trap here, and it's the reason for the getEpochInfo call. The schedule comes back keyed by validator identity with slot indexes relative to the epoch start, not absolute slot numbers. Subtracting slotIndex from absoluteSlot gives you the epoch's first absolute slot, and adding that to each index turns the whole schedule into a flat slot → leader map.
The Slot Stream
Now the live side. SubscribeSlots returns a stream, and a goroutine drains it into a channel:
slots := jet.SubscribeSlots()
defer slots.Close()
events := make(chan *pb.SlotEvent, 1024)
go func() {
for {
ev, err := slots.Next(ctx)
if err != nil {
log.Fatal(err)
}
events <- ev
}
}()
The main loop is one select over two channels: slot events mutate state, and a 500ms ticker redraws the screen. This is the whole engine of the program:
for {
select {
case ev := <-events:
s := ev.GetSlot()
status[s] = ev.GetStatus()
if s > maxSlot {
maxSlot = s
}
if ev.GetStatus() == pb.SlotStatus_SLOT_STATUS_ALIVE {
if s == lastSlot+1 && !lastAlive.IsZero() {
timings = append(timings, time.Since(lastAlive))
}
lastSlot, lastAlive = s, time.Now()
}
if ev.GetStatus() == pb.SlotStatus_SLOT_STATUS_DEAD {
skipped = append(skipped, s)
}
case <-ticker.C:
render(status, leaderBySlot, maxSlot, timings, skipped, epoch.Epoch)
}
}
Solana's 400ms slot time is a target, not a promise, and the honest way to measure the real one is the gap between consecutive ALIVE events, since ALIVE fires when the first shred of a slot reaches the network. Averaging the last 30 gaps gives you the network's actual current pace, measured from your own connection. Our run above says 443ms.
The Render
The render function walks state and prints. The next-up list scans forward from the current slot and collects the next four distinct leaders:
printed, cur := 0, leaderBySlot[maxSlot]
for s := maxSlot + 1; printed < 4 && s < maxSlot+400; s++ {
if l := leaderBySlot[s]; l != "" && l != cur {
fmt.Printf(" slot %d %s (in %d slots, ~%.1fs)\n",
s, short(l), s-maxSlot, float64(s-maxSlot)*0.4)
printed++
cur = l
}
}
and the tape, which maps the last 60 slots to one glyph each: ✓ for complete, ● for alive, ✗ for dead, and · for slots we haven't heard about. A skipped slot shows up as a ✗ in the tape the moment the network gives up on it.
Running It
git clone https://github.com/orbitflare/slot-radar.git
cd slot-radar
ORBITFLARE_LICENSE_KEY=ORBIT-... go run .
The schedule loads in a couple of seconds, the first slot event lands right after, and from there the radar just ticks. Leaders hand off every four slots, the measured slot time drifts between roughly 390 and 430 milliseconds depending on the day, and every few minutes a ✗ appears when a leader misses its window.
What the SDK Did
The program never mentions reconnection, and that's the point. If the Jetstream connection drops, the stream re-establishes and keeps delivering. If the primary endpoint misbehaves and you gave the builder fallback URLs, the client rotates through them and brings the primary back when it recovers. Keepalives, retry backoff, and API key redaction in error output all come along without a line of your code, and they behave identically to the Rust and TypeScript SDKs, because the three are kept in lockstep.
What's left in the source is the part that's actually yours: two RPC calls, one subscription, and a select loop. That's the trade the SDK offers in every language now, including Go.
Resources
- Full source: github.com/orbitflare/slot-radar
-
orbitflare-sdk-goon GitHub | pkg.go.dev reference - Go SDK docs
- Jetstream v2 docs
If you build something with the Go SDK, or hit an edge that should be smoother, come tell us on Discord. We'd love to see what you ship.

Top comments (0)