DEV Community

Cover image for bitquery-go: A Production-Minded Go SDK for Bitquery GraphQL
Igor
Igor

Posted on

bitquery-go: A Production-Minded Go SDK for Bitquery GraphQL

Blockchain data services rarely fail because sending a GraphQL request is difficult. They fail at the seams: a token appears in a log, an overloaded endpoint receives an avoidable retry storm, a large on-chain amount is decoded as an imprecise floating-point number, or a reconnecting stream is treated as exactly-once delivery. These are not glamorous problems, but they are the problems that determine whether an integration survives contact with production.

bitquery-go is an MIT-licensed Go SDK for Bitquery GraphQL that takes those seams seriously. Rather than wrapping every possible field in a sprawling generated surface, it combines a small set of practical helpers with a direct, inspectable GraphQL operation API. More importantly, it makes the meaningful boundaries visible: Bitquery V1 and V2 are separate clients, HTTP queries and WebSocket subscriptions are separate workflows, and GraphQL-level errors are not confused with transport failures. 1

This is a library for Go teams building indexers, analytics backends, monitoring services, trading-data pipelines, or internal blockchain-data tools. It does not promise to abstract away the Bitquery schema. It gives developers a clear way to work with that schema while adding the safeguards that should surround an API client in a real service.

Core design choice: V1 and V2 remain distinct contracts. The SDK does not rewrite a GraphQL document, switch endpoints, or silently fall back from one version to the other. 1

Start with the correct API contract

The first useful feature of bitquery-go is also the least flashy: it refuses to pretend that the two Bitquery APIs are interchangeable. The package exposes a v1.Client for the historical HTTPS GraphQL API and a v2.Client for the streaming GraphQL API over HTTPS. Live V2 updates use a separate subscription.Client over WebSocket. That separation matters because the schemas, endpoint families, coverage, and migration expectations differ. 2

For a new integration, V2 is the natural place to begin when the required EVM or Solana data is available. A legacy V1 document remains a valid reason to use V1, but migration should be checked query by query rather than assumed. The SDK helps rather than obstructs this decision: it can surface typed deprecation notices for V1 calls on documented deprecated networks, without blocking a deliberate legacy request. 1

The result is a more honest architecture. A codebase can say exactly which API contract it depends on, instead of hiding an important data decision behind a generic “client.” That makes reviews, upgrades, and incident response easier.

Install once, then keep GraphQL visible

The package requires Go 1.21 or newer and installs in the usual way. Client creation is local; no network request occurs until an operation is executed. 1

go get github.com/tigusigalpa/bitquery-go
Enter fullscreen mode Exit fullscreen mode

A V2 query can remain idiomatic Go without turning GraphQL into string concatenation. The Operation keeps the operation name, query, and variables in separate fields. Values belong in Variables, where they can be encoded safely and inspected independently from the document.

package main

import (
    "context"
    "os"

    bitquery "github.com/tigusigalpa/bitquery-go"
    "github.com/tigusigalpa/bitquery-go/v2"
)

func latestBlocks(ctx context.Context) error {
    provider := bitquery.NewStaticTokenProvider(os.Getenv("BITQUERY_TOKEN"))

    client, err := v2.New(provider, bitquery.WithRegion(bitquery.RegionUS))
    if err != nil {
        return err
    }

    response, err := client.Execute(ctx, bitquery.Operation{
        OperationName: "LatestBlocks",
        Query: `query LatestBlocks($network: evm_network!) {
            EVM(network: $network) {
                Blocks(limit: {count: 3}) { Block { Number Time } }
            }
        }`,
        Variables: map[string]any{"network": "eth"},
    })
    if err != nil {
        return err
    }

    return response.DecodeData(&map[string]any{})
}
Enter fullscreen mode Exit fullscreen mode

This approach is a strong fit for an evolving GraphQL API. The SDK offers thin helpers for common operations, including selected EVM DEX trades, transfers, and transactions, plus selected Solana operations. However, raw Execute remains the primary compatibility escape hatch when a team needs a newer cube or field. Instead of waiting for an SDK release to expose every schema addition, a developer can use the current Bitquery schema and keep the operation under version control. 1

Treat authentication and endpoints as configuration, not application logic

The library supports two safe token paths: a pre-minted access token via NewStaticTokenProvider, or an OAuth client-credentials provider. The latter caches the token and coalesces concurrent refresh work, which is the behavior a multi-goroutine service needs when a token is nearing expiry. 1

Endpoint selection is similarly explicit. Europe is the default region, while Asia and US can be chosen through options. A service can override the HTTP base URL or WebSocket URL for a private proxy or test server, with validation for absolute HTTP(S) and WS(S) URLs. The client will not accept a credential-bearing custom endpoint. 1

That final detail is worth noticing. For WebSocket authentication, Bitquery requires the OAuth token in the URL query string. bitquery-go adds that query parameter internally, redacts it from errors and logger output, and tells callers not to build it into a custom endpoint themselves. This is the type of defensive default that prevents a momentary debugging shortcut from becoming a credential leak. 1

Make errors useful without throwing away GraphQL data

GraphQL changes the usual “non-200 equals failure” mental model. An HTTP 200 response can contain errors[], and it can still contain usable partial data. bitquery-go preserves that distinction.

By default, Execute returns the response even when GraphQL errors are present, allowing application code to inspect response.Errors and decide whether partial data is acceptable. A strict mode is available when the application wants any GraphQL error returned as a typed KindGraphQL error. Crucially, strict-mode errors retain the full response, so partial data is still available for an explicit business decision. 1

Transport and API failures also arrive through *bitquery.Error, enabling code to distinguish authentication, authorization, plan-entitlement, rate-limit, server, GraphQL, subscription, and configuration failures. That is much more actionable than branching on error-message text.

var apiErr *bitquery.Error
if errors.As(err, &apiErr) {
    switch apiErr.Kind {
    case bitquery.KindRateLimited:
        // Respect apiErr.RetryAfter and reduce pressure if needed.
    case bitquery.KindPlanEntitlement:
        // The request is outside the plan; retrying will not help.
    case bitquery.KindAuthentication:
        // Refresh or replace the credentials used by the provider.
    case bitquery.KindServer:
        // Temporary failure: apply service-level observability and policy.
    }
}
Enter fullscreen mode Exit fullscreen mode

The default retry policy is deliberately conservative: up to four attempts with jittered exponential backoff, capped at 60 seconds, with Retry-After taking precedence when the service sends it. It retries transient transport conditions, selected 5xx responses, rate limits, and documented shared-compute blocks. It does not automatically replay mutations or HTTP subscriptions, because repeating those operations may be unsafe for the calling application. 1

This policy is a sensible baseline rather than a hidden “reliability” switch. A workload can disable retries, add a rate limiter, set a timeout, and still use its own worker limits. In particular, the SDK does not fan out expensive queries or invent concurrency on the caller’s behalf. That makes capacity ownership clear.

Preserve numeric precision where blockchain data needs it

On-chain values are a poor match for casual float64 handling. Token amounts, decimals, identifiers, and block heights may exceed the range in which a floating-point representation is exact. bitquery-go deliberately leaves response data as json.RawMessage and uses json.Number when decoding into interface values, so the application can decide whether a field belongs in math/big, a decimal package, or a domain-specific type. 1

This is an understated but important design decision. It avoids a library-level conversion that looks convenient in a demo and quietly corrupts large values in a financial or reconciliation workflow. The caller has a small additional responsibility, but it is the correct responsibility: choose the numeric representation that matches the business meaning of the field.

Build subscriptions as durable workers, not request handlers

The subscription package is equally pragmatic. Live V2 GraphQL subscriptions are exposed by subscription.Client, not bolted onto the ordinary HTTP client. That design communicates the operational reality: a subscription is a long-lived worker with cancellation, reconnection, queueing, and downstream idempotency concerns. 1

A subscription client supports both graphql-transport-ws and the legacy graphql-ws protocol. It handles the protocol lifecycle, keepalives, bounded reconnects, and clean shutdown. Its event queue is bounded so a slow consumer cannot grow memory without limit. Applications can choose drop_oldest, which keeps recent events and exposes a dropped-event count, or fail, which stops the stream with a typed error rather than losing an event silently. 1

workerCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

client, err := subscription.New(provider,
    bitquery.WithSubProtocol(bitquery.SubProtocolGraphQLTransportWS),
    bitquery.WithSubscriptionQueue(500, bitquery.OverflowDropOldest),
)
if err != nil {
    return err
}

stream, err := client.Subscribe(workerCtx, bitquery.Operation{
    Query: `subscription {
        EVM(network: eth) { Blocks { Block { Number Time } } }
    }`,
})
if err != nil {
    return err
}
defer stream.Close()

for event := range stream.Events {
    if event.Type == subscription.EventData {
        // Persist a stable event key before applying the payload.
    }
}
if err := stream.Err(); err != nil {
    return err
}
Enter fullscreen mode Exit fullscreen mode

The final comment is not ceremonial. Realtime messages should be treated as at-least-once, and portions may be unordered. A production consumer should persist a stable, domain-appropriate deduplication key and backfill a missed interval with an HTTP query after a longer outage. The SDK provides the lifecycle machinery, but it does not make an impossible exactly-once guarantee on behalf of a downstream database or business process. 2

Where bitquery-go fits best

bitquery-go is compelling when a Go team wants a dependable client boundary without surrendering control of its GraphQL documents. It suits services that need a straightforward HTTP query path today and may later add live updates. It also suits teams maintaining a V1 integration that need to recognize its legacy status without breaking it through an automatic migration.

It is intentionally not a generated, exhaustive Go model of every Bitquery cube and field. That is a feature for projects where the live schema evolves faster than a typed SDK can be regenerated and reviewed. Use the Bitquery IDE and current schema as the authority for data availability; keep important operations in the application; use this package for authentication, endpoint management, execution, error handling, rate limits, cancellation, and subscriptions. 1

The repository also includes runnable examples for HTTP, OAuth client credentials, subscriptions, V1 historical queries, V2 Solana transfers, and custom endpoints. Examples compile without credentials and only make a live request when the required environment variables are supplied. That makes them practical starting points for an internal spike or a production integration review. 1

A small SDK with the right boundaries

The most valuable API libraries are not those that hide every underlying detail. They are the ones that hide repetitive plumbing while making consequential behavior explicit. bitquery-go does that well: it keeps V1 and V2 honest, keeps GraphQL operations inspectable, protects sensitive token material in diagnostics, treats partial responses correctly, preserves numeric precision, and gives streaming workloads the controls they actually need.

If you are building Bitquery-backed software in Go, start with the README, run the example closest to your workload, and make the version and delivery semantics part of your design from day one. You will spend less time untangling invisible client behavior later—and more time building the blockchain-data product your users actually need. 1

References

Top comments (0)