Farbeam

Alerts

An alert is a Beamline rule plus a destination. When the index stores a new transfer that matches the rule, Farbeam sends it to a signed JSON webhook, a Discord webhook or a linked Telegram chat. Email delivery is Planned.

Telegram delivery is Planned. The code is ready, but the bot is not connected yet. It will need a linked private chat. Until the bot is connected, POST /account/telegram returns 503 telegram_not_configured.

#Rule samples

Each rule below was tested against POST /rules/validate at 2026-11-04T19:21:20Z. The test runs the rule on the newest stored transfers, 1,948 rows at that time. The index was about 18 minutes old, so the match counts show that the rule works, and they are not typical rates.

RulePurposeMatches
usd >= 5m and token in ["USDC","USDT"]Large stablecoin moves18
to_type == "exchange" and usd >= 1mInto a labeled exchange1
from_type == "exchange" and usd >= 1mOut of a labeled exchange1
chain == "ethereum" and kind == "native" and usd >= 1mLarge native ETH transfers7
from == "0x0000000000000000000000000000000000000000" and usd >= 1mMints of curated tokens4
to == "0x0000000000000000000000000000000000000000" and usd >= 1mBurns of curated tokens4
sampled == false and token in ["USDC","USDT","DAI"] and usd >= 10mVery large stablecoin moves, Solana excluded15

You can run any of these in the rule tester. The test window is the newest 2,000 stored rows, about 20 minutes of data on 2026-11-04 because Base produces many rows. More patterns are on Beamline examples.

#What a rule can read

FieldTypeNotes
chaintextethereum, base, bnb, arbitrum or solana produce events. OP Mainnet and Polygon have no index
token, token_addresstextCurated tokens only
kindtextnative or token
from, to, txtextComparison ignores case
from_label, to_label, from_type, to_typetextFrom the label set. Null when unlabeled
usd, amount, blocknumberSuffixes work: 250k, 5m, 1b. So do underscores: 100_000
sampledbooleanTrue on Solana events
liquidity_usd, change_pctnumberNaming either one makes the rule a liquidity rule. See below

Operators are ==, !=, >, >=, <, <=, contains, startswith, in [...] and not in [...], joined with and, or, not and parentheses. A rule can have up to 1,000 characters and 64 conditions. It is parsed by a small compiler, with no eval, and a parse error reports the position:

Text
usd >= "5"        ->  422 invalid_rule: "usd" needs a number (at position 7)
wallet == "0x"    ->  422 invalid_rule: Unknown field "wallet" (at position 0)

#Liquidity rules

A rule that names liquidity_usd or change_pct watches liquidity samples and does not watch transfers. The sampler reads DexScreener liquidity of curated tokens every 5 minutes, and it skips stablecoins and wrapped native assets. change_pct compares the new sample with the oldest one held, up to 12 samples back, which is about one hour. The sampler runs only while such a rule exists, so a new rule has no history at first. The history is in memory, so it starts again after a restart.

Beamline
change_pct <= -20 and liquidity_usd >= 100k

The rule tester parses this expression, but it tests stored transfers only. Those rows have no liquidity field, so the match count says nothing about this kind of rule.

#Delivery

PropertyValue
MethodHTTPS POST, content-type: application/json
SuccessAny 2xx reply within 6 seconds
RedirectsNot followed. A 3xx counts as a failure
Attempts4: at once, then after 30 seconds, 5 minutes and 30 minutes
WorkerRuns every 3 seconds, up to 20 due deliveries per pass
Payload typestransfer.matched, liquidity.matched, rule.test
Formatsjson, discord, telegram
CooldownAfter a rule fires, further matches are dropped for cooldown_seconds: 60 to 86,400, default 300
LogStatus, attempts, HTTP code and error per delivery, kept 14 days
RestartPending deliveries are in SQLite and continue after a restart

This is a real payload shape with a transfer captured at 2026-11-04T19:08:59Z. The rule_id, rule_name and sent_at values are examples.

JSON
{
  "type": "transfer.matched",
  "rule_id": "rule_000000000000",
  "rule_name": "Into exchanges",
  "sent_at": "2026-11-04T19:09:02.000Z",
  "data": {
    "uid": "ethereum:0x635ceb5456c60caf1364b4499ab484d762c8ca9b02e3f3ed55e6e12c89b48091:58",
    "ts": 1793819339000,
    "chain": "ethereum",
    "block": 26343447,
    "tx": "0x635ceb5456c60caf1364b4499ab484d762c8ca9b02e3f3ed55e6e12c89b48091",
    "kind": "token",
    "token": "USDC",
    "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": 12000000,
    "usd": 11996244,
    "from": "0x3d927af0b7ebbf5c593f45e324f9d0c23bc5d4b4",
    "to": "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
    "sampled": false,
    "id": 822,
    "from_label": null,
    "from_type": null,
    "to_label": "Coinbase 10",
    "to_type": "exchange"
  }
}

In a webhook payload, data.ts is Unix milliseconds. In GET /transfers the same value is an ISO 8601 string.

#What is guaranteed and what is not

SituationWhat happens
Your endpoint returns 2xxThe delivery is marked delivered
Your endpoint fails 4 timesThe delivery is marked failed, and the event is not sent again
Your endpoint handled the event but replied after 6 secondsFarbeam retries, so you receive a duplicate. Remove duplicates with data.uid
A match arrives during the rule's cooldownIt is dropped, and no log row is written. With the default of 300 seconds, a rule sends at most 12 events per hour
The rule reached 120 deliveries in the current UTC hourFurther matches in that hour are dropped. No log row is written for them
A poller is behind or a block range is skippedThe event was never indexed, so no rule can match it
The process restartsTransfers during the restart are not backfilled and are not matched
A transfer is below $100,000 or the token is not curatedIt is not stored, so no rule can match it
The event is on SolanaOnly a small sample of blocks is read, so most events are not seen

For a monitoring job where a miss matters, also poll GET /transfers and compare uid values. Even then, Farbeam is one source with the coverage on the platform page. Use a second source for anything critical.

#Signature

Each request has these headers. The signature covers the raw body, so verify before you parse JSON.

HTTP
x-farbeam-signature: t=1793820348,v1=458e2d66988d1d9740ad3d7e5e6f3fe79eaa20147cc769d8e34fba053212df6a
x-farbeam-rule: rule_000000000000
user-agent: farbeam-webhooks/1

v1 is the hex HMAC-SHA256 of <t>.<raw body>, using the signing secret of the rule. t is set again on each attempt, so a retry passes a time check. The header above is an example. The code below was tested against the same formula the server uses.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(want), b = Buffer.from(parts.v1 || "");
  return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance:
        return False
    want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts.get("v1", ""))

The signing secret is returned once, when you create the rule. There is no rotate endpoint. To change a secret, delete the rule and create a new one. More on webhook signatures.

#Limits

LimitValue
Rules per account, or per older key without an account10
Cooldown60 to 86,400 seconds, default 300
Deliveries per rule per UTC hour120. Test deliveries count
Test deliveries per rule per hour10
Failed deliveries in a row before the rule turns off20
Name length80 characters
URLPublic HTTPS on port 443, up to 500 characters
Refused URL hostsIP literals, localhost, .local, .internal, and hosts that resolve to a private address
Delivery log14 days

A single event that fails all 4 attempts counts as one failed delivery. The count returns to 0 after any success. Attempts overlap, so a broken endpoint on a busy rule can reach 20 failures in about 36 minutes. To turn the rule on again, send PATCH /rules/{id} with {"enabled": true}. That also clears the count.

#How to start

  1. Write the expression in the rule tester and confirm that it matches real rows.
  2. Sign in to the console with a Solana wallet. Signing a text message sends no transaction and costs nothing.
  3. Create the rule in the console, or with a key and POST /rules. Save the signing secret.
  4. Send a test delivery with POST /rules/{id}/test. The payload type is rule.test and data is null.
  5. Check the delivery log for the HTTP code your endpoint returned.
curl -s -X POST "https://farbeamlabs.com/api/v1/rules" \
  -H "Authorization: Bearer $FARBEAM_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"Into exchanges","expression":"to_type == \"exchange\" and usd >= 1m","url":"https://example.com/hooks/farbeam","format":"json","cooldown_seconds":600}'
const res = await fetch("https://farbeamlabs.com/api/v1/rules", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.FARBEAM_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({
    name: "Into exchanges",
    expression: 'to_type == "exchange" and usd >= 1m',
    url: "https://example.com/hooks/farbeam",
    format: "json",
    cooldown_seconds: 600,
  }),
});
const { data } = await res.json();
console.log(data.id, data.signing_secret); // the secret is shown once
import os, requests

res = requests.post(
    "https://farbeamlabs.com/api/v1/rules",
    headers={"Authorization": f"Bearer {os.environ['FARBEAM_KEY']}"},
    json={
        "name": "Into exchanges",
        "expression": 'to_type == "exchange" and usd >= 1m',
        "url": "https://example.com/hooks/farbeam",
        "format": "json",
        "cooldown_seconds": 600,
    },
    timeout=30,
)
data = res.json()["data"]
print(data["id"], data["signing_secret"])  # the secret is shown once

For a Discord channel, set format to discord and use the Discord webhook URL as url. For Telegram, link a chat in the console and set format to telegram, with no URL. See webhooks.

An alert is a notice that a public event happened. It is not advice and it makes no statement about a token or an address.