All projects

Production

AI Spam Detection for Web Forms

Two-stage Claude-powered spam filtering built directly into the CMS HTTP intercept pipeline

Java 21Spring Boot 3.5Spring AIClaude Haiku
AI Spam Detection for Web Forms - abstract illustration

The problem

Contact form spam is solved badly by most rule-based filters. Keyword blocklists create false positives. CAPTCHAs degrade UX. IP blocklists miss shared-IP attacks. The CMS already had a honeypot field, but sophisticated bots fill real fields too. What I wanted was a filter that reads the actual submission content and makes a judgment call - the same way a person would.

Two-stage architecture

There are two independent checks. The first is a synchronous email-only check that runs as an HTTP intercept before the form handler executes. It sends just the email address to Claude Haiku and gets back a single word - SPAM or CLEAN. This catches obviously fake, disposable, or placeholder addresses before any form processing happens. The second check is a full-content analysis that runs asynchronously after the contact message is already saved to the database. It sends the name, email, message body, and any additional form fields to Claude and checks whether the submission reads as spam, promotional, or bot-generated. If spam is confirmed, the message is flagged in the database and no email is sent.

How it hooks into the HTTP pipeline

The email check is implemented as a CMS intercept class that sits in the intercept chain before ContactFormIntercept, MailingListSignupIntercept, and several others. It stores the result as a servlet request attribute. Downstream intercepts read that attribute and short-circuit if the value marks the email as spam. This keeps the spam logic decoupled from the form-handling logic - any intercept can consume the result without knowing how it was produced. The async contact check uses a bounded queue that a background thread drains continuously. The contact message is written to the database immediately with a pending-check marker; the background thread updates that marker once the check completes.

Cost and failure design

Claude Haiku with a single-word token budget is about as cheap as an AI API call gets - the model only needs room for one word. The more interesting design choice is the failure behavior. Both checks fail open: if the Claude API call fails after retries (with exponential backoff and jitter), the submission is treated as clean. For the async contact check, a failed API call causes a warning prefix to be prepended to the email body before it's sent, so I know the check was skipped. The alternative - failing closed on API errors and dropping legitimate messages - would be worse than the spam itself.

Audit logging

Every API call, successful or not, is written to an audit log table. Each row records the model, input and output token counts, response text, duration, success flag, error message if failed, retry attempt number, and calculated cost using per-model token pricing from config. This gives visibility into how much the spam filter costs per day and lets me audit any submission where the AI result looks wrong.

Configuration

Spam detection is enabled per-site via a YAML config flag, so the same CMS backend can run sites with and without it. Individual contact forms can opt out via a skip flag in the page config - useful for internal forms where submissions are always trusted. The Claude model and retry settings are configured in the shared app config, separate from the per-site toggle.

Stack

Java 21Spring Boot 3.5Spring AIClaude Haiku

Want something like this built?

This is the kind of system I build for clients. Tell me what you're trying to do, and I'll give you a straight answer on whether AI is the right tool.

Book a 30-minute call