Docs / SDK guides
Go client
This is a complete client in one file, using only the standard library. It needs Go 1.21 or later. It is sample code to copy, because official packages are planned and do not exist today.
#The client
Save this as farbeam/farbeam.go in your module.
go
// Package farbeam is a small client for the Farbeam API. Sample code, not an official package.
package farbeam
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
)
const DefaultBase = "https://farbeamlabs.com/api/v1"
// Error is an API error response.
type Error struct {
Status int
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
}
func (e *Error) Error() string {
return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message)
}
// Client talks to the API.
type Client struct {
Key string
Base string
Tries int
HTTP *http.Client
}
// New returns a client with defaults. Pass an empty key for anonymous access.
func New(key string) *Client {
return &Client{Key: key, Base: DefaultBase, Tries: 5, HTTP: &http.Client{Timeout: 30 * time.Second}}
}
type envelope struct {
Data json.RawMessage `json:"data"`
Meta json.RawMessage `json:"meta"`
Error *Error `json:"error"`
}
// Do sends a request with retries and returns the data and meta parts.
func (c *Client) Do(ctx context.Context, method, path string, query url.Values, body any) (json.RawMessage, json.RawMessage, error) {
target := c.Base + path
if len(query) > 0 {
target += "?" + query.Encode()
}
var payload []byte
if body != nil {
var err error
if payload, err = json.Marshal(body); err != nil {
return nil, nil, err
}
}
var lastErr error
for attempt := 0; attempt < c.Tries; attempt++ {
var wait time.Duration
req, err := http.NewRequestWithContext(ctx, method, target, bytes.NewReader(payload))
if err != nil {
return nil, nil, err
}
if c.Key != "" {
req.Header.Set("Authorization", "Bearer "+c.Key)
}
if body != nil {
req.Header.Set("content-type", "application/json")
}
res, err := c.HTTP.Do(req)
if err != nil {
lastErr = err
} else {
raw, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
lastErr = readErr
} else {
var env envelope
if err := json.Unmarshal(raw, &env); err != nil {
return nil, nil, err
}
if env.Error == nil {
return env.Data, env.Meta, nil
}
env.Error.Status = res.StatusCode
lastErr = env.Error
// Only a unit-limit 429 sends Retry-After. Other 429 caps last far longer than a retry loop.
after, _ := strconv.Atoi(res.Header.Get("retry-after"))
switch {
case res.StatusCode == http.StatusTooManyRequests && after > 0:
wait = time.Duration(after) * time.Second
case res.StatusCode == http.StatusBadGateway:
default:
return nil, nil, env.Error
}
}
}
if attempt+1 == c.Tries {
break
}
if wait == 0 {
backoff := time.Second << attempt
if backoff > 30*time.Second {
backoff = 30 * time.Second
}
wait = backoff/2 + time.Duration(rand.Int63n(int64(backoff/2)))
}
select {
case <-time.After(wait):
case <-ctx.Done():
return nil, nil, ctx.Err()
}
}
return nil, nil, lastErr
}
// Get decodes the data part of a GET response into out.
func (c *Client) Get(ctx context.Context, path string, query url.Values, out any) error {
data, _, err := c.Do(ctx, http.MethodGet, path, query, nil)
if err != nil {
return err
}
return json.Unmarshal(data, out)
}
// Transfer is one stored row. Ts is an ISO string on this endpoint.
type Transfer struct {
ID int64 `json:"id"`
UID string `json:"uid"`
Ts string `json:"ts"`
Chain string `json:"chain"`
Block int64 `json:"block"`
Tx string `json:"tx"`
Kind string `json:"kind"`
Token string `json:"token"`
TokenAddress *string `json:"token_address"`
Amount float64 `json:"amount"`
USD float64 `json:"usd"`
From string `json:"from"`
To string `json:"to"`
Sampled bool `json:"sampled"`
FromLabel *string `json:"from_label"`
FromType *string `json:"from_type"`
ToLabel *string `json:"to_label"`
ToType *string `json:"to_type"`
}
// ErrStop ends a Transfers walk early with no error.
var ErrStop = errors.New("stop")
// Transfers walks pages, newest first, and calls fn for each row.
func (c *Client) Transfers(ctx context.Context, filters url.Values, fn func(Transfer) error) error {
query := url.Values{"limit": {"100"}}
for name, values := range filters {
query[name] = values
}
for {
data, metaRaw, err := c.Do(ctx, http.MethodGet, "/transfers", query, nil)
if err != nil {
return err
}
var rows []Transfer
if err := json.Unmarshal(data, &rows); err != nil {
return err
}
for _, row := range rows {
if err := fn(row); err != nil {
if errors.Is(err, ErrStop) {
return nil
}
return err
}
}
var meta struct {
NextCursor *int64 `json:"next_cursor"`
}
if err := json.Unmarshal(metaRaw, &meta); err != nil {
return err
}
if meta.NextCursor == nil {
return nil
}
query.Set("before", strconv.FormatInt(*meta.NextCursor, 10))
}
}#Use it
go
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/url"
"os"
"example.com/app/farbeam"
)
func main() {
ctx := context.Background()
client := farbeam.New(os.Getenv("FARBEAM_KEY"))
// Read the state of each chain
var status struct {
Chains []struct {
Chain string `json:"chain"`
Status string `json:"status"`
} `json:"chains"`
}
if err := client.Get(ctx, "/status", nil, &status); err != nil {
log.Fatal(err)
}
for _, c := range status.Chains {
fmt.Println(c.Chain, c.Status)
}
// Read the first 200 rows on Ethereum at $1m or more
count := 0
filters := url.Values{"chain": {"ethereum"}, "min_usd": {"1000000"}}
err := client.Transfers(ctx, filters, func(row farbeam.Transfer) error {
count++
if count == 200 {
return farbeam.ErrStop
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Println("rows", count)
// Handle a known error by code
var label json.RawMessage
err = client.Get(ctx, "/labels/0x1111111111111111111111111111111111111111", nil, &label)
var apiErr *farbeam.Error
if errors.As(err, &apiErr) && apiErr.Code == "not_found" {
fmt.Println("no label for this address")
} else if err != nil {
log.Fatal(err)
}
// Test a rule
data, _, err := client.Do(ctx, "POST", "/rules/validate", nil, map[string]string{
"expression": `usd >= 5m and token in ["USDC","USDT"]`,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data[:80]))
}Replace example.com/app/farbeam with your module path.
#How it handles failure
| Case | Behavior |
|---|---|
429 with Retry-After | Waits that long, then retries |
429 with no Retry-After | Returns the error. It is a daily or hourly cap. |
502 upstream_unavailable | Backs off and retries |
| Network error or timeout | Backs off and retries |
| Other 4xx, 500 | Returns *farbeam.Error at once |
| Context cancelled | Returns ctx.Err() during a wait |
#Types to watch
- Nullable text fields are
*string, becausetoken_addressand the label fields can benull. tsdiffers by surface: a string on REST and an integer on webhooks and the stream. A webhook struct needsTs int64. See versioning.- For a token report, decode into your own struct with only the fields you read, and use the data model.
json.Unmarshalignores extra fields, so new API fields will not break it. - Numeric fields that do not apply to a chain family are
null. Use*float64for fields such asbase_fee_gwei.
#Webhook verification in Go
See the Go sample on the webhook signatures page. Read the body once with io.ReadAll before you verify, and limit its size.