A chain-agnostic, multi-provider blockchain client library for Go.
chainkit gives you a single interface to multiple blockchain data providers with automatic fallback, circuit breaking, rate limiting, and provider selection strategies — so your application keeps working when any individual provider is down or rate-limited.
Bitcoin is the first supported chain. The architecture is designed to add Ethereum and other chains without breaking existing code.
Want managed observability + remote configuration over your provider fleet? Drop in the
cloudagentsub-package and point it at chainkit cloud — live provider scoreboard, per-chain strategy switching from a dashboard, no-rebuild retry / breaker tuning. The SDK stays the same; the cloud is opt-in and never sits in your blockchain request path. See the chainkit cloud section below.
Eight network providers + one local primitive, all drop-in. Most are free or have a generous free tier; the fallback chain lets you mix free and paid so the cheap providers serve most traffic and the paid ones step in only when the free tier rate-limits or 5xx's.
Plus chainkit's local Metal primitives for key derivation, address generation/validation, and transaction assembly/signing/sizing — no network required for the crypto-only operations.
Full per-provider capability matrix is in the Bitcoin providers section below.
- Multi-provider fallback — register N providers per role; chainkit tries them in priority order until one succeeds
- Circuit breaker — opens automatically after repeated failures, re-checks on a configurable timeout
- Rate limiting — per-chain request rate limiting with burst support
- Retry with backoff — exponential backoff with optional jitter
- Provider selection strategies — priority-only, round-robin, random, least-loaded
- Metrics hook — inject your own
MetricsRecorder(Prometheus, Datadog, etc.) - Health checks — optional periodic health probing per provider chain
- Zero required roles — register only the capabilities you use; unregistered roles return
ErrProviderNotConfigured
The SDK is fully usable on its own — your blockchain RPC calls go direct to providers, and chainkit is never in the request path.
When you want observability + remote configuration over your provider fleet without standing up Prometheus + Grafana + a config service yourself, chainkit cloud is the official hosted complement:
- Live provider scoreboard — per-operation latency p50/p95/p99, success rate, rate-limit hits, error class breakdown, recent failures.
- Per-chain selection strategy at runtime — flip a chain from
priority→round-robin→weightedfrom a dashboard; the SDK picks the change up within ~30 s, no redeploy. - Retry + circuit-breaker + scoring-weight tuning from the dashboard — change behaviour in production without a rebuild.
- Cloud is observability + config only — it never proxies blockchain RPC. If chainkit cloud goes dark, your customer-facing calls keep working. The SDK buffers telemetry locally (drop-oldest on overflow) until the cloud is reachable again.
Add it to an existing chainkit setup with two extra calls:
import "github.com/exapsy/chainkit/cloudagent"
opts := cloudagent.Options{
Endpoint: "https://api.chainkit.dev",
APIKey: "ck_live_...",
}
// 1. Pipe per-request + scoring telemetry to the cloud.
rec := cloudagent.NewMetricsRecorder(opts)
// 2. Poll the cloud for per-chain config changes every ~30s and apply
// them via the SDK's ConfigUpdater interface — no client restart.
poller := cloudagent.NewConfigPoller(client, opts)
poller.Start(ctx)
defer poller.Stop()Pass rec via chainkit.WithMetricsRecorder when building your
MixedProviders. See cloudagent/doc.go for the
full surface. Sign up + pricing at https://chainkit.dev/pricing.
go get github.com/exapsy/chainkit@latest
Requires Go 1.22+.
import (
"github.com/exapsy/chainkit"
"github.com/exapsy/chainkit/bitcoin/providers"
"github.com/exapsy/chainkit/bitcoin/types"
)
metal := providers.NewMetal(providers.MetalProviderOptions{
Network: types.BitcoinNetworkMainnet,
})
mempool := providers.NewMempool(providers.MempoolOptions{
Network: types.BitcoinNetworkMainnet,
BaseURL: "https://mempool.space/api",
})
client, err := chainkit.NewMixedProvidersBuilder().
WithAddressGeneratorChain(chainkit.AddressGeneratorConfig{Generator: metal, Priority: 1}).
WithTxAssemblerChain(chainkit.TxAssemblerConfig{Assembler: metal, Priority: 1}).
WithTxSignerChain(chainkit.TxSignerConfig{Signer: metal, Priority: 1}).
WithTxSizerChain(chainkit.TxSizerConfig{Sizer: metal, Priority: 1}).
WithFeeEstimatorChain(chainkit.FeeEstimatorConfig{Estimator: metal, Priority: 1}).
WithFeeRecommenderChain(chainkit.FeeRecommenderConfig{Recommender: mempool, Priority: 1}).
WithBalanceFetcherChain(chainkit.BalanceFetcherConfig{Fetcher: mempool, Priority: 1}).
WithUTXOFetcherChain(chainkit.UTXOFetcherConfig{Fetcher: mempool, Priority: 1}).
WithRateFetcherChain(chainkit.RateFetcherConfig{Fetcher: mempool, Priority: 1}).
WithTxBroadcasterChain(chainkit.TxBroadcasterConfig{Broadcaster: mempool, Priority: 1}).
WithTxStatusFetcher(mempool).
Build()
if err != nil {
return err
}
// Get UTXOs
utxos, err := client.GetUTXOs(ctx, "bc1q...")
// Get balance
balance, err := client.GetBalance(ctx, "bc1q...", nil)
// Get fee recommendations
fees, err := client.GetTxFees(ctx)Register multiple providers for the same role. chainkit tries them in priority order (lower number = higher priority) and falls back automatically.
client, err := chainkit.NewMixedProvidersBuilder().
WithBalanceFetcherChain(
chainkit.BalanceFetcherConfig{Fetcher: mempool, Priority: 1},
chainkit.BalanceFetcherConfig{Fetcher: blockcypher, Priority: 2},
chainkit.BalanceFetcherConfig{Fetcher: blockstream, Priority: 3},
).
Build()Override circuit breaker, retry, rate limit, and selection strategy per role:
import "time"
chainCfg := chainkit.ChainConfig{
RetryPolicy: chainkit.RetryPolicy{
Enabled: true,
MaxAttempts: 3,
InitialDelay: 200 * time.Millisecond,
MaxDelay: 10 * time.Second,
BackoffMultiplier: 2.0,
Jitter: true,
},
CircuitBreaker: chainkit.CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 5,
SuccessThreshold: 2,
Timeout: 30 * time.Second,
},
SelectionStrategy: chainkit.SelectionStrategyRoundRobin,
Timeout: 15 * time.Second,
}
client, err := chainkit.NewMixedProvidersBuilder().
WithBalanceFetcherChain(
chainkit.BalanceFetcherConfig{
Fetcher: mempool,
Priority: 1,
ChainConfig: &chainCfg,
},
).
Build()| Strategy | Behaviour |
|---|---|
SelectionStrategyPriorityOnly |
Always try providers in priority order (default) |
SelectionStrategyRoundRobin |
Round-robin within same-priority providers |
SelectionStrategyRandom |
Random ordering within same-priority providers |
SelectionStrategyLeastLoaded |
Prefer providers with fewer recent failures |
Use ProviderSelector to pin all calls to a named provider (useful for debugging or A/B testing):
selector := chainkit.NewProviderSelector(client, "Mempool")
balance, err := selector.GetBalance(ctx, address, nil)Implement MetricsRecorder and pass it to the builder:
type prometheusRecorder struct{ ... }
func (r *prometheusRecorder) RecordBlockchainRequest(
ctx context.Context,
provider, operation string,
success bool,
duration time.Duration,
) {
// record to your metrics system
}
client, err := chainkit.NewMixedProvidersBuilder().
WithMetricsRecorder(&prometheusRecorder{}).
// ...
Build()The default is NoOpMetricsRecorder which discards all metrics.
All nine providers wired into the SDK today. Auth column is what chainkit needs to construct the provider; chains lists the BTC networks each backend serves; capabilities is the canonical set of operations the provider implements.
| Provider | Auth | Chains | Capabilities |
|---|---|---|---|
| Metal (chainkit-internal) | none, local | mainnet · testnet3 · testnet4 | address generation · address validation · tx assembly · tx signing · tx sizing · fee estimation |
| mempool.space | none | mainnet · testnet3 | balance · UTXOs · tx status · broadcast · fee tiers · BTC/USDT rate |
| Blockstream Esplora | Enterprise OAuth (ClientID + ClientSecret) | mainnet · testnet3 · testnet4 | balance · UTXOs · tx status · broadcast · fee tiers · address validation |
| BlockCypher | API key | mainnet · testnet3 | balance · UTXOs · tx status · broadcast · address validation · api-key validation |
| Tatum | API key | mainnet · testnet3 | tx status · broadcast · fee tiers · api-key validation |
| Blockchain.com | none | mainnet | balance · broadcast |
| Bitref.com | API key | mainnet | balance · broadcast · fee tiers · fee estimation |
| CoinGecko | optional API key | mainnet (rate only) | BTC/USD rate · api-key validation |
| Binance | none | mainnet (rate only) | BTC/USDT rate |
import (
"github.com/exapsy/chainkit/bitcoin/providers"
"github.com/exapsy/chainkit/bitcoin/types"
)
// Metal — local crypto primitives; no network
metal := providers.NewMetal(providers.MetalProviderOptions{
Network: types.BitcoinNetworkMainnet,
})
// mempool.space — public API, no auth
mempool := providers.NewMempool(providers.MempoolOptions{
Network: types.BitcoinNetworkMainnet,
BaseURL: "https://mempool.space/api",
})
// Blockstream Esplora — Enterprise OAuth required
blockstream, err := providers.NewBlockstream(providers.BlockstreamOptions{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
BaseURL: "https://enterprise.blockstream.com",
LoginURL: "https://login.blockstream.com",
})
// BlockCypher — API key required
blockcypher := providers.NewBlockcypher(providers.BlockcypherProviderOptions{
Network: types.BitcoinNetworkMainnet,
APIKey: "your-token",
})
// Tatum — API key required (positional API; pending migration to options)
tatum := providers.NewTatum(
types.BitcoinNetworkMainnet,
"https://api.tatum.io/v3",
"your-api-key",
)
// Blockchain.com — no auth
blockchain := providers.NewBlockchainCom()
// Bitref.com — API key required
bitref := providers.NewBitrefcom(providers.BitrefcomOptions{
APIKey: "your-api-key",
BaseURL: "https://bitref.com/api",
})
// CoinGecko — public API; pass an API key for higher rate limits
coingecko := providers.NewCoingecko(providers.CoingeckoOptions{})
// Binance — no auth (public spot endpoints)
binance := providers.NewBinance()import "github.com/exapsy/chainkit/payment"
// Build a BIP-21 bitcoin: payment URI
link := payment.BuildPaymentLink(payment.BuildPaymentLinkOptions{
Address: "bc1q...",
Amount: 1500000, // satoshis
Coin: payment.CoinBTC,
})
// "bitcoin:bc1q...?amount=0.015"
// Convert satoshis to BTC
btc := payment.ConvertSatoshisToBitcoin(1500000) // 0.015import "github.com/exapsy/chainkit/bitcoin"
// Derive BIP32 index and child-index from an arbitrary string key
index, childIndex, err := bitcoin.DeriveHDIndices("some-session-id42")When no provider is registered for a role:
balance, err := client.GetBalance(ctx, address, nil)
if errors.Is(err, chainkit.ErrProviderNotConfigured) {
// register a BalanceFetcher with WithBalanceFetcherChain
}chainkit/
├── interfaces.go # Core provider interfaces
├── config.go # RetryPolicy, CircuitBreakerConfig, ChainConfig, etc.
├── builder.go # MixedProvidersBuilder
├── providers.go # MixedProviders composite implementation
├── provider_selector.go # ProviderSelector — pins calls to a named provider
├── manager.go # ProviderManager — circuit breaker, retry, rate limit
├── metrics.go # MetricsRecorder interface + NoOpMetricsRecorder
├── provider_types.go # ProviderChainType, SelectionStrategy constants
├── selection_strategy.go # Priority, round-robin, random, least-loaded selectors
├── debug.go # DebugInfo, ExtractDebugInfo
│
├── bitcoin/
│ ├── types/types.go # UTXO, Tx, SignedTx, FeeTier, CoinRate, etc.
│ ├── providers/ # Bitcoin provider implementations
│ ├── derive.go # DeriveHDIndices (BIP32)
│ ├── address.go # ValidatePublicAddress
│ └── keys.go # GenerateKeys (secp256k1)
│
└── payment/
└── bitcoin.go # BuildPaymentLink, ConvertSatoshisToBitcoin
v0.x.y— unstable; interfaces may change between minorsv1.0.0— stable API, once the interface is confirmed in production
MIT