# Stop Fake SaaS Signups: How to Block Disposable Email Addresses at Signup

**A static list of disposable email domains is out of date the moment you ship it — new throwaway-mail domains go live continuously, and a list baked into your codebase is only ever a snapshot of the day you last updated it. The fix isn't a longer list; it's running disposable and role-account detection as part of a live validation call at signup, then gating on a risk score and specific signals instead of hard-blocking every address that isn't a clean "valid."**

This is written for the person who owns the signup handler, not the person cleaning a marketing list afterward: product and platform engineers trying to keep fake accounts, free-trial abuse, and bot registrations out of a registration form without turning away real users who happen to trip a filter.

## Why Blocking Disposable Email Addresses at Signup Breaks Down at the Blocklist

The standard first move is a list: pull a public GitHub repo of known disposable domains, check the signup email's domain against it, reject a match. It works — for the domains on the list, as of the day the list was last pulled.

The mechanism that breaks this: disposable-mail providers don't run on a fixed set of domains. Anyone can register a new domain, point MX records at a disposable-inbox service, and start handing out addresses on it — for a few dollars and a few minutes. A domain that's brand new this morning isn't on any list yet, by definition. In the gap between updates, every new domain is invisible to the check. A list you maintain yourself inherits this problem directly: it's exactly as current as your last deploy, and it competes with everything else on your backlog for that update.

This isn't a reason to give up on domain-based detection — it's the reason it belongs in a live check rather than a file in your repo.

## Where the Check Belongs: Inline at the Form vs. Async After Signup

**Inline, at form submit, before the account is created.** The check runs as part of the signup request itself. This is the only place a check can actually stop a bad signup from becoming a row in your database. PingValid states a single lookup completes in under 300 ms — its own published figure, not an independently audited SLA — which is what makes a synchronous call realistic rather than a UX problem.

One detail if latency is tight: disposable and role-account detection don't require the slower SMTP step. They run alongside syntax, DNS and MX at the Quick tier; the SMTP mailbox check is what Standard adds, and catch-all detection is what Deep adds beyond that. If disposable/role detection and basic domain sanity are all your signup form needs, you don't have to pay for the SMTP round trip to get it.

**Async, after the account already exists.** Real work — but a different question than "should this signup be allowed." By the time an async check resolves, the account exists, a session may be active, and any resource the signup was after (a trial credit, an invite slot) may already be spent. Async is right for enriching or re-checking over time, not for stopping the signup itself.

## The Trade-off: Gate on Risk Score, Don't Hard-Block on Anything Less Than "Valid"

Here's the mistake that costs you real users: treating "verdict is not exactly `valid`" as the block condition.

PingValid returns one of four verdicts — `valid`, `risky`, `invalid`, `unknown` — plus a 0–100 risk score and per-check evidence. It's tempting to write `if (verdict !== 'valid') reject()`. It isn't right. A `risky` verdict is a real address with a genuine ambiguous signal behind it — a catch-all domain, a newly-registered domain with thin DNS history, a temporary SMTP deferral. None of those mean "fake." A signup form that hard-rejects every `risky` verdict rejects real people along with fake ones, and the false positive looks identical to the true one until a human investigates.

Gate on the risk score and the specific signals instead:

- **`invalid`, and a disposable-domain hit,** are worth rejecting outright. A mailbox built to expire isn't going to receive a password reset or a billing notice six months from now.
- **A `risky` verdict, or a risk score above your tolerance,** is a soft-gate signal: let the signup through into a lower-trust state — hold the trial credit, require email confirmation before activation, flag for review — rather than turning the person away.
- **`unknown` means the check couldn't reach a conclusion,** not that the address is bad. Silently rejecting it punishes the user for a transient DNS or SMTP hiccup on the checking side. Let the signup through and re-check later if your flow allows.

Worth knowing: PingValid's pipeline treats disposable and role-account detection as tiers that **feed the risk score**, not as automatic-fail checks the way a missing MX record or a rejected SMTP handshake are. The product isn't handing you a binary "block this" flag — it's handing you evidence, and the block/no-block line is a decision your signup flow makes.

## Implementation: Gating a Signup Handler on Risk Score

```bash
curl -X POST https://pingvalid.com/v1/validate \
  -H "Authorization: Bearer pv_live_…" \
  -H "Content-Type: application/json" \
  -d '{"email":"someone@example.com"}'
```

`tier` is an optional field on that body — `quick`, `standard`, or `deep`, defaulting to `standard`. A trimmed response:

```json
{
  "email": "alice@acme.com",
  "normalized": "alice@acme.com",
  "verdict": "valid",
  "risk_score": 0,
  "tier_results": {
    "t1_syntax": { "outcome": "pass" },
    "t4_disposable": { "outcome": "pass" },
    "t5_role": { "outcome": "pass" },
    "t6_smtp": { "outcome": "pass" }
  },
  "cached_hit": false,
  "checked_at": "2026-07-12T09:30:00+00:00",
  "request_id": "req_5f2c…",
  "duration_ms": 212,
  "cost_credits": 1
}
```

Per-check results live under `tier_results`, keyed by tier id — `t4_disposable` and `t5_role` are the two to read here, each with an `outcome` of `pass`, `fail`, or `unknown`.

```javascript
app.post('/signup', async (req, res) => {
  const { email, password } = req.body;

  const check = await fetch('https://pingvalid.com/v1/validate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PINGVALID_API_KEY}`, // scope: validate:single only
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(), // retries are spend-safe
    },
    body: JSON.stringify({ email }),
  }).then(r => r.json());

  const { verdict, risk_score, tier_results } = check;
  const disposable = tier_results.t4_disposable?.outcome === 'fail';
  const roleAccount = tier_results.t5_role?.outcome === 'fail';

  const meta = { verdict, risk_score, disposable, roleAccount };

  // Hard-block: the one thing worth rejecting outright before an account exists.
  if (verdict === 'invalid' || disposable) {
    return res.status(422).json({ error: 'Please use a different email address.' });
  }

  // Soft-gate: a real signal, not a clean pass. Add friction, don't reject.
  // 60 is illustrative — pick a threshold matching your own risk tolerance.
  if (verdict === 'risky' || risk_score >= 60) {
    return createAccount({ email, password, status: 'pending_verification', meta });
  }

  // Unknown: the check didn't conclude. Don't punish the signup for it.
  if (verdict === 'unknown') {
    return createAccount({ email, password, status: 'pending_reverify', meta });
  }

  return createAccount({ email, password, status: 'active', meta });
});
```

Two operational details worth building in from the start: `/v1/validate` is rate-limited at 1,000 requests per minute per API key, and you should issue the key a signup handler uses with only the `validate:single` scope — a form endpoint has no reason to hold bulk or billing scopes.

Treat the call like any third-party dependency on your signup path: short timeout, and decide up front whether a failure fails open or closed. That's a product decision, not a validation one.

**If your signup flow runs through an agent** rather than your own request code, the same check is available as an MCP tool call: `pingvalid_validate_email`, served from `https://pingvalid.com/mcp` over Streamable HTTP, returning the same verdict, risk score and tier evidence with no REST client code to write. Setup: [pingvalid.com/docs/mcp](https://pingvalid.com/docs/mcp).

## Role Accounts Are a Different Decision Than Disposable Addresses

`info@`, `admin@`, `support@` are role accounts, detected as a separate tier (`t5_role`) from disposable detection (`t4_disposable`). A disposable address is built not to exist for long. A role account is the opposite: a real, deliverable mailbox, usually monitored by more than one person.

Detecting a role account tells you nothing about fraud risk. It tells you the signup isn't tied to one identifiable person's inbox — which matters for support, billing disputes and account recovery, not for whether to let them in. For a B2B flow a shared `it@company.com` is completely normal. Keep the two as independent signals rather than collapsing them into one "suspicious" flag.

## What This Doesn't Solve

Blocking disposable and high-risk addresses at signup does not stop all bots, all fraud, or all free-trial abuse. Someone determined to abuse a trial can do it with a real, non-disposable mailbox from a free provider, reused or newly created each time. Email validation confirms whether a mailbox is real and reachable; it has no way to see whether the same person is behind ten different real addresses. That's a different problem — device fingerprinting, rate limiting by IP or payment method, account-linking heuristics.

It's also not a consent check. A validated address says nothing about whether the person behind it agreed to receive email from you. This is a signup-quality gate, not a compliance control.

## FAQ

### Why does a static list of disposable email domains stop working over time?
Because new disposable-mail domains go live continuously and a fixed list only reflects what existed as of its last update. A domain registered today isn't on any list yet by definition.

### Should I reject a signup outright if the email verdict isn't "valid"?
No. `risky` and `unknown` are not the same as fraudulent. Hard-blocking on anything short of `valid` rejects real users along with fake ones. Gate on the specific signal — disposable, role, risk score — instead of the verdict alone.

### Where should the email check run in a signup flow — inline or after signup?
Inline, before the account is created, if the goal is to prevent the signup. An async check can enrich the record afterward, but by then any resource the signup was after may already be spent.

### What should happen when a verdict comes back "unknown"?
Let the signup through rather than rejecting it. `unknown` means the check couldn't reach a conclusion, not that the address is bad.

### Is a role account like info@ the same risk as a disposable address?
No. A disposable address is designed to stop existing soon; a role account is a real, ongoing, shared mailbox. Role detection is relevant to B2B account ownership, not fraud risk.

### Does blocking disposable emails stop free-trial abuse entirely?
No. It closes off one path. Someone can still abuse a trial with real, non-disposable addresses — that needs device or payment-method signals, not an email check.