This document explains the main decisions behind the client and what it deliberately leaves out. For usage, see the README.
The client was extracted from the HTTP layer of
glassnode-cli and aligned with the
features of the TypeScript client,
glassnode-api-ts-client.
- Standard library only. The client is a single package with no third-party dependencies. It is easy to vendor and audit, and it never forces a version of another library on its users.
- Explicit configuration. Everything is passed to
NewClient. The package never reads environment variables, config files or global state, so two clients in one process never affect each other. - Immutable, shareable client. Options are validated once in
NewClient. After that theClienthas no mutable state except a lock-protected record of the last token replacement, so it can be shared between goroutines. - Typed where the API is stable, open where it is not. Common response
shapes have typed methods. Everything else goes through
GetMetric,GetorRaw, so new metrics and endpoints work without a client release.
MetricParams names the common query parameters (a, e, s, u, i, c)
and uses time.Time for the time range. Less common parameters go in Extra,
a url.Values, because the API adds metric-specific parameters regularly.
Setting the same parameter in both places is rejected rather than resolved
silently.
Lists are sent as repeated parameters (a=BTC&a=ETH), which is the form the
API expects for bulk requests. The client copies every map and slice it
receives, so callers can reuse their parameters.
Paths are validated against a strict pattern. A path cannot contain a scheme,
host, query string or .., so it can never point a request, and the
credentials attached to it, at a different server.
- Nullable values are pointers. The API returns
nullfor missing data. A*float64keeps that distinct from a real zero. - Timestamps stay as Unix seconds (
int64), as the API returns them. The client does not convert them totime.Time, so no time zone or precision choice is hidden in it. - Numbers in untyped JSON are
json.Number. Decoding intoanywould otherwise turn large integers into imprecisefloat64values. - Unknown fields are ignored, so new fields in API responses do not break
existing code. Required fields such as
tandvare checked, and a missing one produces aDecodeErrorinstead of a silent zero. - CSV is passed through, not parsed.
GetMetricCSVcopies the API's CSV to a writer as it arrives; the client adds no columns and no parsing, so large downloads need no memory and the file is exactly what the API sent. - Bulk responses are unwrapped.
GetBulkMetricreturns the contents of thedataenvelope directly. Each entry keeps all of its selectors inParams, because they depend on the metric (e,network,category, ...) and are the only way to tell entries apart. Bulk requests withoutSinceare rejected before sending, since the API requires it.
API keys are sent in the X-Api-Key header by default. Query authentication
(api_key=...) is still supported, but it puts the key in URLs, which end up
in proxy and access logs.
OAuth access tokens can be supplied as a fixed token or through a
TokenSource. The token source is called before every HTTP attempt, so a
refreshed token is picked up on retries. A source that also implements
TokenRefresher is asked for a new token once when the API answers 401, and
the request is repeated with it; this covers a token revoked before its
expiry without the client knowing how tokens are obtained. The client does not
perform the OAuth login or store tokens: how a session is obtained and kept is
specific to each application. glassnode-cli, for example, keeps its own login
and refresh logic and exposes it through a TokenRefresher.
Exactly one authentication method must be configured. Accepting several and picking one would hide configuration mistakes.
Only GET requests exist, so every request is idempotent and safe to retry.
Retries happen after network errors, 429 and 5xx responses, with two
retries by default.
The delay uses exponential backoff with full jitter: a random duration between
zero and an upper bound that starts at BaseDelay and doubles with each retry,
up to MaxDelay. Jitter keeps many clients
from retrying at the same moment after an outage.
The default cap is 65 seconds because the API's rate limit works in one-minute windows: a 429 at the start of a window asks for a wait of up to 60 seconds, and a 30-second cap would have returned the error without retrying at all.
Retry-After (seconds or an HTTP date) is treated as a minimum delay. An
integer above one year in either header is read as a Unix timestamp, so a
change of format on the server would not silently turn every 429 into an
immediate failure. The API
reports rate limits per minute through x-rate-limit-reset, so a 429 without
Retry-After uses that header instead; retrying a rate-limited request after
a second would only fail again. If the delay exceeds MaxDelay, the client
returns the error rather than retrying early or blocking for longer than the
caller configured. The delay is exposed as APIError.RetryAfter.
The response body is read completely inside the retry loop and decoded once,
after it, from the first successful attempt. Decoding inside the loop would
let a failed attempt leave values in the caller's destination: encoding/json
merges into existing maps and keeps struct fields that the input lacks.
stripe-go and go-github buffer for the same reason, and aws-sdk-go-v2
deserialises every attempt into a fresh output value.
The per-attempt timeout comes from the HTTP client. The total duration of a call is controlled by the context, which also interrupts the waits between retries.
- No redirects. Redirects are disabled even on a user-supplied
http.Client, because a redirect would resend the credentials to whatever host it points at. A3xxresponse is returned as anAPIError. - Redacted errors. Error messages and
APIError.Detailhave the API key and every token used in the call removed, including tokens from earlier retry attempts. The original errors are kept forerrors.Isanderrors.As, so callers should not log unwrapped causes. - Validated credentials. Credentials with control characters or surrounding whitespace are rejected before any request, so they cannot inject headers.
- OAuth login flows and token storage.
- x402 payments, which the TypeScript client supports.
- Response size limits by default. Public API clients such as go-github,
stripe-go and slack-go do not impose one, because the server decides how
much a request returns.
WithMaxResponseBytesis available for callers that want a hard bound. - Response caching and client-side rate limiting. Applications that need them
can add them in a custom
http.RoundTripperor around the client. - Reading configuration from the environment or files.
| TypeScript client | Go client |
|---|---|
| Asset metadata, metric list | ListAssets, ListMetrics |
| Metric metadata, lag statistics | GetMetricMetadata, GetMetricStats |
| Generic metric with a Zod schema | GetMetric with a Go type and, optionally, UnmarshalJSON |
| Scalar and object series | GetTimeSeries, GetObjectTimeSeries |
| Bulk metrics | GetBulkMetric |
| Metadata tags, categories, blockchains | ListMetricTags, ListAssetTags, ListAssetCategories, ListAssetBlockchains |
Custom fetch, observability hooks |
WithHTTPClient with a custom http.RoundTripper |
AbortSignal, per-call timeout |
context.Context |
| Error classes | Error types for errors.As |
Retries with jitter and Retry-After |
RetryPolicy |
| OAuth token refresh | WithTokenSource |
| x402 payments | Not supported |
GetAPIUsage, ListExchanges, ListNetworks, ListMiners and the generic
ListNames have no TypeScript counterpart.
The module root is the library. Releases are Git tags such as v0.1.0.
examples/ is a separate Go module with its own go.mod. Go leaves nested
modules out of the module archive, so users who install the client do not
download the examples. The examples module uses a relative replace directive
to build against the local checkout. CI lints and builds it separately.