DEV Community

nProejct
nProejct

Posted on

How I Built an Explainable Signup Email Risk Checker with Node.js

A valid-looking email address is not always a trustworthy signup.

Basic email validation usually answers only one question:

Does this string look like an email address?

That is useful, but it does not tell us whether the address uses a disposable domain, whether the domain can receive mail, or whether the result is reliable enough to accept automatically.

I wanted a small server-side check that returns an explainable decision instead of a simple true or false.

Why a Boolean Result Is Not Enough

Consider these cases:

  • The email syntax is invalid.
  • The domain is a known disposable email provider.
  • The domain has no MX records.
  • The address appears to be a role account such as admin@ or support@.
  • The domain looks like a typo of a common provider.
  • A DNS request temporarily fails.

These situations should not all produce the same result.

A temporary DNS failure, for example, should not automatically cause a legitimate customer to be rejected.

For that reason, I use four decisions:

Decision Recommended action
ACCEPT Allow the signup to continue
REVIEW Require additional verification
REJECT Block because strong risk evidence was found
UNKNOWN Retry later or use a fallback flow

The important part is that UNKNOWN remains separate from REJECT.

Signals Used for the Decision

The checker evaluates several signals together:

  • Email syntax
  • Disposable email domain evidence
  • MX mail-routing records
  • Free email provider detection
  • Role-based local parts
  • Common domain typo suggestions
  • Confidence and risk level

No single weak signal should decide the entire result.

For example, a role address such as support@example.com may be undesirable for a consumer signup but completely normal for a B2B contact form.

The final policy depends on the application.

Calling the API with Node.js

The following example requires Node.js 18 or later because it uses the built-in fetch function.

Create a file named check-email.mjs:

const email = process.argv[2];
const apiKey = process.env.RAPIDAPI_KEY;
const host =
  "riskbeacon-signup-email-domain-intelligence-api.p.rapidapi.com";

if (!email || !apiKey) {
  console.error(
    "Usage: RAPIDAPI_KEY=your_key node check-email.mjs user@example.com"
  );
  process.exit(1);
}

const response = await fetch(`https://${host}/email/v1/verify`, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-rapidapi-key": apiKey,
    "x-rapidapi-host": host
  },
  body: JSON.stringify({
    email,
    profile: "SIGNUP_BALANCED"
  })
});

const result = await response.json();

if (!response.ok) {
  console.error(`Request failed: ${response.status}`, result);
  process.exit(1);
}

console.log(JSON.stringify({
  decision: result.decision,
  confidence: result.confidence,
  riskLevel: result.riskLevel,
  disposable: result.disposable,
  roleAccount: result.roleAccount,
  reason: result.reason
}, null, 2));
Enter fullscreen mode Exit fullscreen mode

Set your RapidAPI key and run the script.

macOS or Linux

RAPIDAPI_KEY="YOUR_RAPIDAPI_KEY" \
node check-email.mjs "new.user@gmail.com"
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell

$env:RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
node .\check-email.mjs "new.user@gmail.com"
Enter fullscreen mode Exit fullscreen mode

A simplified result looks like this:

{
  "decision": "ACCEPT",
  "confidence": 90,
  "riskLevel": "LOW",
  "disposable": false,
  "roleAccount": false,
  "reason": "Email passed the selected validation policy"
}
Enter fullscreen mode Exit fullscreen mode

Using the Decision in a Signup Flow

A simple application policy might look like this:

switch (result.decision) {
  case "ACCEPT":
    // Continue the normal signup flow.
    break;

  case "REVIEW":
    // Require an email OTP, magic link, or CAPTCHA.
    break;

  case "REJECT":
    // Stop the signup and show a neutral error message.
    break;

  case "UNKNOWN":
    // Retry later or use the normal email verification flow.
    break;
}
Enter fullscreen mode Exit fullscreen mode

This lets the application add friction only when the available evidence justifies it.

What This Check Does Not Prove

An MX record proves that a domain is configured to receive mail. It does not prove that a specific mailbox exists.

This checker intentionally does not perform SMTP mailbox probing. SMTP probing can be unreliable, may expose user information, and is often blocked or intentionally obscured by mail providers.

The result should therefore be used as a risk signal before signup, not as a replacement for:

  • Email ownership verification
  • OTP or magic-link confirmation
  • Rate limiting
  • CAPTCHA
  • Device and IP abuse detection

Privacy Considerations

The verification service does not persist submitted email addresses.

Applications should still avoid logging full email addresses unnecessarily. If logging is required, consider masking the local part or storing a one-way hash.

Try the Example

I published the complete Node.js example under the MIT license:

GitHub: Signup Email Risk Checker

The underlying email and domain intelligence API is available here:

RiskBeacon on RapidAPI

Disclosure: I built both the example project and the RiskBeacon API. I would appreciate feedback about the response structure, decision model, and signals that would be most useful in a real signup flow.

Top comments (0)