Fraud & Suspicion Detection
Every lead is assigned a suspicion score (0-100) based on multiple signals that indicate bot activity, competitor scraping, or spam submissions.
Suspicion Signals
| Signal | Points | Trigger |
|---|---|---|
| Honeypot filled | +40 | Invisible field has a value (bots fill all fields) |
| Fast submit (<5s) | +30 | Form submitted in under 5 seconds (inhuman speed) |
| Quick submit (5-10s) | +10 | Form submitted between 5-10 seconds (suspicious) |
| No tracking session | +15 | No visitor session found for this visitor_id or IP |
| Zero engagement | +10 | Tracking session exists but engagement score is 0 |
| VPN suspected (high) | +15 | VPN suspicion score > 50 |
| VPN suspected (moderate) | +5 | VPN suspicion score > 20 |
| High form velocity (3+ in 24h) | +20 | 3 or more form submissions from same visitor in 24 hours |
| Moderate form velocity (2 in 24h) | +10 | 2 form submissions from same visitor in 24 hours |
| Competitor email domain | +25 | Email from a known competitor domain |
Maximum possible score: 100 (signals are additive but capped)
How It Works
Honeypot Field
The form builder (/form.js) injects an invisible field (HTML name="__pg_website"; the payload key sent to the server is __pg_hp) that is:
- Positioned off-screen (
left: -9999px) - Hidden with
aria-hidden="true" - Has
tabindex="-1"andautocomplete="off"
Real users never see or fill this field. Automated bots that fill all fields will trigger a +40 suspicion score. The server checks __pg_hp in the request body to detect honeypot-filled submissions.
When the honeypot is filled (bot lead), the submission is stored in BlockedIPLeads (same audit table as leads rejected because the IP was already blocked), then the IP is added to BlockedIPs. It is not written to Leads/LeadSubmissions, KV, or Analytics Engine, and is not sent to Make.com. This behaviour is controlled by the BOT_LEAD_DETECTION env var (see Settings / Environment config). When BOT_LEAD_DETECTION is off, honeypot-filled leads are processed as normal. In this codebase, “bot” is defined only by the honeypot (hidden field __pg_hp filled for WebSite source); the suspicion score is separate and used for scoring/CRM, not for reject/block.
Honeypot flow (when bot lead detection is on): (1) The lead is stored in BlockedIPLeads (dashboard Blocked IP leads list). (2) The lead IP is added to BlockedIPs with reason "Honeypot" (country and referer stored when available). The dashboard Blocked IPs and Blocked IP leads sections then show the blocked IP and the honeypot submission.
Time-to-Submit
The form records Date.now() when the form renders and calculates the elapsed seconds when the user submits. This is sent as __pg_tts.
- Under 5 seconds: +30 (bot speed — no human reads a form this fast)
- 5-10 seconds: +10 (suspicious — most real users take 15-30 seconds)
Multi-Form Tracking
countRecentSubmissions() queries the Leads table for submissions by the same VisitorId or ContactIP within the last 24 hours. The count is stored as FormSubmitCount on each lead.
- 2 submissions in 24h: +10
- 3+ submissions in 24h: +20
Note: Multiple submissions can be legitimate when a visitor inquires about multiple projects on the same page.
Competitor Email Detection
The system checks the email domain against a configurable list of known competitor domains. Leads from competitor emails receive +25 suspicion.
Data Stored
| Column | Type | Description |
|---|---|---|
SuspicionScore | REAL | Composite score 0-100 |
SuspicionReasons | TEXT | Comma-separated list of triggered signals |
TimeToSubmit | INTEGER | Seconds between form render and submit |
FormSubmitCount | INTEGER | Number of submissions by this visitor in 24h (including current) |
Usage in CRM
The suspicion score is included in the webhook payload to Make.com:
{
"suspicion_score": 55,
"suspicion_reasons": "fast_submit,no_tracking_session,form_velocity_high",
"time_to_submit": 3,
"form_submit_count": 4
}Sales teams can use this to:
- Auto-archive leads with suspicion > 70
- Flag for review leads with suspicion 30-70
- Prioritize leads with suspicion 0 + high engagement score
D1 Schema (Migration 006)
ALTER TABLE Leads ADD COLUMN FormSubmitCount INTEGER DEFAULT 1;
ALTER TABLE Leads ADD COLUMN SuspicionScore REAL DEFAULT 0;
ALTER TABLE Leads ADD COLUMN SuspicionReasons TEXT;
ALTER TABLE Leads ADD COLUMN TimeToSubmit INTEGER DEFAULT 0;