Quick start
- 1. Install. Download tw-form-guard-free-2.0.0.zip (Premium buyers: the Pro zip linked in the purchase email). In WordPress go to Plugins → Add New → Upload Plugin, then Activate. Elementor Pro must already be active; the plugin does nothing without it.
- 2. Leave the mode on Dry-run. Settings → TW Form Guard opens on the mode switch. Dry-run is the default: every submission is scored and logged, nothing is blocked, every Elementor action (email, redirect, webhook) still runs.
- 3. Send yourself a test. Submit your own contact form once. Within a minute the Log tab shows the row with verdict
passand an empty reason column. If no row appears, the form is not an Elementor Pro form or a caching layer is serving a stale page; see Outage and edge cases. - 4. Premium only: paste the license key. Settings → License → paste
TWFG-XXXX-XXXX-XXXX→ Verify. The status chip turns to Premium active and the AI judge switch unlocks. The switch is off until you read the privacy section and turn it on. - 5. Read the log for a week, then decide. Follow the Dry-run to Block guide below.
Multisite: activate per site, not network-wide. The plugin creates one small log table per site on activation and removes it on uninstall.
From Dry-run to Block
Block mode rejects a submission before Elementor runs its actions: no notification email, no redirect, no webhook, and the visitor sees a plain error under the form. That is exactly why you should not switch it on blind.
- Run Dry-run for at least 7 days, or 50 submissions, whichever comes later. Low-traffic sites need the calendar week to see the weekend spam waves; busy sites need the volume.
- Open the Log and filter on
flagged. These are the rows Block mode would have rejected. Read each one. Real buyers write short, imperfect English, use Gmail or mail.ru, and ask for prices, MOQ, specs or samples. Spam sells you SEO, loans, casino traffic, or pretends to be a royal family. - For every genuine row that was flagged, fix the cause, not the symptom. Read the reason column: a
keyword:hit means your own keyword is too broad;repeat_domain_x21on a buying team means you should add that domain to Trusted domains; a soft rule likebot_nameon a real person means you may want to disable that rule under Settings → Rules. - Zero genuine rows flagged? Switch to Block. The switch asks for confirmation. Keep an eye on the Log for the first week in Block: the
blockfilter now shows quarantined rows, and each has a Release button. - Something looks wrong later? Flip back to Dry-run. Nothing else changes; the log keeps recording.
For scale: on the first live day of one agrochemical exporter, 43 submissions produced 29 flags and 0 false positives. Across the 29,361 historical submissions we replayed, every genuine flag we found was fixable with a trusted domain, a rule toggle or a keyword edit.
Rule reference
Every value that can appear in the log's reason column. Hard rules decide on their own. Soft rules are skipped for trusted-domain senders. The toggle column is the key you see under Settings → Rules when you disable a rule.
Layer 0 · traps (recorded by the plugin, no toggle)
| Reason | Meaning |
|---|---|
honeypot | The hidden field that humans cannot see was filled in. Almost always a script. |
instant_submit_<n>s | The form was submitted less than 3 seconds after the page armed it; n is the elapsed seconds. Humans need longer to type. |
signal:no_js_traps | The submission reached Elementor's endpoint without the trap fields the page's JavaScript adds, meaning it skipped the browser entirely. Recorded as a signal in 2.0.0; signals are logged with the signal: prefix and never block on their own. |
Layer 1 · hard rules
| Reason | Toggle | Meaning |
|---|---|---|
invalid_email | invalid_email | The email field is filled but is not a valid address. A browser's built-in validation would have stopped a human, so this is a bot posting directly. |
template_spam | template | The message is, word for word, one of 28 known "hi, I wanted to know your price" templates in long-tail languages (Latin, Zulu, Hawaiian, Icelandic…). Major languages are deliberately not on the list. |
bbcode | bbcode | Forum markup such as [url=, [b], [link] or [img] in the message or name. Contact forms do not render BBCode; only bots paste it. |
multi_link | links2 | Two or more http:// or https:// links in the message. |
spam_industry | industry | A whole-word match on casino, betting brands (1win, mostbet, olymp, pin-up), jackpot, darknet, adult, viagra/cialis or forex. |
throwaway_email | throwaway | The sender's domain is a disposable mail service or a known spam farm (mailinator, 10minutemail, guerrillamail, ventura17.ru, registry.godaddy and about forty more). |
cyrillic_promo | cyr_link | The message contains a link and Cyrillic text together. Genuine Russian-speaking buyers rarely paste links; Russian-language promo bots always do. |
scam_phrase | scam | Classic advance-fee and lead-gen phrases: "dear beloved", "next of kin", "business proposal", "first page of google", "targeted traffic", "working capital", "followers and likes", "stop overpaying", and similar. |
repeat_domain_x<n> | repeat | The sender's non-free-mail domain has already appeared n times (15 or more) in your recent submissions and the message carries links or Cyrillic text. Link-free, non-Cyrillic messages from a repeated domain are treated as a buying team and waived. |
keyword:<word> | — | The message, name or email contains one of your own block keywords from Settings → Keywords. Case-insensitive substring match, so keep keywords specific. |
Layer 1 · soft rules (skipped for trusted-domain senders)
| Reason | Toggle | Meaning |
|---|---|---|
bot_name | botname | The name is a lowercase run-on of 8+ letters or ends in an underscore-suffix, and the submission is otherwise weak (message under 15 characters or invalid email). Real lowercase names with a real message are not flagged. |
random_string | random | The message is a single token longer than 10 characters with mixed upper and lower case and no spaces, dots or @. Typical of form-testing bots. |
empty_free_mail | emptyfree | Empty message from a free-mail address (gmail, yahoo, mail.ru, qq.com…). |
gibberish | gibberish | Seven or more consonants in a row somewhere in the message. |
repeat_email_x<n>repeat_name_x<n> | repeat | The same email address or the same name has already appeared n times (8 or more) in your recent submissions. Real buyers do follow up, which is why the threshold is 8, not 3. |
Layer 2 · AI judge (Premium, off by default)
| Reason | Meaning |
|---|---|
ai_judge | Traps and rules found nothing, the submission was sent to the relay, and the model answered SPAM. Blocks in Block mode. |
signal:ai_ham | The model answered HAM. The submission passed; logged for your review only. |
signal:ai_unavailable | The relay could not be reached, the license was not valid, the monthly quota or the daily budget was exhausted, or the model did not return a clear SPAM/HAM. The submission passed. |
signal:ai_ratelimited | The plugin's own limit was hit (10 AI calls per visitor address per minute, 60 per site per minute) and the relay was not called. The submission passed on rules alone. |
signal:corp_domain | Informational: the sender uses a non-free, non-disposable domain and wrote a link-free message of 15+ characters. Before 2.0.0 this alone skipped the soft rules; now it is only noted. |
whitelist | The sender's domain or exact address is on your trusted list. Soft rules and the AI judge were skipped; hard rules still ran. |
Whitelisting: a sender whose domain is in your Trusted domains list skips soft rules and the AI judge, but hard rules still apply. Since 2.0.0 a "corporate-looking" address is no longer trusted automatically; too many scams wear a corporate domain.
Privacy and the AI judge
Free: nothing leaves your WordPress site. No license checks, no update pings, no telemetry. The plugin writes a log table in your own database; you choose the retention period (default 90 days) and the WordPress privacy tools can export and erase a person's rows by email address.
Premium, AI judge off: the plugin checks the license state against guard.storefixkit.com about twice a day, sending the license key and your site URL. No form content is sent.
Premium, AI judge on: for each submission that traps and rules could not decide, the plugin sends to the relay over HTTPS:
- the message text, truncated to 1,500 characters;
- the sender's name (up to 190 characters) and email address as typed into the form;
- the license key, your site's host name, a timestamp and a one-time nonce, with an HMAC signature made from the license secret.
Not sent: phone numbers or any other form field, the visitor's IP address, your Elementor submission record, or anything about your WordPress users. The relay formats name, email and message into a short text, sends it to the model provider (Hangzhou DeepSeek Artificial Intelligence Co., Ltd., China), receives one word, and returns spam, ham or null to your site. The relay holds the content in memory for the duration of the request only. What it stores: a usage counter per license, a nonce hash for ten minutes, and a judgment log row with license id, verdict, latency and token counts. No name, email or message is written to disk on our side.
The plugin caches each verdict for 24 hours keyed by the message and address, so a duplicate submission is not sent twice. The AI switch is off after installation; the settings page repeats this notice next to the switch. For the full picture, including sub-processors and the open question about the provider's training terms, read the privacy policy and the DPA.
Quarantine and release
In Block mode a rejected submission is not thrown away. It is written to the TW Form Guard log with verdict block, the visitor sees a short error under the form, and no Elementor action runs, so you get no email and no webhook fires. That log row is the quarantine.
- Release on a row marks it as Released, shows you the full message so you can reply to the sender by hand, and allow-lists that exact email address so the same person is not blocked again. Release does not re-run Elementor actions.
- Also trust @domain is a separate checkbox next to Release, unchecked by default. Tick it only for a company domain you recognise; public mailbox and disposable domains are refused because trusting
gmail.comwould switch the soft rules off for half the internet. - The log supports search, verdict filters, paging, bulk release and bulk delete. Rows older than your retention setting (default 90 days; 0 keeps everything) are deleted by a daily task.
In Dry-run the same rows appear with verdict flagged; Elementor delivered them normally, so there is nothing to release, only something to read.
License, activation and moving sites
- Format.
TWFG-XXXX-XXXX-XXXX, sent by email after checkout and shown once on the confirmation page. Keep it private; anyone with the key can activate on their site and use your quota. - Activate. Settings → License → paste → Verify. The plugin contacts
guard.storefixkit.com, which binds the key to your site URL. One key, one production site. - Move to another site. On the old site click Deactivate under Settings → License before you remove the plugin; uninstalling alone does not release the binding. Then activate on the new site. If the old site is already gone, email support with the last four characters of the key and the new URL.
- Second activation while the first is active is refused with a clear message and an email to the checkout address, so a leaked key shows up quickly.
- Staging copies. A staging site with the same key does not activate automatically; it shows "license bound to another site" and runs as Free. That is intended.
- Status caching. The plugin caches a verified status for 12 hours. If the license server cannot be reached, the last known good status is kept for 72 hours, then the plugin drops to Free until the server answers again. Nothing is ever blocked because of a license problem.
- Renewal, failed payment, cancellation, refund. Handled by Stripe. A failed renewal keeps Premium working for a 7-day grace period; a cancellation stops Premium at the end of the paid period; a refund revokes the key immediately.
Outage and edge cases
- Relay down, DNS broken, timeout. The plugin waits at most 8 seconds, then passes the submission and logs
ai_unavailable. Your form's own response time is the only visible effect. Check status if it persists. - Monthly quota reached. The relay refuses further requests for the rest of the UTC month with reason
quota. The plugin treats that as unavailable, logssignal:ai_unavailable, and decides on traps and rules alone. No extra charge. - Our daily model budget tripped. The relay answers
nullwith reasonbudgetand the submission passes. This is our cost fuse, not yours; it resets at midnight UTC. - Model answers something other than SPAM or HAM. Treated as unavailable. The relay never forwards model text to your site, so a prompt-injection attempt in a message cannot reach your log. After three consecutive failures the plugin stops calling the relay for ten minutes.
- Full-page cache older than a day. Since 2.0.0 the timing trap is armed by JavaScript at page load, so a cached page cannot make real visitors look stale. If you use a cache that strips inline scripts (some "guest mode" optimisers), the trap fields are missing and submissions are logged with
no_js_traps; whitelist the plugin's small script in your optimiser. - Behind Cloudflare or another proxy. By default the plugin records the direct connection address. Turn on "Site is behind a trusted proxy" in Settings if your server sits behind Cloudflare, so repeat-visitor rules see the real address instead of one proxy IP.
- Popup forms. The source page of a popup submission is recorded through Elementor's own referrer metadata; nothing in your Elementor submission tables is modified by the plugin.
- Uninstall. Removes the log table, all
twfg_*options and transients on every site of a network. Deactivating keeps them.
Benchmark methodology
The accuracy figures on this site come from one fixed benchmark, run on 2 September 2026, and we quote it exactly rather than rounding it into a percentage:
88 real submissions taken verbatim from production contact forms of Chinese exporter sites: 69 spam, 19 genuine. Result: 63 of 69 spam caught, 19 of 19 genuine inquiries kept.
- Where the sample comes from. The 69 spam rows are the first-day dry-run flags of one agrochemical exporter (40), the spam that slipped past the rules on that day (12), and 17 scams written on corporate-looking domains collected across our sites. The 19 genuine rows are 12 real inquiries from the same exporter plus 7 protected cases from other sites: a Pakistani mother buying raw material for her daughter's business, a Russian buyer pasting a valve model list, and similar short, imperfect but real messages.
- What was measured. Traps, rules and the AI judge together, with the production prompt and parameters (deepseek-v4-flash, no thinking, temperature 0, five-token answer). Latency: 1.35 s median, 1.76 s at the 95th percentile.
- What was not caught. Three of the six misses are high-quality scams that read like a B2B self-introduction (a fake "Mclaren Group" tender, a "GCX BIDCO" vendor registration, a Dubai royal-family partnership). No rule or prompt we tried catches them without also hitting real buyers, so they are left to you: release or keyword-block from the log.
- What this does not prove. 88 rows is a small sample from one industry. Your traffic will differ. This is why Dry-run is the default and why nothing on this site is worded as a guarantee. Earlier and larger replays (29,361 historical submissions across nine sites) shaped the rules but were not labelled by hand, so we do not quote accuracy from them.
We will re-run the benchmark with a larger labelled set before changing the prompt or the model, and update this section with the date and the new numbers.
Changelog
2.0.0 — September 2026
- No API keys in the plugin. The AI judge now goes through the StoreFixKit relay with a signed request, plugin-side rate limits and a 10-minute circuit breaker; the model provider's key never leaves our server. Previous versions' key options are removed on upgrade.
- Timing trap armed by JavaScript at page load; the old server-rendered timestamp that misfired on cached pages is gone, and so is the
stale_formreason. Newno_js_trapssignal. - Popup-form source page recorded through Elementor's own metadata; the plugin no longer writes to Elementor submission tables.
- Corporate-looking addresses are no longer trusted automatically; only your Trusted domains list is.
- Release allow-lists the exact sender address only; trusting the whole domain is a separate checkbox that refuses public mailbox providers.
- License verified with the license server (12-hour cache, 72-hour grace), not by pattern.
- Log retention setting (default 90 days) with daily cleanup, WordPress privacy exporter and eraser, trusted-proxy setting for visitor addresses.
- Bounded repeat-sender lookups (90-day window, cached) for sites with large submission tables.
- Field detection uses Elementor field types before falling back to field names, so a
mailing_addressfield is no longer mistaken for the email. - Works without the mbstring extension.
- Rebuilt settings and log screens: first-run guidance, confirmation before Block, help text on every control, license status chip, searchable and paged log with verdict badges and full-message view, mobile layout.
- Clean uninstall, readme.txt, single version number, separate Free and Premium builds.
1.5 — August 2026
- Gambling brands mostbet, olymp and pin-up added to
spam_industry; throwaway list gains ventura17.ru, 321mail, govtopshop and xmc.pl after the first live dry-run day (43 rows, 29 flagged, 0 false positives).
1.4
repeat_domainwaived for link-free, non-Cyrillic messages after a real 10-person buying team tripped it.
1.3
repeat_domainpromoted to a hard rule (threshold 15) so spam farms on corporate-looking domains cannot hide behind the whitelist.
1.2
bot_nameonly fires on otherwise weak submissions; repeat thresholds raised from 3 to 8 because real buyers follow up.
1.1
invalid_email,template_spam(28 languages) andrepeat_domainadded after replaying 29,361 fleet submissions; scam and throwaway lists extended.
1.0
- First release: honeypot, timing trap, ten rules, dry-run and block modes, locked to a 182-submission baseline (169 blocked, 13 passed).