Farbeam
Docs / SDK guides

JavaScript client

This is a complete client in one file. It has no dependencies. It needs Node.js 18 or later, or any browser with fetch. It is sample code to copy, because official packages are planned and do not exist today.

#The client

Save this as farbeam.mjs.

JavaScript
// farbeam.mjs: a small client for the Farbeam API. Sample code, not an official package.
const DEFAULT_BASE = "https://farbeamlabs.com/api/v1";

export class FarbeamError extends Error {
  constructor(status, code, message, requestId) {
    super(`${status} ${code}: ${message}`);
    this.name = "FarbeamError";
    this.status = status;
    this.code = code;
    this.requestId = requestId;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export class Farbeam {
  constructor({ key = null, base = DEFAULT_BASE, tries = 5, timeoutMs = 30000 } = {}) {
    this.key = key;
    this.base = base;
    this.tries = tries;
    this.timeoutMs = timeoutMs;
    this.rate = null;
  }

  async request(method, path, { query, body } = {}) {
    const url = new URL(this.base + path);
    for (const [name, value] of Object.entries(query ?? {})) {
      if (value !== undefined && value !== null) url.searchParams.set(name, String(value));
    }
    const headers = {};
    if (this.key) headers.Authorization = `Bearer ${this.key}`;
    if (body !== undefined) headers["content-type"] = "application/json";

    let lastError;
    for (let attempt = 0; attempt < this.tries; attempt++) {
      let wait = null;
      try {
        const res = await fetch(url, {
          method,
          headers,
          body: body === undefined ? undefined : JSON.stringify(body),
          signal: AbortSignal.timeout(this.timeoutMs),
        });
        if (res.headers.has("x-ratelimit-remaining")) {
          this.rate = {
            limit: Number(res.headers.get("x-ratelimit-limit")),
            remaining: Number(res.headers.get("x-ratelimit-remaining")),
            reset: Number(res.headers.get("x-ratelimit-reset")),
          };
        }
        const json = await res.json();
        if (res.ok) return json;
        const { code, message, request_id: requestId } = json.error;
        lastError = new FarbeamError(res.status, code, message, requestId);
        // Only a unit-limit 429 sends Retry-After. Other 429 caps last far longer than a retry loop.
        const after = Number(res.headers.get("retry-after"));
        if (res.status === 429 && after > 0) wait = after * 1000;
        else if (res.status !== 502) throw lastError;
      } catch (err) {
        if (err instanceof FarbeamError && err.status !== 502 && err.status !== 429) throw err;
        if (err instanceof FarbeamError && err.status === 429 && wait === null) throw err;
        lastError = err;
      }
      if (attempt + 1 < this.tries) {
        await sleep(wait ?? Math.min(30000, 1000 * 2 ** attempt) * (0.5 + Math.random() / 2));
      }
    }
    throw lastError;
  }

  async get(path, query) {
    return (await this.request("GET", path, { query })).data;
  }

  // Yields rows newest first. Stop early with break, or set maxPages.
  async *transfers(filters = {}, { maxPages = Infinity } = {}) {
    let before = filters.before ?? null;
    for (let page = 0; page < maxPages; page++) {
      const { data, meta } = await this.request("GET", "/transfers", {
        query: { limit: 100, ...filters, before },
      });
      yield* data;
      if (meta.next_cursor === null) return;
      before = meta.next_cursor;
    }
  }

  status() { return this.get("/status"); }
  chains() { return this.get("/chains"); }
  gas() { return this.get("/gas"); }
  prices() { return this.get("/prices"); }
  transferStats(hours = 24) { return this.get("/transfers/stats", { hours }); }
  wallet(address) { return this.get(`/wallets/${address}`); }
  walletActivity(address, chain) { return this.get(`/wallets/${address}/activity`, { chain }); }
  token(chain, address) { return this.get(`/tokens/${chain}/${address}`); }
  tx(chain, hash) { return this.get(`/tx/${chain}/${hash}`); }
  labels(filters = {}) { return this.get("/labels", filters); }
  label(address) { return this.get(`/labels/${address}`); }

  async validateRule(expression) {
    return (await this.request("POST", "/rules/validate", { body: { expression } })).data;
  }
  rules() { return this.get("/rules"); }
  async createRule(rule) {
    return (await this.request("POST", "/rules", { body: rule })).data;
  }
  async updateRule(id, changes) {
    return (await this.request("PATCH", `/rules/${id}`, { body: changes })).data;
  }
  async deleteRule(id) {
    return (await this.request("DELETE", `/rules/${id}`)).data;
  }
  async testRule(id) {
    return (await this.request("POST", `/rules/${id}/test`)).data;
  }
  deliveries(id, limit = 25) { return this.get(`/rules/${id}/deliveries`, { limit }); }
}

#Use it

JavaScript
import { Farbeam, FarbeamError } from "./farbeam.mjs";

const fb = new Farbeam({ key: process.env.FARBEAM_KEY });

// Read the state of each chain
const { chains } = await fb.status();
console.log(chains.map((c) => `${c.chain}: ${c.status}`).join(", "));

// Walk two pages of transfers on Ethereum, $1m or more
let total = 0;
for await (const row of fb.transfers({ chain: "ethereum", min_usd: 1000000 }, { maxPages: 2 })) {
  total += row.usd;
}
console.log("sum of rows", Math.round(total), "(rows repeat value across hops)");

// Handle a known error by code
try {
  await fb.label("0x1111111111111111111111111111111111111111");
} catch (err) {
  if (err instanceof FarbeamError && err.code === "not_found") {
    console.log("no label for this address");
  } else {
    throw err;
  }
}

// Test a rule
const check = await fb.validateRule('usd >= 5m and token in ["USDC","USDT"]');
console.log(check.tested, check.matched);

// Units left in this minute
console.log(fb.rate);

#How it handles failure

CaseBehavior
429 with Retry-AfterWaits that long, then retries
429 with no Retry-AfterThrows. It is a daily or hourly cap, and retrying will not help.
502 upstream_unavailableBacks off and retries
Network error or timeoutBacks off and retries
Other 4xx, 500Throws FarbeamError at once

Retries repeat the same request. POST /rules is not safe to retry after a network error, because a rule may have been created. After such a failure, list your rules before you create again. POST /keys has the same concern, and each attempt uses up your allowance.

#Types

ts is an ISO string from /transfers and an integer in milliseconds on webhooks, on the stream and in indexed_transfers. new Date(value) handles both. Numeric fields that do not apply to a chain family are null. See the data model.

#Browser notes

  • The API allows cross-origin calls with access-control-allow-origin: *.
  • Do not put a key into public front-end code. Without a key, each visitor's own network has its own limit of 30 units per minute.
  • A key that must stay private needs a small server of your own in front.

For the stream, see streaming. For webhooks in Node.js, see the receiver in the quickstart.