Forms are the front door of every web application, and most of them are wide open to abuse. Spam bots, disposable email addresses, profanity-laced submissions, and geographic mismatches cost businesses millions in wasted customer support time, polluted databases, and degraded user experiences. The standard defense, a regex for email format and a required field check, is the digital equivalent of a screen door on a submarine.
In this article, we are building The Validation Gauntlet, a multi-layer form validation system that combines three free APIs into a security pipeline that catches problems that client-side validation alone cannot detect. Disify identifies disposable and temporary email addresses. IPapi provides geolocation data that reveals geographic mismatches and VPN usage. PurgoMalum filters profanity and inappropriate content from text inputs. Together, they create a validation stack that transforms a simple contact form into a data quality fortress.
This is not about preventing determined attackers. If someone wants to bypass your form validation badly enough, they will find a way. This is about raising the cost of abuse high enough that casual spammers, lazy bots, and low-effort trolls move on to easier targets. The Pareto principle applies here: twenty percent of the effort eliminates eighty percent of the garbage, and that twenty percent is what we are building today.
Disify at https://disify.com/api is a free email validation service that checks whether an email address uses a disposable or temporary email provider. These are services like Guerrilla Mail, Temp Mail, and the hundreds of clones that let users create throwaway addresses with zero accountability. Disify maintains a database of known disposable email domains and checks submitted addresses against it.
// Disify API endpoint
// GET https://disify.com/api/email/test@guerrillamail.com
{
"format": true, // Is the format valid?
"alias": false, // Is it an alias (e.g., user+tag@gmail.com)?
"domain": "guerrillamail.com",
"disposable": true, // Is this a disposable email provider?
"dns": true // Does the domain have valid DNS?
}
// Legitimate email example
// GET https://disify.com/api/email/user@gmail.com
{
"format": true,
"alias": false,
"domain": "gmail.com",
"disposable": false,
"dns": true
}
Disify is free with no authentication required. Rate limits are not formally documented but are generous enough for form validation purposes. The key fields for our purposes are disposable (is this a throwaway address?), format (is the email format valid?), and dns (does the domain actually exist on the internet?).
IPapi at https://ipapi.co provides IP geolocation data. By checking the user's IP address against their claimed location (if your form collects it), you can detect geographic mismatches that often indicate fraudulent submissions. More importantly, IPapi can identify VPN and proxy usage, which is a strong signal of bot activity or intentional identity masking on forms that should not require anonymity.
// IPapi endpoint (requesting client's own IP info)
// GET https://ipapi.co/json/
{
"ip": "203.0.113.42",
"city": "San Francisco",
"region": "California",
"region_code": "CA",
"country_code": "US",
"country_name": "United States",
"continent_code": "NA",
"postal": "94107",
"latitude": 37.7749,
"longitude": -122.4194,
"timezone": "America/Los_Angeles",
"utc_offset": "-0700",
"country_calling_code": "+1",
"currency": "USD",
"languages": "en-US,es-US",
"asn": "AS13335",
"org": "Cloudflare, Inc."
}
// Look up a specific IP
// GET https://ipapi.co/8.8.8.8/json/
// Get just one field
// GET https://ipapi.co/country_code/
IPapi offers a free tier with a limit of approximately 1,000 requests per day (based on the requesting IP) and no authentication token is needed for basic queries. The org field is particularly useful: it often reveals hosting providers, VPN services, or known proxy networks.
PurgoMalum at https://www.purgomalum.com/service is a free profanity and content filtering API. It checks text against a comprehensive dictionary of profane, obscene, and inappropriate words and phrases, including common obfuscation patterns like character substitution (replacing letters with similar-looking numbers or symbols). This catches the kind of content that makes customer support representatives reach for the job board.
// PurgoMalum endpoints
// Check if text contains profanity
// GET https://www.purgomalum.com/service/containsprofanity?text=hello+world
// Response: false
// GET https://www.purgomalum.com/service/containsprofanity?text=some+bad+word
// Response: true
// Get cleaned text with profanity replaced
// GET https://www.purgomalum.com/service/plain?text=what+the+hell+is+this
// Response: "what the **** is this"
// Custom replacement character
// GET https://www.purgomalum.com/service/plain?text=damn+it&fill_char=-
// Response: "---- it"
// JSON response format
// GET https://www.purgomalum.com/service/json?text=hello+world
{
"result": "hello world"
}
// Add custom words to the filter
// GET https://www.purgomalum.com/service/containsprofanity?text=spammy+text&add=spammy,junk
PurgoMalum is completely free, requires no API key, and has no documented rate limits. It is fast, typically responding in under 100ms, which makes it suitable for real-time validation as users type. The ability to add custom words to the filter list is valuable for businesses that need to block industry-specific or brand-specific terms.
The Validation Gauntlet runs submissions through five distinct validation stages, each adding a score component that contributes to an overall quality score. Submissions that score below a configurable threshold are flagged, blocked, or require additional verification.
class QualityScoreEngine {
constructor() {
this.weights = {
emailFormat: 10,
emailDomain: 15,
emailDisposable: 25,
contentProfanity: 20,
geoConsistency: 15,
behaviorSignals: 15
};
this.maxScore = Object.values(this.weights).reduce((a, b) => a + b, 0);
}
calculate(validationResults) {
let score = 0;
const breakdown = {};
// Email format validation
if (validationResults.email?.formatValid) {
score += this.weights.emailFormat;
breakdown.emailFormat = { score: this.weights.emailFormat, status: 'pass' };
} else {
breakdown.emailFormat = { score: 0, status: 'fail', reason: 'Invalid email format' };
}
// Email domain DNS
if (validationResults.email?.dnsValid) {
score += this.weights.emailDomain;
breakdown.emailDomain = { score: this.weights.emailDomain, status: 'pass' };
} else {
breakdown.emailDomain = { score: 0, status: 'fail', reason: 'Domain has no DNS records' };
}
// Disposable email check
if (validationResults.email && !validationResults.email.isDisposable) {
score += this.weights.emailDisposable;
breakdown.emailDisposable = { score: this.weights.emailDisposable, status: 'pass' };
} else if (validationResults.email?.isDisposable) {
breakdown.emailDisposable = { score: 0, status: 'fail', reason: 'Disposable email detected' };
} else {
// If email check failed, give partial credit
score += Math.round(this.weights.emailDisposable * 0.5);
breakdown.emailDisposable = {
score: Math.round(this.weights.emailDisposable * 0.5),
status: 'unknown',
reason: 'Could not verify'
};
}
// Content profanity check
if (validationResults.content && !validationResults.content.hasProfanity) {
score += this.weights.contentProfanity;
breakdown.contentProfanity = { score: this.weights.contentProfanity, status: 'pass' };
} else if (validationResults.content?.hasProfanity) {
breakdown.contentProfanity = { score: 0, status: 'fail', reason: 'Profanity detected' };
} else {
score += this.weights.contentProfanity; // Give benefit of doubt
breakdown.contentProfanity = { score: this.weights.contentProfanity, status: 'pass' };
}
// Geographic consistency
if (validationResults.geo?.isConsistent) {
score += this.weights.geoConsistency;
breakdown.geoConsistency = { score: this.weights.geoConsistency, status: 'pass' };
} else if (validationResults.geo?.isVPN) {
score += Math.round(this.weights.geoConsistency * 0.3);
breakdown.geoConsistency = {
score: Math.round(this.weights.geoConsistency * 0.3),
status: 'warning',
reason: 'VPN or proxy detected'
};
} else {
score += this.weights.geoConsistency; // No geo data required
breakdown.geoConsistency = { score: this.weights.geoConsistency, status: 'pass' };
}
// Behavior signals (client-side heuristics)
const behaviorScore = this.scoreBehavior(validationResults.behavior);
score += behaviorScore;
breakdown.behavior = {
score: behaviorScore,
status: behaviorScore >= this.weights.behaviorSignals * 0.7 ? 'pass' : 'warning',
reason: behaviorScore < this.weights.behaviorSignals * 0.7 ? 'Suspicious behavior patterns' : null
};
const percentage = Math.round((score / this.maxScore) * 100);
return {
score,
maxScore: this.maxScore,
percentage,
grade: this.getGrade(percentage),
breakdown,
recommendation: this.getRecommendation(percentage, breakdown)
};
}
scoreBehavior(behavior) {
if (!behavior) return this.weights.behaviorSignals;
let score = 0;
const max = this.weights.behaviorSignals;
// Time to fill form (too fast = bot)
if (behavior.fillTimeMs > 3000) score += max * 0.3; // At least 3 seconds
else if (behavior.fillTimeMs > 1000) score += max * 0.1;
// Mouse movement detected
if (behavior.hadMouseMovement) score += max * 0.25;
// Keyboard events detected (not just paste)
if (behavior.hadKeystrokes) score += max * 0.25;
// Focus/blur events (tab navigation)
if (behavior.hadFocusChanges) score += max * 0.2;
return Math.round(score);
}
getGrade(percentage) {
if (percentage >= 90) return { letter: 'A', color: '#00b894', label: 'Excellent' };
if (percentage >= 75) return { letter: 'B', color: '#6c5ce7', label: 'Good' };
if (percentage >= 60) return { letter: 'C', color: '#fdcb6e', label: 'Acceptable' };
if (percentage >= 40) return { letter: 'D', color: '#e17055', label: 'Suspicious' };
return { letter: 'F', color: '#d63031', label: 'Rejected' };
}
getRecommendation(percentage, breakdown) {
if (percentage >= 75) return { action: 'accept', message: 'Submission appears legitimate.' };
if (percentage >= 60) return { action: 'review', message: 'Submission requires manual review.' };
if (percentage >= 40) {
const issues = Object.entries(breakdown)
.filter(([, v]) => v.status === 'fail')
.map(([, v]) => v.reason);
return { action: 'challenge', message: `Issues detected: ${issues.join('; ')}. Consider CAPTCHA.` };
}
return { action: 'reject', message: 'Submission rejected due to multiple quality failures.' };
}
}
class EmailValidator {
async validate(email) {
const result = {
email,
formatValid: false,
dnsValid: false,
isDisposable: false,
isAlias: false,
domain: null,
error: null
};
// Layer 1: Local format validation (instant, no API call)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
result.formatValid = emailRegex.test(email);
if (!result.formatValid) {
result.error = 'Invalid email format';
return result;
}
result.domain = email.split('@')[1];
// Layer 2: Disify API check
try {
const response = await fetch(
`https://disify.com/api/email/${encodeURIComponent(email)}`,
{ signal: AbortSignal.timeout(5000) }
);
if (response.ok) {
const data = await response.json();
result.formatValid = data.format !== false;
result.dnsValid = data.dns !== false;
result.isDisposable = data.disposable === true;
result.isAlias = data.alias === true;
} else {
// API failure - fall back to format-only validation
result.dnsValid = true; // Assume valid
result.error = 'Disify API unavailable, using format-only validation';
}
} catch (error) {
result.dnsValid = true; // Assume valid on timeout
result.error = `Disify check failed: ${error.message}`;
}
return result;
}
}
class ContentValidator {
async validate(text, customBannedWords = []) {
const result = {
originalText: text,
cleanedText: text,
hasProfanity: false,
error: null
};
if (!text || text.trim().length === 0) {
return result;
}
try {
// Check for profanity
let url = `https://www.purgomalum.com/service/containsprofanity?text=${encodeURIComponent(text)}`;
if (customBannedWords.length > 0) {
url += `&add=${encodeURIComponent(customBannedWords.join(','))}`;
}
const containsResponse = await fetch(url, {
signal: AbortSignal.timeout(5000)
});
const containsText = await containsResponse.text();
result.hasProfanity = containsText.trim().toLowerCase() === 'true';
// If profanity detected, get the cleaned version
if (result.hasProfanity) {
let cleanUrl = `https://www.purgomalum.com/service/json?text=${encodeURIComponent(text)}`;
if (customBannedWords.length > 0) {
cleanUrl += `&add=${encodeURIComponent(customBannedWords.join(','))}`;
}
const cleanResponse = await fetch(cleanUrl, {
signal: AbortSignal.timeout(5000)
});
const cleanData = await cleanResponse.json();
result.cleanedText = cleanData.result;
}
} catch (error) {
result.error = `Content check failed: ${error.message}`;
// On failure, pass through without filtering
}
return result;
}
}
class GeoValidator {
constructor() {
this.cachedGeoData = null;
}
async validate() {
const result = {
ip: null,
country: null,
city: null,
isVPN: false,
isConsistent: true,
org: null,
error: null
};
try {
const response = await fetch('https://ipapi.co/json/', {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
// Might be rate limited (free tier: ~1000/day)
if (response.status === 429) {
result.error = 'IP geolocation rate limit exceeded';
result.isConsistent = true; // Give benefit of doubt
return result;
}
throw new Error(`IPapi error: ${response.status}`);
}
const data = await response.json();
this.cachedGeoData = data;
result.ip = data.ip;
result.country = data.country_name;
result.city = data.city;
result.org = data.org;
// Heuristic VPN/proxy detection based on org name
const vpnIndicators = [
'vpn', 'proxy', 'tor', 'hosting', 'cloud',
'digitalocean', 'amazon', 'google cloud', 'azure',
'linode', 'vultr', 'hetzner', 'ovh'
];
const orgLower = (data.org || '').toLowerCase();
result.isVPN = vpnIndicators.some(indicator =>
orgLower.includes(indicator)
);
// If using a known hosting/VPN provider, flag as potentially inconsistent
result.isConsistent = !result.isVPN;
} catch (error) {
result.error = `Geo check failed: ${error.message}`;
result.isConsistent = true; // Assume consistent on failure
}
return result;
}
getGeoData() {
return this.cachedGeoData;
}
}
Bots fill forms differently than humans. They paste content instantaneously, they do not move the mouse, they do not tab between fields, and they fill hidden honeypot fields. Our behavior tracker collects these signals and feeds them into the quality score.
class BehaviorTracker {
constructor(formElement) {
this.form = formElement;
this.startTime = Date.now();
this.hadMouseMovement = false;
this.hadKeystrokes = false;
this.hadFocusChanges = false;
this.mouseMovements = 0;
this.keystrokes = 0;
this.focusChanges = 0;
this.fieldInteractions = new Set();
this.attachListeners();
}
attachListeners() {
// Track mouse movement
this.form.addEventListener('mousemove', () => {
this.hadMouseMovement = true;
this.mouseMovements++;
}, { passive: true });
// Track keystrokes
this.form.addEventListener('keydown', (e) => {
// Ignore if it is a paste shortcut
if (e.ctrlKey || e.metaKey) return;
this.hadKeystrokes = true;
this.keystrokes++;
});
// Track focus changes between fields
this.form.addEventListener('focusin', (e) => {
if (e.target.name) {
this.fieldInteractions.add(e.target.name);
this.hadFocusChanges = true;
this.focusChanges++;
}
});
// Track paste events (high paste count with low keystrokes = suspicious)
this.form.addEventListener('paste', () => {
this.pasteCount = (this.pasteCount || 0) + 1;
});
}
getMetrics() {
return {
fillTimeMs: Date.now() - this.startTime,
hadMouseMovement: this.hadMouseMovement,
hadKeystrokes: this.hadKeystrokes,
hadFocusChanges: this.hadFocusChanges,
mouseMovements: this.mouseMovements,
keystrokes: this.keystrokes,
focusChanges: this.focusChanges,
fieldsInteracted: this.fieldInteractions.size,
pasteCount: this.pasteCount || 0,
keystrokeToFieldRatio: this.fieldInteractions.size > 0
? this.keystrokes / this.fieldInteractions.size
: 0
};
}
reset() {
this.startTime = Date.now();
this.hadMouseMovement = false;
this.hadKeystrokes = false;
this.hadFocusChanges = false;
this.mouseMovements = 0;
this.keystrokes = 0;
this.focusChanges = 0;
this.fieldInteractions.clear();
this.pasteCount = 0;
}
}
class ValidationGauntlet {
constructor(options = {}) {
this.emailValidator = new EmailValidator();
this.contentValidator = new ContentValidator();
this.geoValidator = new GeoValidator();
this.scoreEngine = new QualityScoreEngine();
this.options = {
validateEmail: true,
validateContent: true,
validateGeo: true,
trackBehavior: true,
customBannedWords: [],
minimumScore: 60,
...options
};
this.behaviorTracker = null;
}
attachToForm(formElement) {
if (this.options.trackBehavior) {
this.behaviorTracker = new BehaviorTracker(formElement);
}
}
async validateSubmission(formData) {
const validationResults = {};
// Run validations in parallel where possible
const promises = [];
if (this.options.validateEmail && formData.email) {
promises.push(
this.emailValidator.validate(formData.email)
.then(result => { validationResults.email = result; })
);
}
if (this.options.validateContent) {
const textFields = ['message', 'name', 'subject', 'comment'];
const allText = textFields
.map(field => formData[field] || '')
.filter(t => t.length > 0)
.join(' ');
if (allText.length > 0) {
promises.push(
this.contentValidator.validate(allText, this.options.customBannedWords)
.then(result => { validationResults.content = result; })
);
}
}
if (this.options.validateGeo) {
promises.push(
this.geoValidator.validate()
.then(result => { validationResults.geo = result; })
);
}
// Wait for all API validations to complete
await Promise.allSettled(promises);
// Add behavior metrics
if (this.behaviorTracker) {
validationResults.behavior = this.behaviorTracker.getMetrics();
}
// Calculate quality score
const qualityScore = this.scoreEngine.calculate(validationResults);
return {
isValid: qualityScore.percentage >= this.options.minimumScore,
qualityScore,
validationResults,
timestamp: new Date().toISOString()
};
}
}
class ValidationGauntletUI {
constructor(rootId) {
this.root = document.getElementById(rootId);
this.gauntlet = new ValidationGauntlet({
customBannedWords: ['spam', 'scam', 'buy now'],
minimumScore: 60
});
this.render();
}
render() {
this.root.innerHTML = `
Validation Gauntlet
Multi-layer form validation with real-time quality scoring
`;
const form = this.root.querySelector('#gauntletForm');
this.gauntlet.attachToForm(form);
// Real-time email validation on blur
const emailInput = form.querySelector('[name="email"]');
let emailDebounce;
emailInput.addEventListener('blur', () => {
clearTimeout(emailDebounce);
if (emailInput.value.trim()) {
this.validateEmailRealTime(emailInput.value.trim());
}
});
// Real-time content check on blur
const messageInput = form.querySelector('[name="message"]');
messageInput.addEventListener('blur', () => {
if (messageInput.value.trim()) {
this.checkContentRealTime(messageInput.value.trim());
}
});
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Check honeypot
const honeypot = form.querySelector('[name="website"]');
if (honeypot.value) {
// Bot detected - silently "accept" to avoid revealing detection
this.showFakeSuccess();
return;
}
const formData = {
name: form.querySelector('[name="name"]').value,
email: form.querySelector('[name="email"]').value,
subject: form.querySelector('[name="subject"]').value,
message: form.querySelector('[name="message"]').value
};
await this.runGauntlet(formData);
});
}
async validateEmailRealTime(email) {
const status = this.root.querySelector('#emailStatus');
status.innerHTML = 'Checking email...';
try {
const result = await this.gauntlet.emailValidator.validate(email);
if (!result.formatValid) {
status.innerHTML = 'Invalid email format';
} else if (result.isDisposable) {
status.innerHTML = 'Disposable email detected - please use a permanent address';
} else if (!result.dnsValid) {
status.innerHTML = 'Email domain does not exist';
} else {
status.innerHTML = 'Email looks valid';
}
} catch {
status.innerHTML = '';
}
}
async checkContentRealTime(text) {
const status = this.root.querySelector('#contentStatus');
try {
const result = await this.gauntlet.contentValidator.validate(text);
if (result.hasProfanity) {
status.innerHTML = 'Inappropriate content detected - please revise';
} else {
status.innerHTML = 'Content looks clean';
}
} catch {
status.innerHTML = '';
}
}
async runGauntlet(formData) {
const submitBtn = this.root.querySelector('#submitBtn');
const resultsDiv = this.root.querySelector('#validationResults');
submitBtn.disabled = true;
submitBtn.textContent = 'Running Validation Gauntlet...';
resultsDiv.innerHTML = `
Running 5-layer validation pipeline...
`;
try {
const result = await this.gauntlet.validateSubmission(formData);
this.renderResults(result);
} catch (error) {
resultsDiv.innerHTML = `
Validation error: ${error.message}
`;
}
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Through Gauntlet';
}
renderResults(result) {
const { qualityScore, validationResults, isValid } = result;
const resultsDiv = this.root.querySelector('#validationResults');
resultsDiv.innerHTML = `
${qualityScore.grade.letter}
${qualityScore.percentage}% Quality Score
${qualityScore.grade.label} ·
${qualityScore.recommendation.action.toUpperCase()}
${qualityScore.recommendation.message}
Validation Breakdown
${Object.entries(qualityScore.breakdown).map(([key, data]) => `
${data.status === 'pass' ? '✔' : data.status === 'fail' ? '✘' : '?'}
${this.formatLabel(key)}
${data.reason ? `
${data.reason}
` : ''}
${data.score}/${this.getMaxForKey(key)}
`).join('')}
${validationResults.geo ? `
GEO INTELLIGENCE
${validationResults.geo.country ? `
Country: ${validationResults.geo.country}
` : ''}
${validationResults.geo.city ? `
City: ${validationResults.geo.city}
` : ''}
${validationResults.geo.org ? `
ISP/Org: ${validationResults.geo.org}
` : ''}
VPN Detected:
${validationResults.geo.isVPN ? 'Yes' : 'No'}
` : ''}
${validationResults.behavior ? `
BEHAVIOR ANALYSIS
Fill Time: ${(validationResults.behavior.fillTimeMs / 1000).toFixed(1)}s
Keystrokes: ${validationResults.behavior.keystrokes}
Mouse Movements: ${validationResults.behavior.mouseMovements}
Fields Interacted: ${validationResults.behavior.fieldsInteracted}
` : ''}
`;
}
formatLabel(key) {
const labels = {
emailFormat: 'Email Format',
emailDomain: 'Email Domain DNS',
emailDisposable: 'Disposable Email Check',
contentProfanity: 'Content Filter',
geoConsistency: 'Geographic Consistency',
behavior: 'Behavior Analysis'
};
return labels[key] || key;
}
getMaxForKey(key) {
const weights = {
emailFormat: 10, emailDomain: 15, emailDisposable: 25,
contentProfanity: 20, geoConsistency: 15, behavior: 15
};
return weights[key] || 0;
}
showFakeSuccess() {
const resultsDiv = this.root.querySelector('#validationResults');
resultsDiv.innerHTML = `
Thank you for your submission!
We will get back to you shortly.
`;
// In reality, this submission is silently discarded
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
new ValidationGauntletUI('app-root');
});
Every layer of validation introduces the risk of false positives: legitimate submissions flagged as suspicious. Disify might not have a new email provider in its database and mark it as unknown. PurgoMalum might flag a legitimate use of a word that has innocent and profane meanings (the Scunthorpe problem, named after the English town that famously gets caught by profanity filters). IPapi might identify a corporate VPN as suspicious when it is just a normal office worker.
The solution is the quality score system. No single validation failure should automatically reject a submission. Instead, each check contributes points to a composite score, and only submissions with multiple failures cross the rejection threshold. This tolerance for individual false positives dramatically reduces the overall false positive rate while maintaining strong protection against genuine abuse.
Disify maintains a database of known disposable email providers, but new disposable email services appear constantly. A brand-new throwaway service that launched last week will not be in Disify's database yet. For high-value forms, supplement Disify with your own growing list of known disposable domains. Track which domains get associated with spam submissions over time and add them to a local blocklist.
The free tier of IPapi allows approximately 1,000 requests per day per IP. For a form that gets a hundred submissions per day, this is plenty. For high-traffic forms, you will need to cache the geo lookup result for the user's session (they are unlikely to change IP mid-session) and implement graceful degradation when the limit is hit. Our code already handles this by setting isConsistent = true on rate limit errors.
PurgoMalum's profanity database is comprehensive but occasionally overreaches. Compound words, technical terms, and non-English text can trigger false positives. The add parameter lets you add custom words to the filter, but there is no remove parameter to whitelist legitimate words that get caught. For forms that need to handle technical vocabulary (medical terms, biological terminology), consider building a pre-processing step that replaces known-safe flagged words with placeholders before sending to PurgoMalum, then restoring them after.
class SafeWordBypass {
constructor() {
this.safeWords = new Map([
['scunthorpe', '__SAFE_TOWN_1__'],
['assessment', '__SAFE_WORD_2__'],
['specialist', '__SAFE_WORD_3__']
]);
}
protect(text) {
let processed = text;
for (const [word, placeholder] of this.safeWords) {
processed = processed.replace(new RegExp(word, 'gi'), placeholder);
}
return processed;
}
restore(text) {
let processed = text;
for (const [word, placeholder] of this.safeWords) {
processed = processed.replace(new RegExp(placeholder, 'g'), word);
}
return processed;
}
}
Our honeypot field is a hidden input that bots fill out but humans cannot see. However, modern bots are getting smarter about honeypots. Some check the CSS visibility of fields before filling them. To make the honeypot more robust, use CSS positioning (position: absolute; left: -9999px) rather than display: none, as the latter is easier for bots to detect. Give the honeypot a plausible name like "website" or "url" that bots expect to see in forms.
SaaS companies lose time and money on fake signups that consume trial resources without converting. The Validation Gauntlet blocks disposable emails (which almost never convert), detects bot signups (which inflate metrics), and maintains clean data that makes user analytics meaningful.
Business contact forms receive enormous volumes of spam. A law firm might get fifty spam submissions for every legitimate inquiry. The multi-layer approach catches automated spam (behavior tracking), manual spam (profanity filtering), and semi-automated spam (disposable email detection), reducing the noise that customer-facing staff has to wade through.
Forums, comment sections, and review platforms need content filtering at scale. PurgoMalum integration provides the first layer of content moderation, catching obvious profanity before human moderators need to review. The quality score can prioritize moderation queues, surfacing low-scoring submissions for immediate review while auto-approving high-scoring ones.
Fraudulent accounts on e-commerce platforms cost money through fake reviews, promotion abuse, and return fraud. Geographic validation catches mismatches between claimed location and IP location. Disposable email detection prevents users from creating multiple accounts for promotion abuse. Behavior tracking identifies automated account creation tools.
Email marketing depends on list quality. A newsletter with thirty percent disposable email addresses has inflated subscriber counts, low open rates, and deliverability problems. Running signups through the Disify layer alone can significantly improve list quality and the metrics that depend on it.
The three validation APIs are independent, so we run them in parallel using Promise.allSettled. This means the total validation time is equal to the slowest API response, not the sum of all responses. Typically, the entire gauntlet completes in two to four seconds, which is acceptable for form submissions.
Do not wait for form submission to start validation. Validate the email on blur (when the user leaves the email field) and check content as sections are completed. This gives real-time feedback and reduces the perceived wait on final submission because some checks are already cached.
PurgoMalum is fast enough for near-real-time checking, but hitting it on every keystroke is wasteful. Debounce content checks by 500ms so the API is only called after the user stops typing. For email validation, trigger on blur rather than on input to avoid checking incomplete addresses.
A user's IP and geographic data do not change during a single session. Cache the IPapi result on first load and reuse it for all subsequent form submissions. This is especially important given the 1,000-per-day rate limit on the free tier.
Train a simple Naive Bayes classifier on your own form submission data. After collecting enough labeled examples of spam and legitimate submissions, the classifier can assign a spam probability that supplements the API-based checks. This self-improving layer gets better over time as your training data grows.
Add phone number validation using the NumVerify API or similar services. Cross-reference the phone number's country with the IP geolocation and email domain's registration country for multi-signal consistency checking.
Instead of simply rejecting low-scoring submissions, escalate them to a CAPTCHA challenge. Submissions scoring between 40% and 60% get a CAPTCHA. Below 40% gets rejected. Above 60% passes through. This balances security with user experience by only inconveniencing suspicious submissions.
Build a dashboard that visualizes submission quality over time. Track the distribution of quality scores, the most common failure reasons, and trends in submission patterns. This data helps you tune the scoring weights and identify new attack patterns early.
When a submission passes the gauntlet, send it to a webhook endpoint. This enables integration with any backend: Slack notifications for new leads, CRM entries for qualified contacts, or email notifications for urgent inquiries. The quality score metadata travels with the submission, so downstream systems can make their own decisions about how to handle different quality levels.
The minimum score threshold (60% in our default configuration) is a tunable parameter with significant business impact. Too high and you reject legitimate submissions. Too low and you let spam through. Build A/B testing capability that runs different thresholds for different traffic segments and measures the impact on both submission quality and conversion rate. The optimal threshold depends on your specific form's traffic patterns and is best discovered empirically.
The Validation Gauntlet demonstrates that form security is not a binary proposition. It is not about whether a form is validated or not; it is about how many layers of validation, how they interact, and how they degrade gracefully when individual layers fail. A single disposable email check is trivially bypassed. A single profanity filter is easily fooled. A single behavior check misses sophisticated bots. But all three, combined with local heuristics and a composite quality score, create a defense that is meaningfully harder to circumvent than any single layer.
The technical insight is about score composition. Each validation check is imperfect. Each has false positives and false negatives. But when you combine imperfect signals through a weighted scoring system, the composite signal is dramatically more accurate than any individual component. This is the same principle behind spam filters, credit scores, and medical diagnostics: multiple weak signals, properly combined, yield a strong signal.
The practical insight is about proportionality. Not every form needs every layer. A newsletter signup needs Disify but probably does not need IPapi. A support ticket form needs PurgoMalum but probably does not need behavior tracking. Match your validation layers to your threat model. Over-validating is as bad as under-validating because it creates friction for legitimate users without proportional security benefit.
The business insight is about data quality as an investment. Every fake submission that enters your database creates downstream costs: wasted sales outreach, inflated metrics that lead to bad decisions, email deliverability problems, and customer support noise. The Validation Gauntlet is cheap to implement (all three APIs are free) and the return on investment materializes every time a salesperson does not waste twenty minutes following up on a disposable email address.
Go build a gauntlet for your forms. The APIs are free. The bots are not going away. And every clean database starts with the decision to stop accepting garbage at the front door.