Documentation · version 2.0.0

TW Form Guard docs

Everything the settings page does not explain: how to go live safely, what every log reason means, what leaves your site and when, and what happens when something on our side fails.

Applies to TW Form Guard 2.0.0 (Free and Premium). Requires WordPress 6.0+, PHP 7.4+ and Elementor Pro with the Forms module (tested with Elementor Pro 4.1). Last updated 3 September 2026.

Quick start

  1. 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. 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. 3. Send yourself a test. Submit your own contact form once. Within a minute the Log tab shows the row with verdict pass and 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. 4. Optional: switch on the AI judge. It is off after installation and it has two backends. Free and Premium: choose "Use my own AI provider" and paste a Base URL, an API key and a model name — the submission then goes from your server straight to that provider and never reaches us; see Choosing your AI backend. Premium: paste your license key under Settings → License → Verify (TWFG-XXXX-XXXX-XXXX), the status chip turns to Premium active, and hosted judging needs no key of your own. Either way, read the privacy section before you turn it on.
  5. 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_x21 on a buying team means you should add that domain to Trusted domains; a soft rule like bot_name on 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 block filter 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)

ReasonMeaning
honeypotThe hidden field that humans cannot see was filled in. Almost always a script.
instant_submit_<n>sThe 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_trapsThe 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

ReasonToggleMeaning
invalid_emailinvalid_emailThe 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_spamtemplateThe 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.
bbcodebbcodeForum markup such as [url=, [b], [link] or [img] in the message or name. Contact forms do not render BBCode; only bots paste it.
multi_linklinks2Two or more http:// or https:// links in the message.
spam_industryindustryA whole-word match on casino, betting brands (1win, mostbet, olymp, pin-up), jackpot, darknet, adult, viagra/cialis or forex.
throwaway_emailthrowawayThe 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_promocyr_linkThe message contains a link and Cyrillic text together. Genuine Russian-speaking buyers rarely paste links; Russian-language promo bots always do.
scam_phrasescamClassic 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>repeatThe 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)

ReasonToggleMeaning
bot_namebotnameThe 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_stringrandomThe 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_mailemptyfreeEmpty message from a free-mail address (gmail, yahoo, mail.ru, qq.com…).
gibberishgibberishSeven or more consonants in a row somewhere in the message.
repeat_email_x<n>
repeat_name_x<n>
repeatThe 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 (off by default; free on your own provider key, or hosted with Premium)

ReasonMeaning
ai_judgeTraps and rules found nothing, the submission was sent to the configured backend (your own provider, or our relay), and the model answered SPAM. Blocks in Block mode.
signal:ai_hamThe model answered HAM. The submission passed; logged for your review only.
signal:ai_unavailableThe endpoint could not be reached, your own provider refused the call (bad key, no credit, its own rate limit), 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_ratelimitedThe plugin's own limit was hit (10 AI calls per visitor address per minute, 60 per site per minute) and no call was made. The submission passed on rules alone.
signal:corp_domainInformational: 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.
whitelistThe 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.

Choosing your AI backend

Layer 2 has two backends and you pick one on the settings page. The prompt, the one-word answer and the fail-open behaviour are identical; what differs is who holds the model key and where the submission travels.

 Use my own AI providerHosted judging
In which buildFree and PremiumPremium only
Request pathYour WordPress server → the endpoint you configuredYour WordPress server → our relay → the model provider
Does StoreFixKit see the submission?No. We do not route, see or store itYes, in memory for the length of the request
What you set upBase URL, API key, model nameA license key
What it costsYour provider's per-token price$79 per site per year, 10,000 judgements a month

1. Get a key from a provider

Any service that exposes an OpenAI-compatible /chat/completions endpoint over https works. The steps are the same everywhere: create an account on the provider's developer platform (not the consumer chat app), open the API-keys page, create a key, and add credit if the provider is prepaid. Copy the key once — most providers show it only at creation. Providers that publish such an endpoint include OpenAI, DeepSeek, Moonshot (Kimi), Zhipu, Qwen, Groq and OpenRouter; each one's own API documentation gives the exact Base URL and the current model names, and those are the values to trust, not this page.

You can also point the plugin at a model you run yourself — Ollama, vLLM or LM Studio all speak the same API — in which case the submission never leaves your own network.

2. Fill in three fields

FieldWhat to enter
Base URLThe root of the API, without /chat/completions — the plugin appends that itself. It must start with https://.
API keyThe key from step 1. It is sealed with your site's own WordPress salts before it is written to the database, so a stolen database dump without wp-config.php does not expose it.
Model nameThe model id exactly as the provider writes it. Small, fast, cheap models are the right choice here: the task is one word of classification, not writing.

Working combinations, as examples of the shape:

  • https://api.deepseek.com/v1 + deepseek-v4-flash — the endpoint and model our own September 2026 benchmark ran against.
  • https://api.openai.com/v1 + gpt-4.1-mini.
  • Your own machine, for example http://localhost:11434/v1 (Ollama) or http://127.0.0.1:8000/v1 (vLLM), plus whatever model name your runtime serves. Nothing leaves your server.

https on the public internet; http only at home. A Base URL that crosses the internet must start with https:// — an API key and your visitors' messages must never travel in clear text. A model you host yourself is the exception: http://localhost:11434/v1 (Ollama), http://127.0.0.1:8000/v1 (vLLM) and a private LAN address such as http://192.168.1.50:1234/v1 (LM Studio) all work as they are, because the request never leaves your machine or your network. Any other http:// address is refused and the AI layer stays off.

3. Run the self-test

The settings page can score your chosen backend against twelve verbatim production messages: six spam that reached real client inboxes (a casino link drop, a Russian darknet promo, an SEO pitch, a fake "Reliance Group" enquiry, a template translation, a fake vendor registration) and six genuine enquiries that must never be blocked (an MOQ question from Malaysia, a Dutch order for gibberellic acid, a Spanish quotation request, a Pakistani CIF question, a mother buying soap-making raw material, a Russian valve model list).

  • Read the genuine column first. Six of six kept is the result that matters. A model that misjudges a real enquiry in this set will misjudge real enquiries on your site, and a lost customer costs more than a spam message delivered. If it is not 6/6, pick another model, or stay in Dry-run.
  • Then the spam column. Five or six of six caught is a good model for this job. Our reference configuration catches 63 of 69 on the larger benchmark; the twelve-message set is a smoke test, not a re-run of that benchmark.
  • "Unavailable" is a configuration answer, not a model answer. It means the endpoint could not be reached, the key was refused, or the reply was not exactly SPAM or HAM. Check the Base URL, the key and the model id.
  • Expect small run-to-run differences. Even at temperature 0 the same model can score a message differently between runs; in our own benchmark repeats moved the spam count by up to three out of 69. One flipped spam verdict between two runs is noise; a genuine enquiry flipping is not.
  • The self-test bypasses the 24-hour verdict cache, so every run costs twelve real calls on your key.

4. What it costs to run

One judgement is about 150 input tokens and a single output token, and only the submissions that traps and rules could not decide are ever sent — on a typical B2B site that is a handful a day, not every visitor. At the per-million-token prices small models were advertised at when we measured this in September 2026, ten thousand judgements came to well under one US dollar, so a normal month is cents. Prices are your provider's and they change: read their current price list, and set a spending cap in their console if they offer one. We do not resell, meter or mark up anything on this path and we never see the invoice.

5. It fails open, always

If the provider is unreachable, times out (the plugin waits at most 8 seconds), refuses the key, runs out of your credit, or answers anything other than SPAM or HAM, the submission is delivered and the log row says signal:ai_unavailable. Nothing is ever lost because a model had a bad day. After three consecutive failures the plugin stops calling the endpoint for ten minutes and decides on traps and rules alone, then tries again. Identical resubmissions reuse the cached verdict for 24 hours instead of paying for a second call.

The backend choice, the endpoint details and the license are separate settings, and turning the AI judge off entirely leaves the traps and rules running exactly as before.

Privacy and the AI judge

Free, AI judge off: 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.

Any build, AI judge on your own provider key: for each submission that traps and rules could not decide, the plugin posts the sender name, the email address, the message text (cut to 1,500 characters) and your model name over https to the Base URL you configured, with your key in the Authorization header. The request goes from your web server to that provider directly. It does not pass through guard.storefixkit.com: we do not route it, see it, store it or count it, and we add no sub-processor to your data flow. Your agreement about that text is with the provider you chose, and their published policies decide what happens to it — read them before you switch this on. Nothing else is sent: not the visitor's IP address, not your other form fields, not your uploads, not your WordPress users. Still nothing goes to StoreFixKit in this mode, in either build.

Premium, hosted judging selected but 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, hosted judging 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 judgement 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 — this applies to both backends. The AI switch is off after installation; the settings page repeats this notice next to the switch. For the full picture, including which sub-processors exist in each mode and what each provider's own policies do and do not say, 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.com would 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

  • Endpoint down, DNS broken, timeout. Whichever backend you use, 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. After three consecutive failures it stops calling for ten minutes. For hosted judging, check status if it persists.
  • Your own key refused, out of credit, or rate-limited by the provider. Same result: the call fails, the submission is delivered, the log says signal:ai_unavailable. The plugin cannot tell you why the provider said no, so check the provider's own console if the log fills with it.
  • Monthly quota reached (hosted judging). The relay refuses further requests for the rest of the UTC month with reason quota. The plugin treats that as unavailable, logs signal:ai_unavailable, and decides on traps and rules alone. No extra charge. On your own key we impose no quota at all.
  • Our daily model budget tripped (hosted judging). The relay answers null with reason budget and 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. Only the two accepted words are ever read — everything else the model returns is discarded rather than forwarded to your site or your log — so a prompt-injection attempt written into a message has nothing to reach. This holds on both backends.
  • 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.
  • It does not transfer to another model. These numbers belong to deepseek-v4-flash with the parameters above. Point the plugin at a different provider or model and the score changes — sometimes for the better, sometimes not. The twelve-message self-test exists so you can measure your own choice instead of assuming ours; repeats of the same configuration also moved the spam count by up to three, so treat single-run differences of one or two as noise.

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

  • The AI judge has two backends and the site owner picks one. Use my own AI provider (in every build, including the free one) calls any OpenAI-compatible https endpoint straight from the site, with the owner's own key sealed using the site's WordPress salts; nothing reaches StoreFixKit. Hosted judging (Premium) goes through the StoreFixKit relay with a signed request, so there is no key to obtain and the model provider's credentials never leave our server. Both paths share the plugin-side rate limits, the 24-hour verdict cache and the 10-minute circuit breaker, and both fail open.
  • Self-test: twelve verbatim production messages (six spam, six genuine) scored against whichever backend is configured, so an owner can see the quality of their chosen model before trusting it.
  • 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_form reason. New no_js_traps signal.
  • 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_address field 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_domain waived for link-free, non-Cyrillic messages after a real 10-person buying team tripped it.

1.3

  • repeat_domain promoted to a hard rule (threshold 15) so spam farms on corporate-looking domains cannot hide behind the whitelist.

1.2

  • bot_name only 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) and repeat_domain added 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).