Bot Defense

Multi-layered bot detection: PoW challenges, heuristic scoring, and Cloudflare integration.


Overview

Every incoming request is scored from 0 (definitely human) to 100 (definitely bot). Views and events with bot_score ≥ 50 are excluded from daily rollups. Known bot User-Agents are dropped entirely before scoring.

The Core Problem

Analytics tools are plagued by bot traffic that distorts your data. Echelon solves this with a novel approach: every tracker script embeds a runtime-generated WebAssembly module that browsers must solve before pageviews are accepted. Combined with heuristic scoring and optional Cloudflare integration, the result is clean analytics data without CAPTCHAs or third-party bot detection services.

Known Bot UA Filter

Requests matching any pattern in ECHELON_BOT_UA_PATTERNS are dropped before scoring. Default list includes:

Bot Scoring Factors

SignalConditionPoints
CF verified botCloudflare confirms known bot UA+15
CF bot score ≤ 2Very confident bot+50
CF bot score 3–29Likely automated+30
CF bot score 30–50Uncertain+10
Interaction < 850msBeacon fired too fast+20
Interaction 850–1000msSuspiciously fast+8
Suspect countryIP in ECHELON_SUSPECT_COUNTRIES+30 (configurable)
Per-site suspect countryIP in ECHELON_SITE_SUSPECT_COUNTRIES+30 (stacks with global)
Burst detection> 15 requests in 5-minute window+25
Missing Accept-LanguageNo header present+10
Missing Sec-CH-UA + Sec-Fetch-SiteBoth headers absent+10
Unrealistic screenWidth/height ≤ 0 or > 10000+10
No referrer + deep pathDirect visit to path with ≥ 2 segments+5
Crawler feedUser-Agent matches monperrus/crawler-user-agents+30
AI crawler feedUser-Agent matches ai-robots-txt/robots.json+40
Datacenter IPAddress in a published AWS, GCP, DigitalOcean or Oracle Cloud range (IPv4 and IPv6)+24
Windows tablet mismatchWindows UA with a 768–1024 px wide screen+34
Linux desktopLinux UA that is not Android (pageviews only; not scored on /e)+10
Cross-siteOne IP and browser on 3+ of this instance’s sites within an hour+34
Country mismatchVisitor country implausible for the site’s home (needs a home country)+15
Cross-continentVisitor on another continent than the site’s home (needs a home country)+8
Windows tablet, foreignWindows tablet mismatch from a common-everywhere country that is not the site’s home+15
Linux desktop, foreignLinux desktop with a country mismatch from a common-everywhere country+30
Old Android, foreignAndroid 6 or older with a country mismatch from a common-everywhere country+40
PoW token missingNo proof-of-work token sent+30
PoW token invalid (pageview)Token fails verification on /b.gif+50
PoW token invalid (events)Token fails verification on /e+40
PoW waivedSession or visitor already presented a valid token in the last hour (shown as pow_ok; pow_ok_by says whether the session or the visitor key granted it)0
No interactionNo click, key press, tap or form use 5–30 minutes after the view+15
No interaction, and a PoW penalty or a waiver by visitor key onlyCarries any PoW penalty (missing or invalid token), or a waiver granted by the visitor key alone, and never interactedraised to 50
No engagement (strict mode)Session never interacted, 30–180 minutes after the view; only with ECHELON_REQUIRE_ENGAGEMENT=trueraised to 50

Score is capped at 100.

Every weight in this table can be changed with ECHELON_BOT_SCORE_WEIGHTS (key:points,key:points, 0–100 each; 0 turns the signal off). See Configuration for the keys. The datacenter lists refresh every six hours; a provider whose refresh fails, is empty or shrinks to under 10% of its previous size keeps its old ranges.

The Windows tablet, cross-site and datacenter weights are 34, 34 and 24 rather than round numbers on purpose: a real visitor can carry any one of them, and one who then reads without clicking gets the +15 for no interaction. The sums stay under the threshold of 50. Linux desktop is scored on pageviews only, not on the events endpoint, where an invalid token is already +40.

Home Audience

Each site can declare a home country and language under Settings → Home Audience (or PATCH /api/sites/:id). The geography signals are measured against it and do nothing until it is set; they also need a visitor country, which comes from ECHELON_BEHIND_CLOUDFLARE or ECHELON_TRUST_GEO_HEADERS. Visitors from the same continent are never a mismatch, and a country the continent table does not know is never foreign. On English-language sites (or with no language set), visitors from countries that appear everywhere (US, CA, AU, NZ, JP, KR, SG, IN, TR, RU, MA, AE, MX, PK) are exempt from the country mismatch. Leave it empty for a worldwide audience.

The home country must be an assigned code that Echelon can place on a continent, or RU. Anything else (UK for GB, EN, XX) is refused with a 400, and a code of that kind already stored is treated as not set. Russia spans two continents and has none in the table, so with a home of RU no visitor shares its continent: every visitor from another country is a mismatch, neighbours included (the exemption above for countries that appear everywhere still holds).

Opting in has a cost for real visitors:

Session Classifier

Some bots are only visible across requests. Every 15 minutes Echelon reviews the last 30 minutes, judging a visitor or session once its oldest view in that window is 12 minutes old:

The first run is two minutes after start. Flagged rows are scored 60 and the visitor is excluded automatically as auto:<reason> for 30 days; removing the exclusion on the admin bots page reverses a wrong one, and excluding such a visitor by hand replaces the auto: label, which makes the exclusion permanent. Validated visitors are spared by session velocity and OS rotation, but not by OS family flip and burst cohort, whose patterns cannot be one real person. As a side effect, browser DevTools device emulation or a user-agent switcher in one tab can trip OS family flip on yourself. On anonymized sites, findings keyed on the session are skipped.

Duplicate Beacons

Mobile networks and service workers sometimes deliver the same pageview request more than once. A beacon with the same site, session and per-pageview nonce as one recorded in the last ten minutes is answered normally but counted only once. The tracker draws a random nonce for each landing pageview, so a reload or new page load is counted. A beacon without a well-formed nonce is always counted, and SPA navigation beacons and beacons without a session id are exempt.

Validated Visitors

A bot score measures suspicion. Validated measures proof: the session did something only a person does. The tracker reports the first click, key press or tap on each page as an engage event. A background validator (every minute) marks a view validated once its session has an interaction event and stayed on the page for at least two seconds, or showed a later interaction plus two distinct interaction types.

The overview API returns validated_visitors and the dashboard shows it next to unique visitors.

Proof-of-Work System

Every request for /ea.js embeds a WebAssembly blob and a challenge string. The browser must solve the challenge before sending beacons.

Why PoW Blocks Robots

Most analytics spam comes from scripts that fire HTTP requests directly — curl, python-requests, headless browsers running thousands of sessions. Proof-of-work stops them because:

How It Works

Content Security Policy on Your Site

If your site sends a Content-Security-Policy header, it must let the tracker reach your Echelon host: the host in script-src (to load /ea.js), in img-src (the /b.gif pixel) and in connect-src (for /c and /e), plus 'wasm-unsafe-eval' in script-src so the proof-of-work module can run:

script-src 'self' https://your-host 'wasm-unsafe-eval';
connect-src 'self' https://your-host;
img-src 'self' https://your-host

Without 'wasm-unsafe-eval' the tracker still works but sends no token, so its requests carry the +30 missing-token penalty instead of being discarded at ingest. A view with no token and no interaction is later raised to the bot threshold by the engagement gate (see Bot Defense).

Server Verification

The server tries all combinations of:

Returns "valid", "missing", or "invalid" — each adding different penalty points.

Burst Detection

Rate Limiting

Tracking endpoints (/b.gif, /e) and the challenge endpoint (/c, the current PoW challenge the tracker uses to refresh its token; public and CORS-enabled) are rate-limited per IP:

🗑️ "Did you know you can set ECHELON_BOT_DISCARD_THRESHOLD to immediately drop high-score requests before they're stored? This saves database space when you're confident in your scoring." -🦭

Cloudflare Integration

Set ECHELON_BEHIND_CLOUDFLARE=true to enable:

Referrer Classification

Incoming referrers are classified into categories:

TypeDomains
aiperplexity.ai, chat.openai.com, chatgpt.com, claude.ai, you.com, phind.com, copilot.microsoft.com, gemini.google.com, poe.com
searchGoogle (17 TLDs), bing.com, yahoo.com, duckduckgo.com, yandex.com/ru, ecosia.org
socialfacebook.com, twitter.com, x.com, reddit.com, linkedin.com, instagram.com, t.co
direct_or_unknownEverything else

Manual Exclusion

Via the admin dashboard or API, you can manually exclude specific visitor IDs from rollups. The session classifier adds its own exclusions labelled auto:<reason>, which daily maintenance removes after 30 days; manual ones are never removed automatically.

# Exclude
POST /api/bots/exclude
{ "visitor_id": "abc123", "label": "Known bot" }

# Re-include
DELETE /api/bots/exclude/abc123

Discard Threshold

Requests scoring at or over ECHELON_BOT_DISCARD_THRESHOLD are discarded before they are stored. The default is 50, the bot threshold. Set it higher (e.g., 80) to store the rows in between, or to 0 to store everything. Stored rows scoring 50 or more are still left out of all statistics; they stay visible, with their score detail, on the admin bots pages. A discarded request cannot be recovered, so 0 is the setting to audit new signals with (see Upgrading).


Installation Features API Reference Configuration Architecture Data Ownership & Open Access Portable Data MCP Server Telemetry Why ea.js?