Rules
The single place enforcement is decided — a safe, composable boolean grammar over every signal field, compiled to an AST and never eval'd.
Rules are the only place Botect decides to block or challenge. A rule is a boolean expression over signal fields plus an action to take when it matches. They cover everything from the simple case (act on a score band) to a decision that depends on a specific path, country, detection ID, or behavioral threshold.
In the resolution order rules run after the two guards (allow_verified, protect_static) and are the last word: the first active rule that matches and carries a terminating action wins, and its name appears in the verdict reason.
Starter rules
Every project is provisioned with two rules covering the enforceable bands. They ship inactive — Botect never starts blocking real traffic on your behalf — so turning one on is the normal way to begin enforcing.
| Name | Expression | Action |
|---|---|---|
| Block confirmed automation | band == "definite" | block |
| Challenge likely-automated traffic | band == "likely_automated" | challenge |
Activating a rule also feeds edge enforcement: a rule whose expression reads only band can be honored by an IP list, so its matching IPs are synced to Cloudflare. A rule that reads path, country, ua or score cannot — an IP list is request-blind — so it applies at the verdict API only.
Safety
Rule expressions are a constrained grammar, not code. At save time each expression is parsed and validated into an abstract syntax tree (AST); the verdict path walks the stored AST. There is no eval, no method calls, no code execution — an invalid or out-of-grammar expression is rejected with a 422 when you create the rule, never at evaluation time.
Grammar
expr := orExpr
orExpr := andExpr ( "OR" andExpr )*
andExpr := unary ( "AND" unary )*
unary := "NOT" unary | "(" expr ")" | predicate
predicate := comparison | boolField
comparison:= field op value
boolField := verified_bot | js_detection.passed | static_resource (bare truthiness)
op := "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not in"
value := number | "quoted string" | "[" list "]" | true | false | null
Allow-listed fields
score, band, verified_bot, verified_bot_category, js_detection.passed, static_resource, detection_ids, path, ip, country, ua, behavioral.mouse_entropy, behavioral.scroll_velocity, behavioral.visibility_changes, behavioral.first_input_delay_ms.
Any field outside this list is rejected. See Signals & fields for each field's meaning and type.
Operators by type
| Field type | Fields | Operators |
|---|---|---|
| Numeric | score, behavioral.* | == != < <= > >= |
| Boolean | verified_bot, js_detection.passed, static_resource | == != (or bare) |
| String | path, country, ua, verified_bot_category, band | == != in not in |
| Array | detection_ids | in / not in (membership) |
Examples
score < 30 AND path == "/login" AND NOT verified_bot
→ block
detection_ids in [50331648, 50331651] AND NOT verified_bot
→ block
band == "likely_automated" AND country in ["RU", "CN"]
→ challenge
behavioral.mouse_entropy < 0.2 AND behavioral.visibility_changes == 0
→ challenge
Creating a rule
Send the expression text and an action. Botect compiles and validates it before storing.
curl -X POST https://api.botect.ai/v1/projects/123/rules \
-H "Authorization: Bearer YOUR_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Protect login from bots",
"expression": "score < 30 AND path == "/login" AND NOT verified_bot",
"action": "block",
"sort_order": 10
}'
See Create a rule for the full request and response. Rules are listed in sort_order (ascending) and evaluated in that order.
Order decides precedence. A narrow exemption only overrides a broad rule if it sits above it — give your allow rules a lower sort_order than the band rules they are meant to pre-empt. The starter rules sit at 10 and 20.
Always guard enforcement rules with AND NOT verified_bot unless you specifically intend to act on verified crawlers — otherwise a misconfigured rule could block Google or an AI crawler you want indexing your site.
Evaluation
When a verdict is read, active rules run in sort_order. Every action terminates evaluation with that action as the verdict — allow and delay included — except log, which records the match and lets evaluation continue so a later block or challenge still applies. If no rule matches, the verdict is allow.
The matched rule populates the verdict reason and is recorded on the block itself, so you can always trace why a request was acted on.
Rule changes apply to verdicts computed from then on. A verdict already cached for a session is served until it expires, so allow up to the verdict cache TTL (60s by default) for a change to be visible everywhere.