Beamline
Beamline is a small expression language for alert rules. An expression reads one transfer row and returns true or false. It has no loops, variables, functions or state, and the parser compiles it to a closure without calling eval.
usd >= 5m and token in ["USDC", "USDT"] and to_type == "exchange"Test an expression with Validate a rule or the rule tester. See fields and examples.
#Grammar
expression = or_expr
or_expr = and_expr { "or" and_expr }
and_expr = not_expr { "and" not_expr }
not_expr = "not" not_expr | atom
atom = "(" or_expr ")"
| bool_field
| bool_field ( "==" | "!=" ) ( "true" | "false" )
| number_field compare number
| number_field [ "not" ] "in" "[" number { "," number } [ "," ] "]"
| text_field text_op string
| text_field [ "not" ] "in" "[" string { "," string } [ "," ] "]"
compare = "==" | "!=" | ">" | ">=" | "<" | "<="
text_op = "==" | "!=" | "contains" | "startswith"
number = digit { digit | "_" } [ "." digit { digit } ] [ "k" | "m" | "b" ]
string = '"' { any character } '"' | "'" { any character } "'"Whitespace between tokens is optional. There are no comments.
#Types and fields
| Type | Fields |
|---|---|
| Text | chain, token, token_address, kind, from, to, from_label, to_label, from_type, to_type, tx |
| Number | usd, amount, block |
| Boolean | sampled |
id, uid and ts are in the payload, and they are not rule fields.
#Operators
| Operator | Text | Number | Boolean | Notes |
|---|---|---|---|---|
==, != | Yes | Yes | Yes, with true or false | |
>, >=, <, <= | No | Yes | No | |
contains | Yes | No | No | Substring |
startswith | Yes | No | No | Prefix |
in, not in | Yes | Yes | No | List of literals |
and, or, not | Works on conditions |
#Rules of comparison
- Text comparison ignores case.
token == "usdc"matchesUSDC. A checksummed address matches a lowercasefrom. This also holds for Solana base58, so two addresses that differ only by case would compare equal. - **
nullbecomes an empty string.**from_label == ""is true for a row with no label, andfrom_label != ""means it has one.token_address == ""is true for native rows. - **
trueandfalsemust be lowercase.** All other keywords and field names ignore case.USD >= 1M AND TOKEN == "usdc"is valid. - Values are not checked against known values.
chain == "etherium"parses and never matches. Run the validate call and look atmatched.
#Numbers
| Form | Value |
|---|---|
1000000 | 1,000,000 |
1_000_000 | 1,000,000 |
250k or 250K | 250,000 |
5m or 5M | 5,000,000 |
2.5m | 2,500,000 |
1b or 1B | 1,000,000,000 |
Numbers cannot be negative and cannot use exponent form. Decimals need a digit before the dot. .5 and 1e6 are errors.
usd and amount are doubles, so == on them almost never matches. Use a range.
#Strings
Use double or single quotes. A backslash makes the next character literal. For example, "a\"b" is the three characters a"b. Field values rarely need this.
#Precedence
From tightest to loosest: not, then and, then or. Use parentheses when you mix them.
| Expression | Reads as |
|---|---|
a or b and c | a or (b and c) |
not a and b | (not a) and b |
not (a or b) | Neither a nor b |
(token == "USDC" or token == "USDT") and usd >= 1mWithout the parentheses, that rule would match every USDC row of any size above the floor, plus USDT rows of $1m or more.
#Boolean shorthand
sampled alone is true for Solana rows. not sampled excludes them. sampled == false and sampled != true give the same result.
#Errors
A rule that does not parse returns 422 invalid_rule. POST /rules/validate and POST /rules return the same errors. Most messages end with (at position N), a zero-based character offset into your string. Each row below was tested on 2026-11-04 UTC.
| Input | Message |
|---|---|
| Empty or spaces | Rule is empty |
| 1,001 characters | Rule is longer than 1000 characters |
| More than 64 conditions | Rule has more than 64 conditions |
foo == 1 | Unknown field "foo" (at position 0) |
usd | Expected an operator after "usd" (at position 3) |
usd > | Expected a value after ">" (at position 5) |
usd == "x" | "usd" needs a number (at position 7) |
token > "a" | ">" works only on number fields (at position 6) |
token > 5 | "token" needs a quoted string (at position 8) |
token == USDC | "token" needs a quoted string (at position 9) |
usd contains 5 | "contains" works only on text fields (at position 4) |
usd "x" | Unknown operator "x" (at position 4) |
usd and 5 | Unknown operator "and" (at position 4) |
usd ~ 5 | Unexpected character "~" (at position 4) |
usd > -1 | Unexpected character "-" (at position 6) |
usd > 1e6 | Unexpected character "1" (at position 6) |
(usd > 1 | Missing ")" (at position 8) |
usd > 1) | Unexpected ")" (at position 7) |
() | Expected a field name (at position 1) |
usd > 1 and | Rule ends too early (at position 11) |
"abc | Unterminated string (at position 0) |
chain == "a" "b" | Unexpected "b" (at position 13) |
token in [] | List is empty (at position 6) |
token in "a" | Expected "[" after "in" (at position 9) |
token in ["a" | Expected "," or "]" (at position 13) |
token in [1] | List values for "token" must be strings (at position 10) |
usd in ["a"] | List values for "usd" must be numbers (at position 8) |
from_label not contains "x" | Expected "in" after "not" (at position 15) |
sampled == 1 | Expected true or false (at position 11) |
sampled == True | Expected true or false (at position 11) |
sampled not in ["a"] | Unexpected "not" (at position 8) |
To negate contains, put not in front of the whole condition: not from_label contains "x".
#Limits
#How rules run
Each enabled rule is compiled when it is saved or when the service starts. A new row runs through all enabled transfer rules. A rule that names liquidity_usd or change_pct runs on liquidity samples instead. After a rule fires, it ignores matches until its cooldown ends. Rules read only the row itself, so a rule cannot refer to earlier rows, sums, counts or time windows. To alert on "more than $10m in an hour", receive the rows in your own service and keep the count there.