APITier 1Tier 2Score
Developer11 min readMarch 30, 2026

Building a Fraud-Resistant Signup Flow

A practical guide to building signup forms that block bots and fake accounts without adding friction for real users. Covers email validation, device signals, rate limiting, and progressive verification.

Your Signup Form Is Your Front Door

Every user who hits your product walks through the signup form first. So does every bot, every burner email, and every bad actor trying to exploit your free tier. The signup form is the single most important fraud prevention surface in your application, and most teams treat it as an afterthought.

This guide walks through building a signup flow that blocks fraud without making real users jump through hoops. No CAPTCHAs, no phone verification walls, no "prove you are human" puzzles. Just smart validation that runs silently and catches fakes before they cost you money.

The Architecture

A fraud-resistant signup flow has four layers. Each one runs independently and feeds into a single decision.

  1. Input validation (client + server): format checks, required fields, type safety
  2. Email risk scoring (server, under 100ms): disposable detection, domain reputation, pattern analysis
  3. Contextual signals (server): IP reputation, request timing, device metadata
  4. Progressive verification (async): confirmation email, activity monitoring, escalation rules

Layers 1 and 2 run synchronously during the signup request. Layer 3 enriches the decision with contextual data. Layer 4 handles the edge cases asynchronously after the account is created.

Layer 1: Input Validation

Start with the basics. Validate inputs on both client and server. The client-side check gives instant feedback. The server-side check is the real gate.

import { z } from 'zod';

const signupSchema = z.object({
  email: z.string().email('Enter a valid email address'),
  name: z.string()
    .min(2, 'Name must be at least 2 characters')
    .max(100, 'Name is too long')
    .regex(/^[a-zA-Zs-'.]+$/, 'Name contains invalid characters'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters'),
});

// In your route handler
const parsed = signupSchema.safeParse(req.body);
if (!parsed.success) {
  return res.status(400).json({
    errors: parsed.error.flatten().fieldErrors,
  });
}

This catches malformed input, empty fields, and obviously junk data. It does not catch a well-formed fake email like throwaway@mailinator.com. That is what layer 2 is for.

Layer 2: Email Risk Scoring

This is the highest-impact layer. A single API call that runs in under 100ms and catches 85 to 95% of fake signups.

Email validation checks dozens of signals that a format regex cannot see: Is the domain disposable? Does it have MX records? Is the local part a random string? Is the domain freshly registered? Is the email on a known blocklist? Does the mailbox actually exist?

import { BigShield } from 'bigshield';

const shield = new BigShield(process.env.BIGSHIELD_API_KEY);

async function validateSignupEmail(email: string) {
  const result = await shield.validate(email);

  return {
    allowed: result.recommendation !== 'reject',
    needsReview: result.recommendation === 'review',
    riskScore: result.risk_score,
    signals: result.signals,
  };
}

The response includes a risk score from 0 (highest risk) to 100 (lowest risk) and a recommendation: accept, review, or reject. Use these to make a three-way decision:

  • Accept (score 70-100): Create the account immediately. This is a legitimate user.
  • Review (score 30-69): Create the account but flag it. Apply stricter rate limits or require email confirmation before activating premium features.
  • Reject (score 0-29): Block the signup. Return a generic "please use a valid email" error. Do not reveal that you detected fraud.

The key here: validate before you provision. If your app allocates AI tokens, compute credits, or trial features on signup, a rejected email should never reach that step. Validate first, create the account second, provision resources third.

Layer 3: Contextual Signals

Email validation catches most fakes, but some attacks use real email addresses. Contextual signals help catch coordinated attacks and suspicious patterns that email analysis alone misses.

IP Intelligence

Check the signup IP against known proxy, VPN, Tor, and datacenter IP ranges. A signup from a datacenter IP is suspicious. Multiple signups from the same IP within a short window is a red flag.

async function getIPContext(ip: string) {
  // Track signup velocity per IP
  const recentSignups = await redis.zrangebyscore(
    `signups:ip:${ip}`,
    Date.now() - 3600000, // last hour
    '+inf'
  );

  return {
    signupsLastHour: recentSignups.length,
    suspicious: recentSignups.length > 5,
  };
}

// After successful signup, record the IP
await redis.zadd(`signups:ip:${ip}`, Date.now(), newUser.id);
await redis.expire(`signups:ip:${ip}`, 86400);

BigShield's Tier 2 signals include IP reputation scoring, datacenter and proxy detection, and per-IP signup velocity tracking. If you want this without building it yourself, the Pro plan includes all contextual signal tiers.

Request Timing

Bots submit forms fast. Measure time between page load and form submission. A real human takes at least three to five seconds to fill out a signup form. A bot does it in under a second.

// Client: embed a timestamp when the page loads
const pageLoadTime = Date.now();

form.addEventListener('submit', () => {
  document.getElementById('form_timing').value = Date.now() - pageLoadTime;
});

// Server: check the timing
const formDuration = parseInt(req.body.form_timing);
if (formDuration < 2000) {
  // Filled out in under 2 seconds — very suspicious
  riskFactors.push('fast_submission');
}

Device Metadata

Collect basic device signals from the request: User-Agent, Accept-Language, timezone offset. Bots often have inconsistent or missing device metadata. A request with no Accept-Language header or a User-Agent claiming to be Chrome 80 (released in 2020) is suspicious.

You do not need a full fingerprinting library for this. Basic header analysis catches the low-hanging fruit.

Layer 4: Progressive Verification

Not every suspicious signup needs to be blocked at the door. Some ambiguous cases are better handled after the account is created.

Email Confirmation

For accounts that score in the "review" range (30-69 risk score), require email confirmation before activating premium features. The account exists and the user can look around, but they cannot access free-tier tokens, API keys, or batch features until they click the confirmation link.

This costs you almost nothing (one transactional email) and catches a large chunk of borderline fraud. A disposable email that made it past initial validation will still fail if the address is abandoned before confirmation.

Activity-Based Escalation

Monitor the first 24 hours of account activity. Accounts that immediately hit API endpoints at high volume, create multiple API keys, or exhibit patterns consistent with automation should be flagged for review.

// Simplified post-signup activity check
async function checkNewAccountActivity(userId: string) {
  const account = await db.user.findUnique({ where: { id: userId } });
  const hoursSinceSignup = (Date.now() - new Date(account.createdAt).getTime()) / 3600000;

  if (hoursSinceSignup > 24) return; // Only monitor first 24 hours

  const apiCalls = await redis.get(`usage:${userId}:today`);
  const apiKeys = await db.apiKey.count({ where: { userId } });

  if (apiCalls > 500 && hoursSinceSignup < 1) {
    await flagAccount(userId, 'high_velocity_first_hour');
  }

  if (apiKeys > 3 && hoursSinceSignup < 2) {
    await flagAccount(userId, 'multiple_keys_created_quickly');
  }
}

Putting It All Together

Here is a complete signup route that implements all four layers:

import express from 'express';
import { BigShield } from 'bigshield';
import { z } from 'zod';

const shield = new BigShield(process.env.BIGSHIELD_API_KEY);

const signupSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  password: z.string().min(8),
  form_timing: z.string().optional(),
});

app.post('/api/signup', async (req, res) => {
  // Layer 1: Input validation
  const parsed = signupSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: 'Invalid input.' });
  }

  const email = parsed.data.email.trim().toLowerCase();
  const ip = req.headers['x-forwarded-for'] || req.ip;

  // Layer 2: Email risk scoring
  let validation;
  try {
    validation = await shield.validate(email);
  } catch {
    // Fail open if validation service is down
    validation = { recommendation: 'accept', risk_score: -1 };
  }

  if (validation.recommendation === 'reject') {
    return res.status(400).json({
      error: 'Please use a valid email address.',
    });
  }

  // Layer 3: Contextual signals
  const formTiming = parseInt(parsed.data.form_timing || '99999');
  const ipSignups = await getRecentSignupsFromIP(ip);

  let requireVerification = validation.recommendation === 'review';
  if (formTiming < 2000) requireVerification = true;
  if (ipSignups > 5) requireVerification = true;

  // Check for duplicate
  const existing = await db.user.findUnique({ where: { email } });
  if (existing) {
    return res.status(409).json({ error: 'Account already exists.' });
  }

  // Create the account
  const user = await db.user.create({
    data: {
      email,
      name: parsed.data.name.trim(),
      password: await hash(parsed.data.password),
      riskScore: validation.risk_score,
      verified: !requireVerification,
    },
  });

  // Layer 4: Progressive verification
  if (requireVerification) {
    await sendConfirmationEmail(email, user.id);
  }

  // Only provision resources for verified users
  if (!requireVerification) {
    await provisionFreeTierCredits(user.id);
  }

  // Record IP for velocity tracking
  await recordSignupIP(ip, user.id);

  return res.status(201).json({
    id: user.id,
    verified: !requireVerification,
  });
});

What This Catches

With all four layers in place:

  • Disposable emails (Mailinator, Guerrilla Mail, Tempmail, and 72,000+ others): caught by email validation
  • Machine-generated patterns (xk7qm3vb9@gmail.com): caught by pattern analysis in email validation
  • Freshly registered abuse domains: caught by domain age checks
  • Headless bot submissions: caught by form timing analysis
  • Coordinated signup attacks: caught by IP velocity tracking
  • Accounts that pass initial checks but behave badly: caught by post-signup activity monitoring

The total added latency to the signup flow is under 100ms (email validation runs in parallel with database duplicate check). Real users notice nothing. Your fake signup rate drops by 80 to 95%.

Common Mistakes

Validating after provisioning. If you allocate tokens, credits, or compute before checking the email, every fake signup costs you real money. Always validate first.

Blocking too aggressively. A risk score of 45 does not mean "definitely fake." Use the review range to require additional verification rather than outright blocking. False positives lose real customers.

Revealing detection logic. Do not return error messages like "disposable email detected" or "your IP is flagged." Use generic messages: "Please use a valid email address." Specific errors teach attackers how to bypass your system.

Relying on a single layer. No single method catches everything. Email validation misses attacks from real email addresses. IP limiting misses proxy networks. Honeypots miss modern bots. Layer them.

Start With the Highest Impact Layer

If you are starting from zero, add email validation first. It is the single highest-signal layer, it runs in under 100ms, and the integration takes about five minutes. BigShield's documentation has SDK examples for TypeScript, Python, PHP, Ruby, and Go.

The free tier gives you 1,500 validations per month with no credit card. That is enough to protect a growing signup flow while you build out the other layers. When you need more, the Starter plan at $29/month handles 5,000 validations.

Try it on your existing user list too. Export your signups from the last 90 days, run them through the free email checker, and see what percentage would have been caught. Most teams are surprised by the number.

Ready to stop fake signups?

BigShield validates emails with 20+ signals in under 200ms. Start for free, no credit card required.

Get Started Free

Related Articles