Skip to content

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

SignalPointsTrigger
Honeypot filled+40Invisible field has a value (bots fill all fields)
Fast submit (<5s)+30Form submitted in under 5 seconds (inhuman speed)
Quick submit (5-10s)+10Form submitted between 5-10 seconds (suspicious)
No tracking session+15No visitor session found for this visitor_id or IP
Zero engagement+10Tracking session exists but engagement score is 0
VPN suspected (high)+15VPN suspicion score > 50
VPN suspected (moderate)+5VPN suspicion score > 20
High form velocity (3+ in 24h)+203 or more form submissions from same visitor in 24 hours
Moderate form velocity (2 in 24h)+102 form submissions from same visitor in 24 hours
Competitor email domain+25Email 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" and autocomplete="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

ColumnTypeDescription
SuspicionScoreREALComposite score 0-100
SuspicionReasonsTEXTComma-separated list of triggered signals
TimeToSubmitINTEGERSeconds between form render and submit
FormSubmitCountINTEGERNumber of submissions by this visitor in 24h (including current)

Usage in CRM

The suspicion score is included in the webhook payload to Make.com:

json
{
  "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)

sql
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;