Working with on-chain intelligence is rarely difficult because of a single HTTP request. The hard part is everything that surrounds the request: modelling response payloads, handling deadlines, preserving decimal values, applying pagination correctly, reacting to rate limits, keeping an eye on API-credit consumption, and maintaining a separate real-time connection when the product needs live transfer data.
That operational layer is where projects tend to accumulate fragile helpers and repeated boilerplate. A handler builds a URL in one package, a retry loop appears in another, and a stream connection is managed somewhere else entirely. The result may work in development, but it becomes difficult to reason about under load, during a partial outage, or when the API evolves.
arkham-go is designed to remove that friction. It is a production-ready Go SDK for the Arkham Intel API, built around idiomatic Go structures and the standard library. The library covers the documented REST surface and WebSocket v2 transfer streams, giving Go teams a structured foundation for applications that use address, entity, transfer, market, portfolio, risk, and related on-chain intelligence. 1
“It handles the REST endpoints and WebSocket v2 streams for you, so you can focus on building with on-chain intelligence instead of wrestling with HTTP plumbing.” — the project README 1
Why an SDK matters here
The Arkham API guide highlights the integration concerns that appear in real applications: authentication, rate limits, credit pricing, pagination, and a data model spanning addresses, entities, labels, and tags. 2 Those are not incidental details. They affect how reliably an integration behaves and how easily its costs and failure modes can be understood.
arkham-go makes those concerns part of the client contract rather than leaving every application to implement them independently. It does not promise to decide what your product should do with on-chain data; it gives your Go code predictable, typed primitives for retrieving and operationally managing that data.
| Integration challenge | What arkham-go provides |
|---|---|
| Request and response decoding | Strongly typed Go structs for documented requests and responses. 1 |
| Cancellation and deadlines | A context.Context argument on every network method. 1
|
| Temporary API failures | Exponential backoff with jitter for 429 and 5xx responses, including support for Retry-After. 1
|
| Error classification | Sentinel errors such as ErrBadRequest, ErrUnauthorized, and ErrRateLimited, compatible with errors.Is and errors.As. 1
|
| Usage observability | Response metadata carrying HTTP details and X-Intel-Datapoints-* headers. 1
|
| Large result sets | Caller-controlled offset pagination with explicit limits for items and pages. 1 |
| Live transfer workflows | WebSocket v2 stream creation, connection, reception, reconnection, and deletion helpers. 1 |
This is a pragmatic design choice. An SDK becomes more valuable when the network is less predictable: a timeout must stop background work, a rate limit must be recognised as a distinct condition, and a delayed response must not turn into unbounded retry traffic. By making these behaviours visible in the API, arkham-go helps keep application-level code focused on business logic.
Get started with a small surface area
The project targets Go 1.21 or newer and requires an Arkham API key. Installation is a normal Go module command: 1
go get github.com/tigusigalpa/arkham-go
Keep the key in an environment variable rather than committing it to source control:
export ARKHAM_API_KEY="your-api-key"
A minimal client can be created either from the key directly or with NewClientFromEnv() when the conventional ARKHAM_API_KEY environment variable is present. The following example looks up intelligence for an address and reads the returned usage metadata.
package main
import (
"context"
"fmt"
"log"
"os"
arkham "github.com/tigusigalpa/arkham-go"
)
func main() {
client, err := arkham.NewClient(os.Getenv("ARKHAM_API_KEY"))
if err != nil {
log.Fatal(err)
}
address, meta, err := client.Intelligence.Address(
context.Background(),
"0x28C6c06298d514Db089934071355E5743bf21d60",
nil,
)
if err != nil {
log.Fatal(err)
}
if address.ArkhamEntity != nil {
fmt.Println("Entity:", address.ArkhamEntity.Name)
}
fmt.Printf("Usage: %s/%s datapoints\n", meta.IntelDatapointsUsage, meta.IntelDatapointsLimit)
}
The point is not simply that the example is short. It is that the return values make an important operational signal available right next to the decoded domain value. A service call returns *arkham.ResponseMetadata alongside its result, including status information, request duration, final URL, and the relevant datapoint headers. That makes it possible to add usage logging or alerting without manually parsing headers throughout the codebase. 1
Build with types, not scattered query strings
One of the most welcome qualities of Go is that its types make incorrect assumptions harder to hide. arkham-go carries that preference into its filter and service interfaces. For transfer queries, typed option structs are converted into query parameters by the SDK. Decimal fields such as a USD threshold are represented as strings, preserving the decimal form expected by the API. 1
filter := &arkham.TransferFilter{
Base: []string{"binance"},
Chains: []string{"ethereum", "bitcoin"},
Flow: arkham.FlowOut,
UsdGte: "100000",
SortKey: arkham.SortKeyTime,
SortDir: arkham.SortDirDesc,
Limit: 25,
TimeRange: &arkham.TimeRange{
TimeLast: "24h",
},
}
transfers, meta, err := client.Transfers.Transfers(ctx, filter)
if err != nil {
return err
}
fmt.Println("Datapoints remaining:", meta.IntelDatapointsRemaining)
This style has a useful maintenance benefit: intent is obvious at the call site. A reviewer can see the requested chains, flow direction, time range, and sorting policy without having to reconstruct a URL. The library also validates relevant options; for example, the documented transfer filter rules prevent mixing TimeLast with TimeGte or TimeLte. 1
The service surface is broad enough to keep related capabilities inside one client. According to the project documentation, the client exposes services for intelligence, balances, chains, counterparties, historical flows and balances, loans, market data, portfolio snapshots, risk, swaps, tokens, transfers, transactions, users, subscriptions, analytics, and WebSocket streams, among other areas. 1 Whether you are building a monitoring workflow, analytics pipeline, research tool, or internal operations dashboard, that unified structure reduces the need to invent a different integration pattern for every endpoint family.
Reliability should be the default
A thin API wrapper can send requests. A production-oriented SDK should also help callers deal with the response when it is not the one they wanted.
arkham-go accepts functional options for the base URL, timeout, retry count, retry delay, user agent, HTTP client, and logger. 1 This lets teams begin with a concise default client and move toward explicit operational controls as an integration grows.
client, err := arkham.NewClient(
apiKey,
arkham.WithTimeout(30*time.Second),
arkham.WithMaxRetries(3),
arkham.WithBaseDelay(500*time.Millisecond),
arkham.WithUserAgent("wallet-monitor/1.0"),
)
The retry policy is intentionally selective. The README documents automatic retries for 429 and 5xx responses on GET requests, with Retry-After honoured when supplied. Mutating requests are not retried unless the SDK knows they are safe to repeat. 1 That distinction respects a basic reliability principle: retrying should improve resilience without quietly multiplying side effects.
Errors are similarly designed for normal Go control flow. Instead of forcing callers to compare incidental error strings, the library offers sentinel errors and a structured APIError. The familiar errors.Is and errors.As pattern remains available.
_, _, err := client.Intelligence.Address(ctx, "0xabc", nil)
if err != nil {
if errors.Is(err, arkham.ErrRateLimited) {
// Queue the work, apply a policy, or notify your scheduler.
return err
}
var apiErr *arkham.APIError
if errors.As(err, &apiErr) {
log.Printf("API status=%d retry_after=%s", apiErr.StatusCode, apiErr.RetryAfter)
}
return err
}
The SDK also wraps transport failures and cancelled contexts in a way that preserves standard error inspection. 1 This matters because application-specific behaviour—such as telling a job runner to retry later or showing a clear error in an operator console—belongs in the application, but it depends on receiving dependable signals from the client.
Pagination without surprises
Offset pagination is easy to make look correct while silently requesting more data than a job can safely process. arkham-go exposes a Paginator for list endpoints that fetches one page at a time and lets the caller cap both the total item count and the number of page requests. It deliberately does not guess that a remote list is exhausted; callers stop when a decoded page is empty. 1
That is a refreshingly explicit contract. It makes resource limits visible in code and prevents hidden work from growing without bound.
pages := arkham.NewPaginator(
ctx,
client,
"/transfers",
query,
100, // page size
500, // maximum items requested; 0 is unlimited
10, // maximum requests; 0 is unlimited
)
for pages.HasNext() {
var page []arkham.Transfer
if _, err := pages.NextPage(&page); err != nil {
return err
}
if len(page) == 0 {
break
}
// Process the current page before asking for the next one.
}
For ETL-style tasks, backfills, and scheduled monitoring, this kind of control is more useful than a convenience abstraction that hides where the next API request will come from.
Real-time transfer streams, without a separate client
Some workflows cannot wait for the next polling interval. A monitoring service may need to react to qualifying transfers as they arrive. arkham-go includes WebSocket v2 stream management alongside its REST services: create a stream, connect to it, receive messages, reconnect if necessary, and delete the stream when the work is complete. 1
stream, _, err := client.Streams.Create(ctx, &arkham.CreateStreamV2Request{
Base: []string{"binance"},
UsdGte: "500000",
})
if err != nil {
return err
}
conn, err := client.Streams.Connect(ctx, stream.StreamID)
if err != nil {
return err
}
defer conn.Close()
defer client.Streams.Delete(ctx, stream.StreamID)
for {
message, err := conn.ReceiveTyped()
if err != nil {
return err
}
fmt.Println(message.Type, string(message.Payload))
}
The README documents reconnect support for interrupted connections within the API reactivation window, and it also notes that unused streams should be deleted. 1 Encapsulating this lifecycle within the same SDK is valuable: the real-time path remains consistent with the same authentication, error, context, and configuration conventions used by ordinary client calls.
A clean dependency story
arkham-go has no runtime dependencies outside the Go standard library. 1 For many teams, that is not a philosophical talking point; it is an operational advantage. Fewer dependencies can mean a smaller dependency review surface, simpler vendoring and builds, and less uncertainty when integrating the SDK into a service with strict deployment requirements.
The project is released under the MIT License, which makes it straightforward to evaluate and adopt in a wide range of codebases. 1 Its repository also includes runnable examples for intelligence, transfers, pagination, and WebSocket workflows, plus standard go test ./... and go vet ./... commands for contributors and evaluators. 1
Where to start
If your Go application needs Arkham API capabilities, begin by cloning the smallest possible integration: create a client, pass a context with a deadline, make one typed service call, and record the response metadata. From there, use typed filters as query needs grow, introduce bounded pagination for batch work, and move to a WebSocket v2 stream when the use case is genuinely real-time.
The value of arkham-go is not that it makes every integration decision for you. Its value is that it packages the repeatable, error-prone client work—request plumbing, typed models, retries, errors, pagination, usage metadata, and streams—into a Go-native interface. That leaves you with more time to build the part your users actually notice.
Explore the project, review the examples, and try it in your next Go service:
go get github.com/tigusigalpa/arkham-go
Repository: github.com/tigusigalpa/arkham-go
Top comments (0)