Our Bot Filter Has No CAPTCHA
Every lead form gets spam eventually. Bots scrape the page, parse the fields, and fire fake submissions. We don't use CAPTCHA. CAPTCHAs slow down real users and require solving a problem that can be handled more cheaply. We use a honeypot.
What a Honeypot Does
A honeypot is a form field that exists in the HTML but is invisible to real users. Bots typically fill every field they find. Real users can't see the field, so they don't. When a submission arrives with the honeypot field populated, we reject it silently without burning resources on emails or database writes.
The React Wrinkle
In a traditional HTML form, the honeypot is a simple server-side check. In a React controlled form, the field has to be wired to component state:
const [honeypot, setHoneypot] = useState("");
<div aria-hidden className="absolute left-[-9999px] h-0 w-0 overflow-hidden">
<label htmlFor="company">Company</label>
<input
id="company"
name="company"
type="text"
tabIndex={-1}
autoComplete="off"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
/>
</div>
Each attribute on the wrapper and input has a specific job.
aria-hidden on the wrapper tells screen readers to skip the entire subtree. Screen reader users never encounter the field, which matters because they navigate by keyboard.
tabIndex={-1} removes the input from the tab order. A sighted user navigating with a keyboard tabs from name to email to submit without ever landing on this input.
autoComplete="off" prevents the browser from filling it with saved data. Without this, a browser that has a saved "Company" value might populate it for a real user who then submits with the honeypot filled.
The positioning is absolute left-[-9999px] rather than display: none or visibility: hidden. Some bots parse HTML and skip fields that are hidden via CSS. Off-screen positioning renders the field in the DOM with no visible space, making it accessible to a bot reading raw markup without applying styles.
The Field Name Is the Bait
We named the field "Company." On a B2B-adjacent form, that's the kind of qualifying field scrapers expect. A bot scanning the field names fills name, email, and company. A real user fills name and email, sees no company field anywhere on the page, and moves on.
The Server Check
The honeypot value travels in the POST body alongside the other fields. The server checks it immediately after Zod validation, before any email sends or Airtable writes:
if (parsed.data.honeypot) {
return NextResponse.json({ success: true });
}
Two deliberate choices here. First, the check is early. A bot that trips the honeypot burns nothing on our side: no Resend calls, no Airtable record, no PostHog event. Second, the response is { success: true } with a 200 status, not a 400 or 403. A 400 signals a problem worth retrying. A 200 signals completion. The bot's client marks the submission done and moves on.
Why Not CAPTCHA
CAPTCHAs add friction to every real user in exchange for blocking automated ones. For the kind of form spam we see, a honeypot handles it with no user-facing cost. If we start seeing bots that parse CSS and skip off-screen fields, the calculus changes. Until then, the $0 solution handles it.
Both our Operations Engine audit form and the /impeccable skill download form use this pattern. Same implementation, same early server check, same silent-success response.