-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Practical, copyable patterns for github.com/cryptohopper/cryptohopper-go-sdk. Each snippet is self-contained — drop into a *.go file and go run it. They use only the public SDK surface, never internals.
The SDK is synchronous: every resource method takes a context.Context and returns (result, error). Cancellation, deadlines, and concurrency are your call to wire up — examples below show the idiomatic pattern for each.
- Cancel an in-flight request from another goroutine
- Per-call deadlines with context.WithTimeout
- Wait for a backtest to finish
- Find every open position across all your hoppers
- Fan out with errgroup
- Detect new fills since the last poll
- Inspect typed errors with errors.As
- Fail fast on auth errors, retry on transient ones
- Bring your own http.Client (proxies, instrumentation)
- Disable the SDK's built-in retry and handle 429 yourself
- Tighten timeouts for short-lived workers
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
<-time.After(2 * time.Second)
cancel() // every in-flight SDK call using ctx will return immediately
}()
_, err := ch.Hoppers.List(ctx, nil)
if errors.Is(err, context.Canceled) {
log.Println("cancelled cleanly")
}The SDK propagates context cancellation to the underlying http.Client.Do, so cancellation is immediate — no goroutine leaks.
The SDK's WithTimeout(d) option sets a default for every request. For a one-off shorter deadline (e.g. a health-check endpoint), wrap the context per call.
ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel()
ticker, err := ch.Exchange.Ticker(ctx, "binance", "BTC/USDT")
if errors.Is(err, context.DeadlineExceeded) {
log.Println("ticker timed out — back off and retry later")
}context.DeadlineExceeded propagates as-is; the SDK does not wrap it as a *cryptohopper.Error. Compare with errors.Is, not by string.
Backtests are async server-side. Create returns immediately; you poll Get until status is terminal.
func RunBacktest(ctx context.Context, ch *cryptohopper.Client, hopperID any, from, to string) (map[string]any, error) {
bt, err := ch.Backtest.Create(ctx, map[string]any{
"hopper_id": hopperID,
"start_date": from,
"end_date": to,
})
if err != nil {
return nil, err
}
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
id := bt["id"]
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
cur, err := ch.Backtest.Get(ctx, id)
if err != nil {
return nil, err
}
switch cur["status"] {
case "completed", "failed":
return cur, nil
}
}
}
}Backtests have their own rate bucket (1 request per 2 seconds). 5-second polling stays well clear.
Sequential — one request per hopper. Simple, no concurrency.
hoppers, err := ch.Hoppers.List(ctx, nil)
if err != nil {
return err
}
for _, h := range hoppers {
positions, err := ch.Hoppers.Positions(ctx, h["id"])
if err != nil {
return fmt.Errorf("hopper %v: %w", h["id"], err)
}
for _, p := range positions {
fmt.Printf("%s (#%v): %v %v @ %v\n", h["name"], h["id"], p["amount"], p["coin"], p["rate"])
}
}For >50 hoppers, parallelise with errgroup (next recipe).
import "golang.org/x/sync/errgroup"
g, gctx := errgroup.WithContext(ctx)
g.SetLimit(10) // bounded concurrency
hoppers, err := ch.Hoppers.List(ctx, nil)
if err != nil {
return err
}
results := make([][]map[string]any, len(hoppers))
for i, h := range hoppers {
i, h := i, h
g.Go(func() error {
ps, err := ch.Hoppers.Positions(gctx, h["id"])
if err != nil {
return err
}
results[i] = ps
return nil
})
}
if err := g.Wait(); err != nil {
return err
}errgroup cancels every other goroutine the moment one returns an error, so you don't waste API calls after a failure. The SDK retries 429s transparently — but if you fan out 100+ requests at once you'll still hit the bucket faster than retries can absorb. Cap concurrency with SetLimit.
seen := map[any]struct{}{}
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(10 * time.Second):
orders, err := ch.Hoppers.Orders(ctx, hopperID, nil)
if err != nil {
log.Printf("poll error: %v", err)
continue
}
for _, o := range orders {
id := o["id"]
if _, ok := seen[id]; ok {
continue
}
if o["status"] == "filled" {
seen[id] = struct{}{}
fmt.Printf("Fill: %v %v %v @ %v\n", o["market"], o["type"], o["amount"], o["price"])
}
}
}
}For production-grade fill notifications, configure the webhooks resource — push beats poll for event delivery.
Every non-2xx response and every transport failure is a *cryptohopper.Error. Extract it with errors.As, never type-assert directly.
import (
"errors"
cryptohopper "github.com/cryptohopper/cryptohopper-go-sdk"
)
_, err := ch.Hoppers.Get(ctx, "999999999")
var ce *cryptohopper.Error
if errors.As(err, &ce) {
log.Printf("code=%s status=%d server=%d ip=%s retry_after=%v",
ce.Code, ce.Status, ce.ServerCode, ce.IPAddress, ce.RetryAfter)
if ce.Code == "NOT_FOUND" {
return ErrHopperGone
}
}ce.Code is a stable string — compare with ==, never substring-match.
The SDK auto-retries 429s. For 5xx and network errors you may want a tighter retry. Auth errors should never be retried.
func WithRetry[T any](ctx context.Context, fn func() (T, error), maxAttempts int) (T, error) {
var zero T
for attempt := 0; attempt < maxAttempts; attempt++ {
v, err := fn()
if err == nil {
return v, nil
}
var ce *cryptohopper.Error
if errors.As(err, &ce) {
switch ce.Code {
case "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "VALIDATION_ERROR":
return zero, err // never retry
}
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return zero, err
}
if attempt < maxAttempts-1 {
select {
case <-ctx.Done():
return zero, ctx.Err()
case <-time.After(500 * time.Millisecond * (1 << attempt)):
}
}
}
return zero, fmt.Errorf("retry budget exhausted")
}hc := &http.Client{
Timeout: 0, // let the SDK manage timeouts via context
Transport: &loggingRoundTripper{rt: http.DefaultTransport},
}
ch, err := cryptohopper.NewClient(
os.Getenv("CRYPTOHOPPER_TOKEN"),
cryptohopper.WithHTTPClient(hc),
)
type loggingRoundTripper struct{ rt http.RoundTripper }
func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := l.rt.RoundTrip(req)
log.Printf("%s %s -> %d (%s)", req.Method, req.URL.Path, resp.StatusCode, time.Since(start))
return resp, err
}For OpenTelemetry, use otelhttp.NewTransport(http.DefaultTransport) and pass that as the wrapped transport. For HTTP/HTTPS proxies, set Transport.Proxy = http.ProxyFromEnvironment (default on http.DefaultTransport).
ch, _ := cryptohopper.NewClient(
os.Getenv("CRYPTOHOPPER_TOKEN"),
cryptohopper.WithMaxRetries(0),
)
_, err := ch.Hoppers.List(ctx, nil)
var ce *cryptohopper.Error
if errors.As(err, &ce) && ce.Code == "RATE_LIMITED" {
log.Printf("rate limited; server says wait %v", ce.RetryAfter)
// your custom queue / circuit breaker / etc.
}Useful when you have your own queue, want exact backoff control, or are running inside something that already does retries (a job runner, a workflow engine).
In an AWS Lambda (15s) or other short-lived worker, the default 30-second SDK timeout outlives the invocation, leading to confusing "function killed" errors instead of clean SDK timeouts.
ch, _ := cryptohopper.NewClient(
os.Getenv("CRYPTOHOPPER_TOKEN"),
cryptohopper.WithTimeout(8*time.Second), // ~half your function budget
cryptohopper.WithMaxRetries(1), // leave room for one retry
)A *cryptohopper.Error with Code == "TIMEOUT" is much easier to handle than a process kill.
Pages
Other SDKs
Resources