Non-custodial Bitcoin payments for Go — plus the multi-provider RPC layer that powers them.
chainkit is two things in one MIT-licensed module:
- Payments (
chainkit/payment) — the execution layer for chainkit cloud, a non-custodial Bitcoin payment processor. Fiat-priced invoices settle straight to the wallet behind your own xpub; chainkit never holds keys or coins. The SDK verifies HMAC-signed webhooks, builds BIP21 payment links, and talks to the cloud API. - RPC routing (the root package) — a chain-agnostic, multi-provider blockchain client: one interface over N data providers with automatic fallback, circuit breaking, rate limiting, and per-call provider selection. No cloud account required; it runs entirely in your process.
Bitcoin is the first supported chain. The architecture is designed to add Ethereum and other chains without breaking existing code.
Links: Docs · Quickstart · Pricing · AI/LLM overview (llms.txt)
import "github.com/exapsy/chainkit/payment"
// On your webhook endpoint — verify the signature before trusting it.
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Chainkit-Signature")
if err := payment.VerifyWebhook(body, sig, os.Getenv("CHAINKIT_WEBHOOK_SECRET")); err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// settled ✓ — the sats are already in your wallet.
}Invoices are issued against the cloud API (payment.NewCloudClient) or any HTTP client — see the payments API docs. Sign up at chainkit.dev; funds settle on-chain to you, so there's nothing to withdraw and nothing we can freeze.
- 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
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(types.BitcoinNetworkMainnet)
mempool, err := providers.NewMempool(types.BitcoinNetworkMainnet, "https://mempool.space/api")
if err != nil {
return err
}
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.
| Provider | Capabilities |
|---|---|
metal |
AddressGenerator, FeeEstimator, TxAssembler, TxSigner, TxSizer, AddressValidator — local only, no network |
mempool |
FeeRecommender, BalanceFetcher, UTXOFetcher, TxBroadcaster, RateFetcher, TxStatusFetcher |
blockcypher |
BalanceFetcher, UTXOFetcher |
blockstream |
UTXOFetcher, BalanceFetcher, AddressValidator, FeeRecommender |
bitrefcom |
TxBroadcaster, FeeRecommender, BalanceFetcher |
coingecko |
RateFetcher |
import "github.com/exapsy/chainkit/bitcoin/providers"
// metal — local key derivation, no network calls
metal := providers.NewMetal(types.BitcoinNetworkMainnet)
// mempool — mempool.space API
mempool := providers.NewMempool(types.BitcoinNetworkMainnet, "https://mempool.space/api")
// blockcypher — requires API token
blockcypher := providers.NewBlockcypher(types.BitcoinNetworkMainnet, "your-token")
// blockstream — public API, no key required
blockstream, err := providers.NewBlockstream(types.BitcoinNetworkMainnet)
// bitrefcom — requires API key
bitrefcom := providers.NewBitrefcom("your-api-key", "https://bitref.com/api")
// coingecko — public API
coingecko := providers.NewCoingecko()import (
"math/big"
"github.com/exapsy/chainkit/payment"
)
// Build a BIP-21 bitcoin: payment URI
link, err := payment.BuildPaymentLink(payment.BuildPaymentLinkOptions{
WalletAddress: "bc1q...",
Amount: big.NewInt(1500000), // satoshis
Coin: payment.BTC,
})
// link == "bitcoin:bc1q...?amount=0.015"
// Convert satoshis to BTC
btc, err := payment.ConvertSatoshisToBitcoin(big.NewInt(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