Go SDK for OrbitFlare - RPC, gRPC (Yellowstone Geyser), Jetstream, and WebSocket clients for Solana.
go get github.com/orbitflare/orbitflare-sdk-goRequires Go 1.25 or later. Import only the clients you use; each lives in its own package.
import (
"github.com/orbitflare/orbitflare-sdk-go/rpc"
"github.com/orbitflare/orbitflare-sdk-go/ws"
"github.com/orbitflare/orbitflare-sdk-go/geyser"
"github.com/orbitflare/orbitflare-sdk-go/jetstream"
jetstreamv2 "github.com/orbitflare/orbitflare-sdk-go/jetstream/v2"
)package main
import (
"context"
"fmt"
"github.com/orbitflare/orbitflare-sdk-go/rpc"
)
func main() {
client, err := rpc.NewBuilder().
URL("http://ny.rpc.orbitflare.com").
APIKey("ORBIT-...").
Commitment("confirmed").
Build()
if err != nil {
panic(err)
}
ctx := context.Background()
slot, _ := client.GetSlot(ctx)
balance, _ := client.GetBalance(ctx, "CKs1E69a2e9TmH4mKKLrXFF8kD3ZnwKjoEuXa6sz9WqX")
blockhash, lastValid, _ := client.GetLatestBlockhash(ctx)
health, _ := client.Request(ctx, "getHealth", []any{})
raw, _ := client.RequestRaw(ctx, `{"jsonrpc":"2.0","id":1,"method":"getHealth","params":[]}`)
fmt.Println(slot, balance, blockhash, lastValid, string(health), string(raw))
}| Method | Returns |
|---|---|
GetSlot(ctx) |
Current slot (uint64) |
GetBalance(ctx, address) |
Lamports (uint64) |
GetAccountInfo(ctx, address) |
Account data (json.RawMessage) |
GetMultipleAccounts(ctx, addresses) |
Slice of accounts (auto-chunks to 100) |
GetLatestBlockhash(ctx) |
(blockhash, lastValidBlockHeight) |
GetTransaction(ctx, signature) |
Full transaction with metadata |
GetSignaturesForAddress(ctx, address, limit) |
Recent signatures |
GetProgramAccounts(ctx, programID) |
All accounts owned by a program |
GetRecentPrioritizationFees(ctx, addresses) |
Recent priority fees |
SendTransaction(ctx, txBase64) |
Signature string |
SimulateTransaction(ctx, txBase64) |
Simulation result |
GetTokenAccountsByOwner(ctx, owner, mint, programID) |
Token accounts |
GetTransactionsForAddress(ctx, address, options) |
Full transaction history with filters and pagination (OrbitFlare-specific) |
Request(ctx, method, params) |
Any RPC method by name |
RequestRaw(ctx, body) |
Raw JSON-RPC body string |
Build the subscription with the typed filter builder:
package main
import (
"context"
"errors"
"fmt"
"io"
"github.com/orbitflare/orbitflare-sdk-go/geyser"
)
func main() {
client, err := geyser.NewBuilder().
URL("http://ny.rpc.orbitflare.com:10000").
Build()
if err != nil {
panic(err)
}
defer client.Close()
request := geyser.NewSubscribeRequestBuilder().
Transactions("pumpfun", geyser.NewTransactionFilter().
Vote(false).
Failed(false).
AccountInclude("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P").
AccountRequired("So11111111111111111111111111111111111111112")).
Accounts("wsol-token-accounts", geyser.NewAccountFilter().
Owner("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").
Datasize(165).
Memcmp(geyser.MemcmpBase58(0, "So11111111111111111111111111111111111111112")).
Lamports(geyser.LamportsGt(0))).
Slots("slots", geyser.NewSlotFilter().FilterByCommitment(true)).
Commitment(geyser.Confirmed).
Build()
stream := client.Subscribe(request)
defer stream.Close()
ctx := context.Background()
for {
update, err := stream.Next(ctx)
if errors.Is(err, io.EOF) {
return
}
if err != nil {
panic(err)
}
if tx := update.GetTransaction(); tx != nil {
fmt.Printf("slot=%d\n", tx.GetSlot())
}
}
}Subscribe accepts a *geyser.SubscribeRequest (a re-export of the raw proto type), so you can also construct the request by hand instead of using the builder.
Available filters: TransactionFilter, AccountFilter (with Memcmp, Datasize, Lamports, TokenAccountState), SlotFilter, BlockFilter, and DeshredTransactionFilter.
package main
import (
"context"
"errors"
"fmt"
"io"
"github.com/orbitflare/orbitflare-sdk-go/jetstream"
)
func main() {
client, err := jetstream.NewBuilder().
URL("http://ny.jetstream.orbitflare.com").
Build()
if err != nil {
panic(err)
}
defer client.Close()
request := jetstream.NewSubscribeRequestBuilder().
Transactions("raydium", jetstream.NewTransactionFilter().
AccountInclude("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")).
Build()
stream := client.Subscribe(request)
defer stream.Close()
ctx := context.Background()
for {
update, err := stream.Next(ctx)
if errors.Is(err, io.EOF) {
return
}
if err != nil {
panic(err)
}
if tx := update.GetTransaction(); tx != nil {
fmt.Printf("slot=%d\n", tx.GetSlot())
}
}
}v2 (jetstream/v2) runs on the same endpoint and auth as v1 and is fully additive. Filters are managed at runtime over a bidirectional stream (no reconnect to change them), every response carries a monotonic Sequence for gap detection, slot lifecycle events are a separate stream, and transactions can opt into enrichment (fee payer, program ids, compute-unit price, loaded address-table addresses, and more).
package main
import (
"context"
"errors"
"fmt"
"io"
jetstreamv2 "github.com/orbitflare/orbitflare-sdk-go/jetstream/v2"
)
func main() {
client, err := jetstreamv2.NewBuilder().
URL("http://ny.jetstream.orbitflare.com").
Build()
if err != nil {
panic(err)
}
defer client.Close()
ctx := context.Background()
version, _ := client.GetVersion(ctx)
fmt.Printf("version=%s\n", version)
filter := jetstreamv2.NewTransactionFilter().
AccountInclude("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P").
IncludeEnrichment(true).
WithID("pumpfun")
stream := client.SubscribeTransactions(filter)
defer stream.Close()
for {
resp, err := stream.Next(ctx)
if errors.Is(err, io.EOF) {
return
}
if err != nil {
panic(err)
}
fmt.Printf("seq=%d\n", resp.GetSequence())
}
}Add or remove filters on a live stream without reconnecting:
handle := stream.Handle()
handle.AddFilters([]*jetstreamv2.TxFilter{
jetstreamv2.NewTransactionFilter().AccountInclude("...").WithID("raydium"),
})
handle.RemoveFilters([]string{"pumpfun"})Slot lifecycle events are a separate server stream:
slots := client.SubscribeSlots()
defer slots.Close()
for {
event, err := slots.Next(ctx)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
panic(err)
}
fmt.Printf("slot=%d status=%s\n", event.GetSlot(), event.GetStatus())
}package main
import (
"context"
"fmt"
"github.com/orbitflare/orbitflare-sdk-go/ws"
)
func main() {
ctx := context.Background()
client, err := ws.NewBuilder().
URL("ws://ny.rpc.orbitflare.com").
APIKey("ORBIT-...").
Build(ctx)
if err != nil {
panic(err)
}
defer client.Close()
sub, err := client.SlotSubscribe(ctx)
if err != nil {
panic(err)
}
defer sub.Unsubscribe()
for {
event, ok := sub.Next(ctx)
if !ok {
break
}
fmt.Println(string(event))
}
}Subscription methods: AccountSubscribe, LogsSubscribe, SlotSubscribe, SignatureSubscribe. All auto-resubscribe on reconnect.
All clients support multiple endpoints with automatic failover and health tracking. The first URL is the primary; the rest are fallbacks.
client, err := rpc.NewBuilder().
URLs(
"http://ny.rpc.orbitflare.com",
"http://fra.rpc.orbitflare.com",
"http://ams.rpc.orbitflare.com",
).
Build()Failing endpoints are quarantined with exponential cooldown (10s, 20s, 40s, max 60s) and automatically retried once the cooldown expires. Healthy endpoints are always preferred.
RPC calls retry on transient errors (5xx, 429, connection resets, Solana error codes -32005, -32007, -32014, -32015, -32016) with exponential backoff before failing over to the next endpoint. 429 responses with a Retry-After header are respected.
gRPC, Jetstream, and WebSocket connections use active ping/pong to detect dead connections. Configurable via the builder:
client, err := geyser.NewBuilder().
URL("http://ny.rpc.orbitflare.com:10000").
PingInterval(15 * time.Second). // send a ping every 15s (default: 10s)
MaxMissedPongs(5). // kill the connection after 5 missed pongs (default: 3)
Build()All three streaming clients reconnect automatically on disconnection. WebSocket also re-subscribes all active subscriptions after reconnecting.
Configure retry behavior with a RetryPolicy:
import (
"time"
"github.com/orbitflare/orbitflare-sdk-go/orbitflare"
)
client, err := rpc.NewBuilder().
URL("http://ny.rpc.orbitflare.com").
Retry(orbitflare.RetryPolicy{
InitialDelay: 200 * time.Millisecond,
MaxDelay: 15 * time.Second,
Multiplier: 2.0,
MaxAttempts: 5,
}).
Build()Every streaming client returns a value plus an error from Next(ctx). io.EOF signals a clean end (including after Close); any other non-nil error is fatal for that stream. Next also returns ctx.Err() if the passed context is cancelled.
for {
update, err := stream.Next(ctx)
if errors.Is(err, io.EOF) {
break // stream closed
}
if err != nil {
// fatal stream error
break
}
// handle update
}Errors are typed. Match them with errors.As:
var rpcErr *orbitflare.RPCError
if errors.As(err, &rpcErr) {
fmt.Println(rpcErr.Code, rpcErr.Message)
}Types: TransportError, RPCError, GRPCError, RateLimitedError, AuthError, StreamError, ConfigError, SerializationError, plus ErrTimeout. orbitflare.IsRetryable(err) reports whether an error is transient.
Set ORBITFLARE_LOG (trace|debug|info|warn|error|silent, default silent) for connection lifecycle logs, or pass your own *slog.Logger to a streaming builder with .Logger(...).
| Variable | Used by | Purpose |
|---|---|---|
ORBITFLARE_LICENSE_KEY |
RPC, WebSocket | API key appended to endpoint URLs |
ORBITFLARE_RPC_URL |
RPC | Default endpoint if URL() is not called |
ORBITFLARE_WS_URL |
WebSocket | Default endpoint if URL() is not called |
ORBITFLARE_GRPC_URL |
gRPC | Default endpoint if URL() is not called |
ORBITFLARE_JETSTREAM_URL |
Jetstream | Default endpoint if URL() is not called |
ORBITFLARE_LOG |
all | Log level for connection lifecycle events |
Runnable programs live in examples/: rpc, ws, geyser, jetstream, and jetstream_v2.
ORBITFLARE_LICENSE_KEY=ORBIT-... go run ./examples/rpc