Core conceptsHow scoring works

How scoring works

Three independent engines turn a session's signals into a single 1–99 bot-likelihood score, combined by strongest evidence, with stable detection IDs and plain-English reasons.

Botect scores a session, not a single request. As signals arrive at POST /v1/events, Botect (asynchronously, off your request path) runs them through independent engines and combines the results into one score, a band, a set of detection IDs, and a human-readable reason.

The score scale

The score is an integer on a 1–99 scale where lower means more bot-confident:

  • 1 — definite bot (strongest evidence)
  • 99 — strongly human
  • 0not computed (sentinel). A session with no signals yet, or a degraded lookup. 0 is never conflated with 1.

A project-level threshold T (default 30) draws the line between bot and human bands. See Score bands.

The engines

Engines are independent and layered — not voted. Each emits an optional score, detection IDs, and reasons, or abstains.

EngineLooks atEmits
HeuristicsHeader-shape fingerprint, user-agent patterns, ASN / network originHard low scores on known-bot matches
JS detectionnavigator/automation/headless tells, capability mismatchesA js_detection.passed signal — non-enforcing
BehavioralMouse entropy, scroll velocity, visibility changes, first-input delayA graded score from how human the interaction looks

JS detection is non-enforcing: a failed JS probe alone can never push the final score into a bot band. It's a contributing signal, not a verdict. Headless browsers that behave like humans still need behavioral evidence to be flagged.

A TLS-fingerprint engine arrives in a future version without changing this interface.

Combining results

The combiner (ScoreCombiner) resolves the engines' opinions into one result:

Verified short-circuit

If the session matches a known verified bot, set verified_bot = true with its category, band verified, and skip bot banding.

Sentinel

If no engine has an opinion, the score is 0 (not computed) — never 1.

Strongest evidence

The final score is the minimum of the engines' scores — the most bot-confident wins.

JS guard

A JS-detection-only negative cannot, by itself, drop the score below the threshold T.

Reasons & band

Union the detection IDs, compose the reason from their templates, and classify the band against T.

Detection IDs and reasons

Every score in a bot band — and every blocking or challenging verdict — carries at least one detection ID and a reason. Detection IDs are stable integers from a fixed registry, so you can match on them in rules and they won't shift under you.

A clean human score may carry an empty detection_ids array with a generic reason ("nothing flagged").

{
  "score": 14,
  "verdict": "definite",
  "detection_ids": [50331648, 50331651],
  "reason": "Headless automation signature; no human interaction recorded."
}

Recomputation

Scoring runs in a background job after ingest, debounced per session so a burst of batches coalesces into one recompute. Each recompute writes the new score, band, detection IDs, and reason — and busts the verdict cache so the next read reflects it.

Because scoring is asynchronous, a freshly-minted session may briefly read as not_computed (score 0, action allow) until its first score lands.