If fake accounts appeared overnight, you are almost certainly being hit by one script rather than many people. That is good news: scripted signups reuse infrastructure, and infrastructure is measurable. Here is the order to check things in, cheapest first.
Check the email domain before you check anything else
Disposable-address providers are the cheapest thing an attacker uses and the cheapest thing for you to detect. There are roughly 8,000 known throwaway domains, and a scripted signup run will lean on a handful of them. This single check typically removes most of the volume, and it never touches a real customer — nobody signs up for a product they intend to pay for using a ten-minute mailbox.
curl "https://www.layercall.com/v1/verify/email?email=test@mailinator.com" \
-H "X-Api-Key: YOUR_KEY"
Then look at where the request came from
A datacenter IP address means the request did not come from a phone or a home broadband connection — it came from a rented server. Real customers occasionally arrive via VPNs, so treat this as a signal rather than a verdict. But a burst of signups from one hosting provider inside an hour is not a coincidence.
curl "https://www.layercall.com/v1/score/ip?ip=1.2.3.4" \
-H "X-Api-Key: YOUR_KEY"
Check how old the email's domain is
Attackers who move past disposable providers register their own domains, and they use them immediately. A domain registered three days ago that is already signing up for your product is worth a second look. Be careful here: real founders launch on week-old domains too, which is why this should raise a challenge rather than a block.
Score the whole signup, not each field separately
The signals matter together. A disposable email is worth a challenge. A disposable email from a datacenter IP on a domain registered yesterday is not ambiguous. One call scores every field and returns a single verdict, so you write one branch instead of five.
const trust = await fetch("https://www.layercall.com/v1/score/user", {
method: "POST",
headers: { "X-Api-Key": process.env.LAYERCALL_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ ip, email, phone }),
}).then(r => r.json());
if (trust.verdict === "block") return reject();
if (trust.verdict === "review") return sendOneTimeCode();
return createAccount();
Send a code — do not slam the door
This is the part most teams get wrong under pressure. Blocking outright turns every false positive into a lost customer who never tells you why they left. A one-time code costs a real person eight seconds and costs a script the entire attack, because the script has no mailbox to receive it. Reserve outright rejection for the cases where several strong signals agree.
What a naive version of this gets wrong
- Blocking every VPN user. Plenty of privacy-conscious people, remote workers on corporate VPNs, and entire countries browse this way. VPN use is a reason to verify, not to refuse.
- Treating an unmeasured signal as a negative. If a domain's registration date could not be determined, that is unknown — not "old and therefore fine". We shipped this bug ourselves and wrote it up.
- Blocking shared IP addresses. University networks, mobile carriers and offices put thousands of unrelated people behind one address. Punishing that address punishes all of them.
- Scoring the wrong device. If you fingerprint the browser, make sure you are reading the end user's user agent and not your own server's HTTP client — we made exactly this mistake and every request looked identical.
Originally published at www.layercall.com/guides/stop-fake-signups.
Top comments (0)