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.
// 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
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
| Case | Behavior |
|---|---|
429 with Retry-After | Waits that long, then retries |
429 with no Retry-After | Throws. It is a daily or hourly cap, and retrying will not help. |
502 upstream_unavailable | Backs off and retries |
| Network error or timeout | Backs off and retries |
| Other 4xx, 500 | Throws 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.