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.
| Rule | Purpose | Matches |
|---|---|---|
usd >= 5m and token in ["USDC","USDT"] | Large stablecoin moves | 18 |
to_type == "exchange" and usd >= 1m | Into a labeled exchange | 1 |
from_type == "exchange" and usd >= 1m | Out of a labeled exchange | 1 |
chain == "ethereum" and kind == "native" and usd >= 1m | Large native ETH transfers | 7 |
from == "0x0000000000000000000000000000000000000000" and usd >= 1m | Mints of curated tokens | 4 |
to == "0x0000000000000000000000000000000000000000" and usd >= 1m | Burns of curated tokens | 4 |
sampled == false and token in ["USDC","USDT","DAI"] and usd >= 10m | Very large stablecoin moves, Solana excluded | 15 |
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
| Field | Type | Notes |
|---|---|---|
chain | text | ethereum, base, bnb, arbitrum or solana produce events. OP Mainnet and Polygon have no index |
token, token_address | text | Curated tokens only |
kind | text | native or token |
from, to, tx | text | Comparison ignores case |
from_label, to_label, from_type, to_type | text | From the label set. Null when unlabeled |
usd, amount, block | number | Suffixes work: 250k, 5m, 1b. So do underscores: 100_000 |
sampled | boolean | True on Solana events |
liquidity_usd, change_pct | number | Naming 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:
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.
change_pct <= -20 and liquidity_usd >= 100kThe 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
| Property | Value |
|---|---|
| Method | HTTPS POST, content-type: application/json |
| Success | Any 2xx reply within 6 seconds |
| Redirects | Not followed. A 3xx counts as a failure |
| Attempts | 4: at once, then after 30 seconds, 5 minutes and 30 minutes |
| Worker | Runs every 3 seconds, up to 20 due deliveries per pass |
| Payload types | transfer.matched, liquidity.matched, rule.test |
| Formats | json, discord, telegram |
| Cooldown | After a rule fires, further matches are dropped for cooldown_seconds: 60 to 86,400, default 300 |
| Log | Status, attempts, HTTP code and error per delivery, kept 14 days |
| Restart | Pending 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.
{
"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
| Situation | What happens |
|---|---|
| Your endpoint returns 2xx | The delivery is marked delivered |
| Your endpoint fails 4 times | The delivery is marked failed, and the event is not sent again |
| Your endpoint handled the event but replied after 6 seconds | Farbeam retries, so you receive a duplicate. Remove duplicates with data.uid |
| A match arrives during the rule's cooldown | It 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 hour | Further matches in that hour are dropped. No log row is written for them |
| A poller is behind or a block range is skipped | The event was never indexed, so no rule can match it |
| The process restarts | Transfers during the restart are not backfilled and are not matched |
| A transfer is below $100,000 or the token is not curated | It is not stored, so no rule can match it |
| The event is on Solana | Only 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.
x-farbeam-signature: t=1793820348,v1=458e2d66988d1d9740ad3d7e5e6f3fe79eaa20147cc769d8e34fba053212df6a
x-farbeam-rule: rule_000000000000
user-agent: farbeam-webhooks/1v1 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
| Limit | Value |
|---|---|
| Rules per account, or per older key without an account | 10 |
| Cooldown | 60 to 86,400 seconds, default 300 |
| Deliveries per rule per UTC hour | 120. Test deliveries count |
| Test deliveries per rule per hour | 10 |
| Failed deliveries in a row before the rule turns off | 20 |
| Name length | 80 characters |
| URL | Public HTTPS on port 443, up to 500 characters |
| Refused URL hosts | IP literals, localhost, .local, .internal, and hosts that resolve to a private address |
| Delivery log | 14 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
- Write the expression in the rule tester and confirm that it matches real rows.
- Sign in to the console with a Solana wallet. Signing a text message sends no transaction and costs nothing.
- Create the rule in the console, or with a key and POST /rules. Save the signing secret.
- Send a test delivery with POST /rules/{id}/test. The payload type is
rule.testanddatais null. - 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 onceimport 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 onceFor 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.