mains rarely change their classification. Caching domain verdicts reduces API calls and latency for subsequent signups from the same domain.
3. Signal Aggregation: The verification service should combine:
* Domain Reputation: Is the domain associated with known disposable providers?
* Registration Age: Is the domain newly registered with minimal web presence?
* MX Topology: Does the MX record point to infrastructure known for temporary hosting?
* Local-Part Entropy: Does the username exhibit machine-generated patterns (e.g., UUIDs, random strings)?
4. Alias Distinction: The solution must differentiate between disposable inboxes and privacy aliases. Aliases forward to permanent inboxes and exhibit the behavior of real addresses. Blocking aliases alienates privacy-focused users.
Implementation Example
The following TypeScript implementation demonstrates a robust verification service. This example uses a wrapper around a verification API (e.g., Tickstem) to handle signal aggregation. The interface and logic are designed for production resilience.
// types/email-verification.ts
export type RiskVerdict = 'ALLOW' | 'BLOCK' | 'REVIEW';
export interface EmailRiskReport {
email: string;
verdict: RiskVerdict;
signals: {
isDisposable: boolean;
isPrivacyAlias: boolean;
domainReputation: 'GOOD' | 'SUSPICIOUS' | 'BAD';
mxValid: boolean;
};
reason?: string;
}
// services/email-guardian.ts
import { CacheManager } from './cache-manager';
export class EmailGuardian {
private cache: CacheManager;
private apiKey: string;
constructor(config: { apiKey: string; cache: CacheManager }) {
this.apiKey = config.apiKey;
this.cache = config.cache;
}
/**
* Assesses email risk with domain-level caching.
* Returns a verdict based on aggregated signals.
*/
async assessRisk(email: string): Promise<EmailRiskReport> {
const domain = email.split('@')[1]?.toLowerCase();
// 1. Check cache for domain verdict
const cachedVerdict = await this.cache.getDomainVerdict(domain);
if (cachedVerdict) {
return {
email,
verdict: cachedVerdict,
signals: { isDisposable: false, isPrivacyAlias: false, domainReputation: 'GOOD', mxValid: true },
reason: 'cached'
};
}
// 2. Fetch fresh assessment from verification API
const report = await this.fetchVerificationReport(email);
// 3. Determine verdict based on policy
const verdict = this.determineVerdict(report);
// 4. Cache domain result to reduce future API calls
// Cache for 24 hours; disposable domains rarely change status
await this.cache.setDomainVerdict(domain, verdict, { ttl: 86400 });
return { ...report, verdict };
}
private async fetchVerificationReport(email: string): Promise<EmailRiskReport> {
// Implementation calls verification API (e.g., Tickstem)
// Returns raw signals including disposable flag, alias detection, etc.
// Example response structure:
return {
email,
verdict: 'ALLOW', // Placeholder, determined by policy
signals: {
isDisposable: false,
isPrivacyAlias: false,
domainReputation: 'GOOD',
mxValid: true
}
};
}
private determineVerdict(report: EmailRiskReport): RiskVerdict {
// Policy Logic:
// - Block disposable inboxes
// - Allow privacy aliases (Apple, SimpleLogin, etc.)
// - Review suspicious domains
if (report.signals.isDisposable) return 'BLOCK';
if (report.signals.isPrivacyAlias) return 'ALLOW';
if (report.signals.domainReputation === 'SUSPICIOUS') return 'REVIEW';
if (!report.signals.mxValid) return 'BLOCK';
return 'ALLOW';
}
}
Usage in Signup Flow
// handlers/signup-handler.ts
import { EmailGuardian } from '../services/email-guardian';
export async function handleUserRegistration(payload: { email: string; password: string }) {
const guardian = new EmailGuardian({
apiKey: process.env.EMAIL_VERIFICATION_KEY,
cache: redisCache
});
const riskReport = await guardian.assessRisk(payload.email);
if (riskReport.verdict === 'BLOCK') {
throw new ValidationError('This email address is not permitted for registration.');
}
if (riskReport.verdict === 'REVIEW') {
// Soft block: Create account but restrict features pending review
return createRestrictedAccount(payload);
}
// Proceed with full registration
return createFullAccount(payload);
}
Rationale:
assessRisk vs check: The method name emphasizes risk evaluation rather than binary validation, reflecting the multi-signal nature of the check.
- Domain Caching: Caching by domain drastically reduces API costs and latency. If 100 users sign up from a disposable domain, only the first triggers an API call.
- Policy Separation:
determineVerdict isolates business logic from API calls. This allows easy adjustment of policies (e.g., changing from hard block to review) without modifying the integration layer.
- Alias Handling: The policy explicitly allows privacy aliases, preserving conversion from users who value privacy but intend to use the product long-term.
Pitfall Guide
-
The Stale List Trap
- Explanation: Importing a static blocklist from a repository and never updating it. New disposable domains appear daily, and existing ones rotate.
- Fix: Use a dynamic verification service that maintains the dataset continuously. Never rely on a file you manage manually.
-
MX Shared Infrastructure False Positives
- Explanation: Blocking domains based solely on shared MX records. Large providers like Gmail and Outlook use shared infrastructure; blocking shared MX would block millions of legitimate users.
- Fix: Combine MX analysis with domain reputation. Only block if the MX points to infrastructure specifically associated with disposable hosting, not general shared mail.
-
The Alias Blind Spot
- Explanation: Treating privacy forwarding aliases (Apple Hide My Email, SimpleLogin, Fastmail aliases) as disposable. These addresses forward to real inboxes and are permanent. Blocking them loses privacy-conscious users.
- Fix: Ensure your verification provider distinguishes between disposable inboxes and forwarding aliases. Configure your policy to allow aliases.
-
Pattern Matching Overfitting
- Explanation: Using regex to block random-looking usernames. Legitimate users often have complex or unique usernames. Regex alone generates high false positives.
- Fix: Use entropy scoring as a supplementary signal, not a primary block criterion. Rely on domain reputation for the verdict.
-
Latency Neglect
- Explanation: Blocking the signup UI while waiting for a slow API response. This degrades user experience and increases abandonment.
- Fix: Implement timeouts. If the verification service is unresponsive, fallback to a permissive policy or queue the check asynchronously. Use domain caching to minimize latency.
-
Soft Block Limbo
- Explanation: Flagging suspicious accounts for review but never processing the queue. These accounts remain in a restricted state, frustrating users and cluttering the database.
- Fix: Automate the review process or set expiration policies for restricted accounts. If manual review is required, ensure SLAs are met.
-
Per-Request Verification
- Explanation: Verifying the email on every API request or page load. This wastes resources and incurs unnecessary API costs.
- Fix: Verify only at signup or email change. Store the verification result in the user record. Re-verify only if the email changes or periodically for high-risk accounts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Volume SaaS | Hard Block via Multi-Signal API | Prevents abuse at scale; protects sender reputation; low maintenance. | API costs scale with volume; offset by reduced fraud and ESP costs. |
| Low-Volume B2B | Soft Block + Manual Review | Maximizes lead capture; allows human judgment for edge cases. | Manual review time; risk of delayed onboarding. |
| Internal Tools | Static Blocklist + MX Check | Zero external API cost; sufficient for controlled environments. | Maintenance overhead; risk of stale data. |
| Consumer App | Hard Block with Alias Allowance | Blocks abuse while respecting privacy; balances growth and security. | API costs; requires careful alias configuration. |
Note: Services like Tickstem offer tiered pricing (e.g., 500 free checks/month), making API-based verification accessible even for early-stage projects.
Configuration Template
Use this template to configure the verification service and policy. Adjust values based on your risk tolerance.
// config/email-verification.config.ts
export const emailVerificationConfig = {
provider: 'tickstem', // API provider
apiKey: process.env.EMAIL_VERIFICATION_API_KEY,
cache: {
enabled: true,
ttlSeconds: 86400, // 24 hours
strategy: 'domain' // Cache by domain, not email
},
policy: {
disposable: 'BLOCK', // Hard block disposable inboxes
privacyAlias: 'ALLOW', // Allow Apple/SimpleLogin aliases
invalidSyntax: 'BLOCK', // Block malformed emails
suspiciousDomain: 'REVIEW',// Flag for manual review
mxInvalid: 'BLOCK' // Block domains without valid MX
},
fallback: {
onTimeout: 'ALLOW', // Allow signup if API times out
onError: 'ALLOW' // Allow signup on API error
}
};
Quick Start Guide
- Obtain API Key: Sign up for a verification service (e.g., Tickstem) and retrieve your API key. Most providers offer a free tier for testing.
- Install Dependencies: Add the verification client to your project.
npm install @tickstem/verify
- Add Middleware: Integrate the
EmailGuardian service into your signup handler. Ensure the check runs before database persistence.
- Configure Policy: Set your policy in the configuration file. Enable domain caching and define rules for disposables and aliases.
- Deploy and Monitor: Deploy the changes. Monitor verification metrics to ensure the policy is effective and false positives are low. Adjust thresholds as needed.