> ## Documentation Index
> Fetch the complete documentation index at: https://formpaste.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Preventing spam

> How to get the most out of Formpaste's heuristic spam filtering - no puzzle for your visitors.

Formpaste never asks a visitor (or an agent submitting on their behalf) to solve a
CAPTCHA. That's not a missing feature - it's the point. This guide covers how the
heuristic filtering actually works and what you can do to get the most out of it,
without ever adding a puzzle to your form.

## Why no CAPTCHA puzzles

CAPTCHAs trade spam reduction for friction: they slow down or block real visitors, they
break for screen readers and other assistive tech, and increasingly they don't even stop
bots - modern solving services defeat most of them cheaply. They also block the exact
audience Formpaste is built for: an AI agent legitimately submitting a form on a human's
behalf shouldn't have to solve a puzzle designed to detect it as non-human. Instead,
every submission is scored by cheap, inline heuristics - no external API call, no
per-submission cost, no visible challenge.

See [Spam filtering](/docs/features/spam-filtering) for the full mechanics; this guide is
about using them well.

## 1. Wire up the honeypot (`botcheck`)

The single highest-value thing you can do is include the `botcheck` field and hide it
properly:

```html theme={null}
<input type="text" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
```

Real visitors never see or fill this field in, so if it arrives with any value, the
submission is quarantined. Two details matter for this to actually work:

* **Hide it with CSS (`display:none`), not `type="hidden"`.** Unsophisticated bots that
  specifically skip `type="hidden"` inputs will still fill in a `text` input that's
  merely styled invisible - that's what catches them.
* **Don't remove it "because nothing fills it in."** An empty `botcheck` field submitting
  successfully alongside real data is exactly what should happen for a human visitor —
  it's silent and free. It only matters when something else - a bot, or a browser
  extension autofilling every field it can find - populates it.

Every framework snippet on the [Framework snippets](/docs/framework-snippets) page already
has `botcheck` wired in; if you hand-rolled your form instead of copying one, add it.

## 2. Add the timing field (`_ts`)

An optional `_ts` field carries the timestamp your form was rendered. A submission that
arrives implausibly fast - faster than a human could plausibly have read the form and
typed a response - scores as suspicious. This is a cheap way to catch scripted
submissions that skip rendering entirely and just POST a payload.

```html theme={null}
<input type="hidden" name="_ts" value="1720700000000">
```

Set its value to the current timestamp at render time (e.g. `Date.now()` in
JavaScript, or the server-render time for a static page).

## 3. Let the rest run automatically

Once `access_key` is wired, the remaining heuristics need no setup and run on every
submission:

* **Link density** - a message packed with multiple URLs is scored as likely spam.
* **Rate limiting** - repeated submissions from the same source in a short window are
  throttled, independent of content.
* **Disposable / duplicate email detection** - submissions from disposable-email
  providers, and near-identical repeat submissions, are flagged. If your use case
  legitimately expects disposable addresses, turn on `allow_disposable_email` for that
  form under [Domain allowlist](/docs/features/custom-domains) rather than fighting the
  default.

## 4. Lock down the domain allowlist

`access_key` is designed to sit in public page source - it isn't a secret, and CORS on
`/submit` is intentionally permissive so any origin can POST. The **allowed domains**
list on each form is the actual boundary that stops someone from copying your key and
routing unrelated traffic (including spam) into your inbox. An empty allowlist accepts
any origin - fine while you're setting up, but worth locking to your real domain(s)
once the form is live. See [Domain allowlist](/docs/features/custom-domains).

## 5. Treat quarantine as a review queue, not a black hole

Nothing that reaches a valid `access_key` is silently dropped. Anything that scores as
suspicious lands in the form's **Spam** tab in the
[dashboard](https://app.formpaste.com) instead of your inbox - check it
periodically for false positives. On Free, quarantine is read-only, so a legitimate
submission that was misflagged needs the visitor to resubmit once you've fixed whatever
tripped the heuristic (usually a `botcheck` wiring issue or an overly strict
disposable-email setting). [Pro](/docs/features/quarantine-rescue) adds a one-click "not
spam" rescue that re-delivers it directly.

## What this doesn't do

Heuristic scoring reduces spam volume; it doesn't guarantee zero. A determined,
well-funded attacker who renders your form in a real headless browser, waits a plausible
amount of time, and sends one submission at a time from a residential IP can still get
through - the same way they could defeat a CAPTCHA. The goal is raising the cost of
spamming a form well above the cost of the alternative (a real inbox with no filtering
at all), not building an impenetrable wall.

<CardGroup cols={2}>
  <Card title="Spam filtering" icon="shield" href="/docs/features/spam-filtering">
    What's checked and how scoring works.
  </Card>

  <Card title="Quarantine" icon="box-archive" href="/docs/features/quarantine">
    Where flagged submissions go, and how to review them.
  </Card>

  <Card title="Domain allowlist" icon="globe" href="/docs/features/custom-domains">
    The real security boundary behind a public access\_key.
  </Card>

  <Card title="Error reference" icon="triangle-exclamation" href="/docs/reference/error-table">
    Including `rate_limited` and other spam-adjacent codes.
  </Card>
</CardGroup>
