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.
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"
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"
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.
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();
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.
Every check here has a cost in real customers. These are the ones that actually bite — several because we shipped them ourselves.
Paste any IP, email, phone or domain into the playground — no signup. Or get a free API key for 1,000 lookups a month, no card.