Read the full writeup at https://seangeng.com/writing/block-disposable-emails.md and implement it in my project.
It covers: Block disposable emails at signup. Throwaway inboxes are how spam, trial-abuse, and fake accounts get in. Checking the email domain against a maintained disposable-domains list at signup takes minutes and pairs well with Cloudflare Turnstile.
Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.
Paste into Claude Code, Codex, Cursor, or any agent. view raw .md
If you let anyone sign up with any email, a chunk of your "users" will be
throwaway inboxes: mailinator.com, 10minutemail.com, and hundreds more.
They're how people farm free trials, dodge bans, and pad your numbers with
accounts that never convert. @venelinkochev
put it well: block these at signup, add Cloudflare Turnstile for the bots, and
signup quality jumps for very little work.
The trick is almost embarrassingly simple. There's a community-maintained list
of disposable domains, and you check the signup email's domain against it.
How it works
The disposable/disposable-email-domains
repo publishes a domains.json: tens of thousands of known throwaway domains,
updated continuously. Fetch it, cache it for a day, drop it in a Set, and
membership-test the domain after the @:
const LIST = "https://rawcdn.githack.com/disposable/disposable-email-domains/master/domains.json";let cache: Set<string> | null = null;let fetchedAt = 0;async function disposableDomains() { if (cache && Date.now() - fetchedAt < 86_400_000) return cache; // 1 day const res = await fetch(LIST); cache = new Set<string>(await res.json()); fetchedAt = Date.now(); return cache;}export async function isDisposableEmail(email: string) { const domain = email.split("@")[1]?.toLowerCase().trim(); if (!domain) return false; return (await disposableDomains()).has(domain);}
Then guard the signup handler:
if (await isDisposableEmail(email)) { return new Response("Please use a permanent email address.", { status: 422 });}
A Set lookup is O(1), and the daily-cached fetch means you're not hammering
the CDN. On an edge runtime like Workers, keep the cache in module scope (or KV)
so it survives between requests.
Do it server-side
The check belongs on the server, at the signup endpoint. Never trust a
client-only check, since anyone can skip it. The demo above runs in your browser
because it's a demo; in production this is a few lines in your API route.
Layer it
No single signal is enough on its own, so stack a few cheap ones. Turnstile (or
hCaptcha) filters out bots before they submit. The disposable-domain block
filters out throwaway humans. And a verification email still requires a real,
owned inbox at the end. Each is minutes of work, and together they cut a
surprising amount of junk.
What I'd flag: use a maintained list, not a hand-rolled regex of domains you
remember. The regex goes stale the day after you write it; the list doesn't.
I put a live checker and the copy-paste guard on the Freebies page.