@agentmarketing/payload-spam-filtering
v1.0.0
Published
Payload spam filtering using Google Gemini (free tier)
Readme
Payload Spam Filtering
We built this for our own Payload sites and decided to share it. It uses Google’s free-tier Gemini API to flag spam on form submissions before anything hits your database—so you get AI-based filtering without extra cost.
By Shane Farmer / Agent Marketing
Questions or issues? Email [email protected].
What it does
- Runs spam checks on form data before you save or email it.
- Uses Gemini’s free tier (you only need an API key).
- Lets you choose how strict the filter is (low / medium / high).
- Can use your business context so “contact us” forms are judged in the right way.
- Keeps a blocklist of known spam so repeat submissions don’t call the API again.
- Optional rate limiting so submissions can’t be sent too quickly.
Install
npm install @agentmarketing/payload-spam-filteringTo update later:
npm update @agentmarketing/payload-spam-filteringQuick setup
1. Add to .env
GEMINI_API_KEY=your_gemini_api_key_here
SPAM_DETECTION_BUSINESS_CONTEXT=Your business description hereGet a key from Google AI Studio (free tier is fine).
2. Use it in your form handler
In the function that handles the form (e.g. src/lib/submitForm.ts):
import { checkForSpam } from '@agentmarketing/payload-spam-filtering'
export async function submitForm(form_data) {
const spamCheckResult = await checkForSpam(form_data, { strictness: 2 })
if (spamCheckResult.isSpam) {
return {
error: "Your message appears to be spam and cannot be submitted.",
spamDetected: true
}
}
// Your normal form processing...
}3. Show errors in the UI
Use the same spamDetected / error you return above (e.g. in a toast or message under the form).
Strictness levels
- Low (1) – Only obvious spam (scams, “BUY NOW”, etc.). Good if you want to block the worst and let most through.
- Medium (2) – Default. Catches promotional and marketing-style messages as well.
- High (3) – Strict. Best when you pass
businessContextso only genuine enquiries get through.
Example with business context:
await checkForSpam(form_data, {
strictness: 3,
businessContext: "Digital marketing agency – SEO, PPC, web design"
})You can also use the helpers:
import { spamDetection } from '@agentmarketing/payload-spam-filtering'
await spamDetection.low(form_data)
await spamDetection.medium(form_data)
await spamDetection.high(form_data, "Your business context")Rate limiting
To stop people sending submissions too fast (e.g. one every 5 seconds):
const result = await checkForSpam(form_data, {
minSubmissionIntervalSeconds: 5,
rateLimitKey: request.headers.get('x-forwarded-for') ?? undefined, // optional: per-IP
})
if (result.rateLimited) {
// result.reasoning has a message like "Too many submissions. Please try again in 4 seconds."
}You can set SPAM_MIN_SUBMISSION_INTERVAL_SECONDS=5 in .env instead if you prefer.
Spam blocklist
When Gemini marks something as spam, we store a hash of the content in a blocklist folder. Next time the same (or very similar) content is submitted, it’s blocked without calling Gemini—saves quota and speeds things up.
- Default folder:
{storagePath}/blocklist(usually under./public/spam-detection/blocklist). - Override with
spamBlocklistPathin options orSPAM_BLOCKLIST_PATHin env. - No extra config needed; it’s used automatically.
Optional API route (view results / stats)
If you want an endpoint to see recent results or stats:
Create src/app/api/spam-detection/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { StorageService } from '@agentmarketing/payload-spam-filtering'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const submissionId = searchParams.get('submissionId')
const limit = searchParams.get('limit')
const stats = searchParams.get('stats')
const storageService = new StorageService('./public/spam-detection')
await storageService.initialize()
if (submissionId) {
const result = await storageService.getSpamDetectionResult(submissionId)
return NextResponse.json({ result })
}
if (stats === 'true') {
const statistics = await storageService.getSpamStatistics()
return NextResponse.json({ statistics })
}
const limitNum = limit ? parseInt(limit, 10) : 50
const results = await storageService.getRecentResults(limitNum)
return NextResponse.json({ results, count: results.length })
}GET /api/spam-detection?limit=50– recent resultsGET /api/spam-detection?submissionId=123– one resultGET /api/spam-detection?stats=true– totals and spam rate
Config reference
| Option | Description |
|--------|-------------|
| geminiApiKey | Override API key (default: GEMINI_API_KEY env) |
| geminiModel | 'free' (default) or a model id e.g. gemini-2.0-flash |
| storagePath | Where to store results (default: ./public/spam-detection) |
| enabled | Turn detection on/off |
| strictness | 1 | 2 | 3 (low / medium / high) |
| businessContext | Short description of your business for strict mode |
| minSubmissionIntervalSeconds | Rate limit interval (e.g. 5) |
| rateLimitKey | Optional key (e.g. IP) for per-client limiting |
| spamBlocklistPath | Override blocklist folder |
Env vars
GEMINI_API_KEY=...
GEMINI_MODEL=free
SPAM_DETECTION_STORAGE_PATH=./public/spam-detection
SPAM_DETECTION_ENABLED=true
SPAM_DETECTION_BUSINESS_CONTEXT=Your business description
SPAM_MIN_SUBMISSION_INTERVAL_SECONDS=5
SPAM_BLOCKLIST_PATH=./data/spam-blocklistGEMINI_MODEL=free uses the current free-tier model so you don’t have to change code when Google updates it.
License
MIT.
Contact
Agent Marketing · [email protected]
