← Blog

Multi-signal signup fraud prevention: phone, email, and IP

One weak signal blocks nothing; here's how to fold email, phone, and IP checks into a single risk score — and exactly where Boundstone's honesty ends.

Contents

Search "signup fraud prevention" and you'll drown in vendors selling a single magic score. The honest version is duller and more useful: no one signal catches a determined abuser, and every signal has a failure mode. What works is combining a few cheap, fast checks — email, phone, and IP — into a risk number your own policy decides how to act on. This walks through exactly that, and it's equally clear about what these checks can't see.

One signal is a coin flip

A disposable email address is suspicious. It is also what a privacy-conscious real user reaches for. A number that fails to parse is a red flag — and so is a legitimate customer fat-fingering their area code. A private source IP means something is off with how the request reached you — or your load balancer just isn't forwarding the client address.

Each signal, alone, produces false positives you'll regret. Together they're a different story: real users rarely trip three independent checks at once, while bulk-created fake accounts tend to. The goal isn't a verdict from any one field. It's a score.

Three cheap signals worth combining

Email: disposable and role, not just "looks like an email"

Your language already validates email shape. A regex, Python's email.utils.parseaddr, or the browser's type="email" gets you syntax for free — often all you need at the form layer. Say so.

Where a stdlib stops is the domain. Is it a throwaway from a disposable provider? Does it even have an MX record to receive mail? Is it a role address like admin@ or support@ that rarely belongs to one human? POST /v1/verify/email answers those over one contract and hands you the receipts:

{
  "valid_syntax": true,
  "domain": "mailinator.com",
  "mx_found": true,
  "disposable": true,
  "role_account": false,
  "free_provider": false,
  "checks": {
    "performed": ["syntax", "mx", "disposable_list", "role_list"],
    "not_performed": ["smtp_mailbox", "catch_all"]
  }
}

Read not_performed before you trust disposable: true. Boundstone does not open an SMTP session to confirm the mailbox exists, and does not detect catch-all domains. It checks shape, MX, and known lists — nothing it can't stand behind. You can try this without an account at /tools/email-validator. And note free_provider: a Gmail address is not fraud. It's a returned flag, deliberately kept out of performed, because "uses a free provider" is context, not a defect.

Phone: plausibility, not liveness

POST /v1/verify/phone tells you whether a number is well-formed for its region and what line type the metadata suggests:

{
  "valid": true,
  "e164": "+16504472983",
  "country": "US",
  "line_type": "fixed_line_or_mobile",
  "national_format": "(650) 447-2983",
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}

That's plausibility, and it's genuinely useful at signup — an unparseable number is a real signal. What it is not is proof the phone rings. Carrier lookup, ported-status, and HLR liveness are not_performed by default; they run as a paid opt-in with hlr:true, priced at 5 credits and refunded when the network cannot answer. Treat valid: true as "this could be a real number," not "someone answered it."

IP: private, bogon, loopback as a red flag

POST /v1/verify/ip classifies the source address by IANA range — public, private, loopback, reserved, bogon:

{
  "valid": true,
  "version": 4,
  "normalized": "10.0.0.7",
  "classification": "private",
  "is_public": false,
  "is_bogon": false,
  "checks": {
    "performed": ["format", "version", "range_classification"],
    "not_performed": ["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]
  }
}

A signup whose source IP is private, loopback, or bogon is worth a second look — a real browser on the public internet doesn't arrive from 10.0.0.7. Usually it means a misconfigured proxy header; sometimes something worse. Either way it's a cheap flag.

What Boundstone does not do

Look hard at that IP not_performed list, because it names the things people expect and Boundstone won't fake: no geolocation, no ASN, no hosting/datacenter detection, no proxy/VPN/Tor detection, no reputation. Those need licensed data that isn't shipped. Boundstone also does not do device fingerprinting.

Those are separate, heavier problems, and mature fraud platforms exist for them. If your threat model needs "is this a known VPN exit node in a high-risk region on a reused device," that's a different tool. Boundstone gives you fast, honest structural signals — the layer you build a policy on, not the whole policy.

A signup handler that scores all three

Here's a Node handler making the three calls in parallel and folding the results into one number. Node 18+ has fetch built in.

import express from "express";

const app = express();
app.use(express.json());

const API = "https://api.boundstone.io";
const KEY = process.env.BOUNDSTONE_API_KEY;

async function verify(kind, body) {
  const res = await fetch(`${API}/v1/verify/${kind}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return res.json();
}

function score({ email, phone, ip }) {
  let risk = 0;
  const reasons = [];

  if (email.disposable)   { risk += 3; reasons.push("disposable email domain"); }
  if (!email.mx_found)    { risk += 2; reasons.push("email domain has no MX"); }
  if (email.role_account) { risk += 1; reasons.push("role address"); }

  if (!phone.valid) { risk += 3; reasons.push("phone did not parse"); }

  if (!ip.is_public) { risk += 3; reasons.push(`non-public source IP (${ip.classification})`); }
  if (ip.is_bogon)   { risk += 2; reasons.push("bogon source IP"); }

  return { risk, reasons };
}

app.post("/signup", async (req, res) => {
  const { email, phone } = req.body;
  const ip =
    req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
    req.socket.remoteAddress;

  const [emailResult, phoneResult, ipResult] = await Promise.all([
    verify("email", { email }),
    verify("phone", { phone }),
    verify("ip", { ip }),
  ]);

  const { risk, reasons } = score({
    email: emailResult,
    phone: phoneResult,
    ip: ipResult,
  });

  if (risk >= 6) return res.status(403).json({ decision: "block", reasons });
  if (risk >= 3) return res.status(202).json({ decision: "review", reasons });
  return res.status(201).json({ decision: "allow" });
});

The weights are yours to tune — they encode your tolerance, not a universal truth.

Flags are inputs, not verdicts

The one rule that keeps this honest: a flag is an input to a policy, never the policy itself. disposable: true doesn't ban a user; it adds three points. Three signals agreeing is what earns a block. Everything in between — step-up email confirmation, a review queue, a soft limit on the new account — is where most fraud actually gets handled without torching real signups. Because Boundstone returns performed and not_performed on every response, you always know exactly which evidence your score is standing on.

The short version

  • No single check stops signup fraud. Combine email, phone and IP into a score.
  • Lead with your stdlib for syntax. Use the API for disposable/role/MX, phone plausibility and IP classification under one contract.
  • Boundstone does not do IP intelligence. No geolocation, ASN, hosting/datacenter, VPN/proxy/Tor, reputation or device fingerprinting — those need licensed data or a dedicated platform.
  • Read not_performed before trusting any valid. That list is why a "valid" means something.
  • Turn scores into graduated policy — block, review, allow — not a single kill switch.

Wire it up with the signup-fraud use-case guide and the full API docs. The free tier is 250 credits a month, no card, and credits never expire — enough to test all three signals against your own traffic before you commit to anything.

Frequently asked questions

How do phone, email, and IP checks work together to prevent signup fraud?

Each signal catches a different class of fake, so stacking them is stronger than any one alone. At signup Boundstone validates that a phone number is correctly formatted for its region and returns its line-type metadata, checks that an email has valid syntax and a domain with MX records while flagging disposable, role, and free-provider addresses, and classifies an IP's version and range while flagging private, reserved, or bogon source addresses. A submission that passes one check but fails another, such as a well-formed email on a disposable domain or a valid number paired with a bogon IP, stands out before you grant access. The value is in the cross-signal pattern, not in any single result being decisive.

Does a 'valid' email or phone result mean the person is real?

No, and this is exactly why every Boundstone response lists checks.performed and checks.not_performed. A valid email means the syntax is correct and the domain has MX records that could accept mail, plus flags for disposable, role, and free-provider addresses, but it does not confirm the mailbox exists because SMTP mailbox and catch-all checks are explicitly not performed. A valid phone means the number is well-formed for its region with line-type metadata, not that it is live or reachable, because carrier lookup, ported status, and HLR liveness are not performed. Read 'valid' as 'well-formed and structurally plausible,' and let the not_performed list tell you what still needs a step-up check.

Can Boundstone detect VPNs, proxies, or the location of a signup's IP address?

Not today, and the API tells you that instead of guessing. The IP check performs format validation, version detection, and range classification, so it can flag private, reserved, and bogon source addresses at signup. Geolocation, ASN, hosting and datacenter detection, proxy, VPN and Tor detection, and IP reputation are all listed under checks.not_performed because they require licensed data Boundstone has not shipped. If another vendor claims flawless VPN detection, ask what data backs it, because an honest not_performed is more useful than a confident guess you cannot trust.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist