Farbeam

Developer integration

A developer who needs a large-transfer feed or token checks in an application can call the REST API directly, with no SDK. Official SDK packages are Planned, and they do not exist today. The OpenAPI file can generate a client.

#Workflow

  1. Prototype with no key. The limit is 30 units per minute.
  2. Sign in to the console with a Solana wallet, create a key, and keep it on the server. POST /keys needs that session, so a script cannot create keys without it. Browser code can use the API with no key, because CORS is open, and a key placed there is public.
  3. Read x-ratelimit-remaining and back off on 429.
  4. Use uid as your primary key, and store rows in your own database if you need more than 7 days.
  5. Combine the live stream with REST polling, because the stream has no replay.
  6. For server-side events, use webhooks instead of a held-open stream.

#Page through transfers

before takes the meta.next_cursor of the last page. The value is null on the last page. Rows come newest first.

curl -s "https://farbeamlabs.com/api/v1/transfers?min_usd=1000000&limit=100" -H "Authorization: Bearer $FARBEAM_KEY" | jq '.meta.next_cursor'
curl -s "https://farbeamlabs.com/api/v1/transfers?min_usd=1000000&limit=100&before=1217" -H "Authorization: Bearer $FARBEAM_KEY"
const BASE = "https://farbeamlabs.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.FARBEAM_KEY}` };

export async function* transfers(params = {}) {
  let before = null;
  for (;;) {
    const qs = new URLSearchParams({ limit: "100", ...params, ...(before ? { before } : {}) });
    const res = await fetch(`${BASE}/transfers?${qs}`, { headers });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") || 5) * 1000));
      continue;
    }
    const { data, meta, error } = await res.json();
    if (error) throw new Error(`${error.code}: ${error.message}`);
    yield* data;
    if (!meta.next_cursor) return;
    before = meta.next_cursor;
  }
}
import os, time, requests

BASE = "https://farbeamlabs.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FARBEAM_KEY']}"}

def transfers(**params):
    before = None
    while True:
        q = {"limit": 100, **params, **({"before": before} if before else {})}
        res = requests.get(f"{BASE}/transfers", params=q, headers=HEADERS, timeout=30)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("retry-after", 5)))
            continue
        body = res.json()
        if "error" in body:
            raise RuntimeError(body["error"]["code"])
        yield from body["data"]
        if not body["meta"]["next_cursor"]:
            return
        before = body["meta"]["next_cursor"]

#Live stream with gap filling

The stream sends transfer and block events. It has no replay. After a reconnect, call REST and stop when you reach a uid you already hold. A stream is limited to 6 per network, and the server sends a comment line every 25 seconds.

const seen = new Set();
const stream = new EventSource("https://farbeamlabs.com/api/live");
const handle = (ev) => { if (!seen.has(ev.uid)) { seen.add(ev.uid); process(ev); } };

stream.addEventListener("transfer", (m) => handle(JSON.parse(m.data)));
stream.addEventListener("open", async () => {
  const res = await fetch("https://farbeamlabs.com/api/v1/transfers?limit=100");
  const { data } = await res.json();
  data.reverse().forEach(handle);
});
import json, requests

seen = set()

def handle(ev):
    if ev["uid"] not in seen:
        seen.add(ev["uid"])
        print(ev["chain"], ev["token"], ev["usd"])

while True:
    for ev in reversed(requests.get("https://farbeamlabs.com/api/v1/transfers?limit=100", timeout=30).json()["data"]):
        handle(ev)
    try:
        with requests.get("https://farbeamlabs.com/api/live", stream=True, timeout=60) as r:
            event = None
            for line in r.iter_lines(decode_unicode=True):
                if line.startswith("event: "):
                    event = line[7:]
                elif line.startswith("data: ") and event == "transfer":
                    handle(json.loads(line[6:]))
    except requests.RequestException:
        pass

data.ts differs by route: an ISO string in REST and Unix milliseconds in the stream. Parse both if you merge them.

#Validate a rule in CI

POST /rules/validate needs no key and costs 1 unit. A bad expression returns 422 with a position. You can test a rule file during a build.

curl
curl -s -X POST "https://farbeamlabs.com/api/v1/rules/validate" -H "content-type: application/json" -d '{"expression":"chain == \"base\" and usd >= 250k"}' | jq '.data | {valid, tested, matched}'

Real result at 2026-11-04T19:21:20Z:

JSON
{ "valid": true, "tested": 1948, "matched": 487 }

#Handling failure

ResponseMeaningAction
429 rate_limitedUnits used up in this minuteWait Retry-After, in seconds
502 upstream_unavailableA public source failed, and source names itRetry with backoff. It usually clears soon. Cache the last good value
Report with sources[].ok: falseOne source failedKeep the result, and treat unknown as unknown
Wallet with partial: trueOne chain failedShow that chain as missing, not as zero
429 daily_limit20,000 units used in this UTC dayWait for the next UTC day
401 invalid_keyKey revoked or wrongCreate a new key. Lost keys cannot be recovered
Report or wallet with stale: trueEvery source failed, and this is the last saved copyShow its as_of time, or treat it as missing

Record the x-request-id header in your logs. Include it when you report a problem.

#What this will not do