Farbeam
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

CaseBehavior
429 with Retry-AfterWaits that long, then retries
429 with no Retry-AfterReturns the error. It is a daily or hourly cap.
502 upstream_unavailableBacks off and retries
Network error or timeoutBacks off and retries
Other 4xx, 500Returns *farbeam.Error at once
Context cancelledReturns ctx.Err() during a wait

#Types to watch

  • Nullable text fields are *string, because token_address and the label fields can be null.
  • ts differs by surface: a string on REST and an integer on webhooks and the stream. A webhook struct needs Ts int64. See versioning.
  • For a token report, decode into your own struct with only the fields you read, and use the data model. json.Unmarshal ignores extra fields, so new API fields will not break it.
  • Numeric fields that do not apply to a chain family are null. Use *float64 for fields such as base_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.