Security

Catch fake signups with three layered checks

OWASP lists mass account creation as its own automated threat, where bots use the normal sign-up form to make many accounts for later misuse 1. No single check stops it, and every single check also catches some honest people. This post layers three cheap lookups that run before a confirmation email is sent, shows what each LookValid product returns, and gives a decision table that combines them.

By LookValid engineering. Published 2026-10-19.

Why one signal is not enough

OWASP describes account creation as creating multiple accounts for subsequent misuse, such as spam, laundering goods or manipulating reviews and surveys 1. The accounts come through the same form your customers use, so the defence is to score each signup on several independent facts rather than to trust one rule.

The three facts below cost one lookup each and need no interaction from the visitor: whether the email domain is a throwaway service, how long the email domain has been registered, and whether the visitor's IP address belongs to a hosting network. Each fact is weak alone. Together they separate a bot farm from a person who simply uses a privacy-minded mailbox.

What each product returns

The email-domain-check product reads public DNS and open lists for the domain behind an address. It returns disposable (the domain is on a list of throwaway providers), free_provider, role_account, the MX records, and a deliverability_signal with reasons. A domain that publishes a null MX record, the single record with preference 0 and . as the host, tells senders it accepts no mail 2, and the product reports that as null_mx. It never opens a mail connection, so it cannot tell you whether a specific mailbox exists.

The domain-age product asks the registry over RDAP. RDAP events carry an eventAction and an eventDate, and the registration action marks when the object was first registered 3. The product returns created_at and age_days, the registrar and the registry server it used. If the registry cannot be reached the answer is unavailable, never an age of zero.

The ip product returns the network owner (asn, asn_org), is_hosting with hosting_provider, and is_tor. Hosting is decided by matching the address against ranges that cloud providers publish themselves; AWS, for example, publishes its ranges as a JSON file meant for identifying traffic from AWS 4. Our own risk_score adds 25 points for a hosting address and 70 for a Tor exit. is_vpn is always null, because we do not have a VPN data source, so an unknown value is never reported as false.

curl -X POST https://YOUR_API_HOST/v1/ip \
  -H "Authorization: Bearer $LOOKVALID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ip":"8.8.8.8"}'

Check the visitor's IP address before the account is created

{
  "asn": 15169,
  "asn_org": "Google LLC",
  "is_hosting": true,
  "hosting_provider": "Google",
  "is_tor": false,
  "is_vpn": null,
  "risk_score": 25
}

Excerpt of our captured ip example for 8.8.8.8

A decision table that combines the signals

The table is a starting policy we suggest. Tune the cut-offs against your own signups, where you can see which rows later produce abuse.

Suggested actions for combinations of the three signals
Throwaway domainEmail domain ageHosting or Tor IPSuggested action
No365 days or moreNoAllow
No365 days or moreHostingAllow, add one risk point
NoUnder 30 daysNoAllow low-value actions, review high-value ones
NoUnder 30 daysHostingStep-up verification
YesAnyNoStep-up verification
YesAnyHosting or TorHold for manual review
Null MXAnyAnyAsk for a different address, since it accepts no mail

Suggested actions for combinations of the three signals

type Signals = { disposable: boolean; nullMx: boolean; ageDays: number | null; hosting: boolean; tor: boolean };
​
function decide(s: Signals): 'allow' | 'verify' | 'review' | 'new_address' {
  if (s.nullMx) return 'new_address';
  const network = s.hosting || s.tor;
  if (s.disposable) return network ? 'review' : 'verify';
  const young = s.ageDays !== null && s.ageDays < 30;
  return young && network ? 'verify' : 'allow';
}

The same table as a scoring function

A missing age (ageDays null) is treated as no signal rather than as a young domain, because a registry outage is not evidence about the visitor.

False positives and what not to block

  • Hosting addresses carry real people. Corporate gateways, remote desktops and some privacy tools exit from cloud networks, so is_hosting alone should never reject a signup.
  • Young domains belong to new businesses. A founder who registered a domain last week is not a bot.
  • The disposable list is a list. A new throwaway service is missed until it is added, and a domain can appear on it by mistake, so offer step-up verification before a hard refusal.
  • A null is_vpn means unknown. Do not read it as a clean result.

Prefer actions that add friction to risky signups, such as a second verification step, over actions that silently drop them, and log which signal fired so you can measure the cost to honest visitors. Our email domain check, domain age and IP pages list every field.

Sources

  1. OWASP Automated Threats to Web Applications: OAT-019 Account Creation, read 2026-10-19
  2. RFC 7505: A "Null MX" No Service Resource Record for Domains That Accept No Mail, read 2026-10-19
  3. RFC 9083: JSON Responses for the Registration Data Access Protocol (RDAP), read 2026-10-19
  4. AWS documentation: AWS IP address ranges, read 2026-10-19