Order of operations
OSpam_Checker::run() runs each check in order. Allowlist matches short-circuit immediately; hard-fail checks (honeypot, rate limit, bot UA, geo, blocklist) block the submission outright; soft checks add to a score that is compared to ospam_score_threshold.
Hard-fail checks
| Check | Trigger | Notes |
|---|---|---|
| Allowlist | IP/email/domain in wp_ospam_allowlist | Skips everything else |
| Honeypot | Invisible ospam_confirm_email field is non-empty | Auto-blocklist after N hits in 24h if enabled |
| Rate limit | More than ospam_ratelimit_max (default 5) submissions per IP per ospam_ratelimit_window seconds (default 3600) | Transient-backed |
| User-agent | Empty or known-bot UA | Opt-in |
| Geo | Country code in ospam_geo_blocked_countries | Lookup via ip-api.com, 24h cache |
| Blocklist | IP/email/domain/keyword in wp_ospam_blocklist | Substring match for keywords |
Soft (score-based) checks
| Check | Score added | Notes |
|---|---|---|
| Timing | +80 if submitted faster than ospam_timing_threshold (default 2s) | JS sets ospam_ts on page load |
| Disposable email | configurable | Domain match against ~500 known throwaway providers |
| Link density | +20 / +50 / +80 | 1-2 / 3-4 / 5+ URLs across all fields |
| Pattern | up to +40 | 5+ repeated chars, ALL CAPS >20 chars, fields >2000 chars |
| Keyword | sum of weighted matches | JSON list in ospam_score_keywords |
| StopForumSpam | +60 | Opt-in, only if email frequency > 5 |
| Akismet | +70 | Opt-in, requires Akismet API key |
Honeypot field
A visually-hidden <input name="ospam_confirm_email"> wrapped in a div with position:absolute;left:-9999px and aria-hidden="true". Real users never see it; bots that fill every field do.

