Netlify Bot Protection: From Monitoring to Enforcement
WebDecoy's Netlify extension now protects selected paths. Install the sensor, enable clearance checks, verify the deployment, and roll out in Monitor mode.
integrationConnect WebDecoy from the Vercel Marketplace to see which crawlers reach your site and which are forged. A log drain does the work: no code, no redeploy.
WebDecoy is now on the Vercel Marketplace. Press Connect Account, choose your projects, and WebDecoy starts reporting the automated traffic reaching your site on the very next request. No package to install, no middleware to add, no redeploy.
The listing makes one promise: WebDecoy tells you which automated traffic reaches your site, and whether it is really who it claims to be. This post covers how the install keeps that promise, and then walks through everything else WebDecoy can do on a Vercel project, from the zero-code drain to the Next.js middleware to rules in your Vercel Firewall.
That is the whole install. WebDecoy provisions a log drain, and the next request your site answers is reported.
To see it work, request your robots file from a terminal:
curl https://your-site.com/robots.txtA detection for cURL appears on the Detections page a few seconds later.
| Capability | What it does | Requires |
|---|---|---|
| Log drain detection | Reports crawlers, tools, and requests with no user agent, from the requests Vercel’s edge answered | Vercel Pro or Enterprise |
| Non-JavaScript visibility | Sees GPTBot, ClaudeBot, CCBot, cache hits, and static files that never invoke a function | Drain |
| Crawler identity verification | Separates verified crawlers from forged ones using the same name | Drain or middleware |
| JA4 TLS fingerprints | Records the TLS fingerprint Vercel’s edge measured for each detected request | Drain |
| Cache status | Records whether each crawler request was served from Vercel’s cache | Drain |
| Signed deliveries | Rejects any delivery not signed with your drain’s own secret | Drain |
| Human traffic discarded | Drops ordinary visitors before storage and strips query strings | Drain |
| Honest coverage reporting | Flags limited coverage when client IPs are hidden, instead of showing a quiet site | Drain |
| Site per project | Maps each Vercel project to its own WebDecoy site, created from its domain if you like | Any plan |
| Nothing written to your project | Creates one log drain and changes nothing else: no code, no variables, no redeploy | Any plan |
| Clean removal | Deletes exactly what it created, by the ids Vercel returned | Any plan |
| Next.js middleware | @webdecoy/nextjs scores requests before your routes run, in monitor or enforce mode | Any plan, Hobby included |
| In-process rules | Rate limits, IP filters, tripwires, bot categories, attack signatures, Web Bot Auth | Middleware |
| Shared rate limits | An Upstash Redis store keeps limits consistent across every Vercel instance | Middleware and Upstash |
| One bot policy | botPolicy() serves the robots.txt it actually enforces | Middleware |
| Honeytoken link | One line in your root layout plants a link only automation follows | Middleware |
| Self-hosted captcha | Proof-of-work captcha as an App Router route, with no third-party script | Middleware |
| OpenTelemetry spans | Detection spans in the tracer you already run | Middleware |
| Test trigger | A reserved user agent fires a labeled test detection, even on next dev | Middleware |
| Vercel Firewall rules | Block or challenge an IP in your project’s firewall, recorded, read back, and expired on time | Vercel API token |
| Firewall connection test | Reads your project’s firewall config and reports Vercel’s own answer | Vercel API token |
The rest of this post is the detail behind each row.
A script tag only sees visitors that run JavaScript. The crawlers most site owners now care about do not. GPTBot, ClaudeBot, CCBot, and PerplexityBot fetch your HTML and leave. Googlebot’s crawl pass is a plain HTTP fetch, with rendering deferred to a separate pass that may never happen for a given URL. None of them ever run your tag.
A Vercel log drain has no such blind spot. Vercel’s edge records every request it answers, including responses served straight from its cache and static files that never invoke a function, and pushes those records to WebDecoy. The drain sees traffic your application code never does.
WebDecoy creates one drain per installation, scoped to the projects you connected. Connect another project and the drain widens. Disconnect one and it narrows. Uninstall and it is deleted. Before the drain goes live, WebDecoy validates its configuration with Vercel’s drain test, so a problem shows up during connect instead of as silence afterwards.
It asks for request logs only. Build output is filtered out on Vercel’s side, so you are not paying to deliver logs WebDecoy would throw away, and it never asks for traces, Web Analytics, Speed Insights, or audit logs.
A drained request becomes a detection when any of these is true:
/robots.txt, /sitemap.xml, /llms.txt, /.well-known/ai.txt, or /ads.txt.Everything else is ordinary browser traffic, and it is dropped before anything is stored. A drain carries every human visitor too. WebDecoy does not keep them. Query strings are removed from what is kept, because the path is what identifies what was asked for.
A user agent is a claim, and a free one. Anyone can put Googlebot in a header.
The drain hands WebDecoy the client IP exactly as Vercel’s edge saw it, which is not something the client can set. WebDecoy checks every crawler claim against what the operator publishes: its IP ranges and forward-confirmed reverse DNS. Each crawler request comes out as one of three answers:
| Answer | Meaning |
|---|---|
| Verified | The request came from where the named operator says its crawler lives |
| Claimed | The name is a known agent, but nothing proves it either way |
| Forged | The operator publishes where its crawler runs, and this request came from somewhere else |
Forged crawlers are the interesting ones. Something that dresses up as Googlebot to get past your rules is telling you what it intends to do.
Each drained request carries a JA4 digest: the TLS fingerprint Vercel’s edge computed during the handshake. WebDecoy records it with the detection. A script can write any user agent it likes, but its TLS handshake is produced by the library it actually runs on, and the fingerprint sits right next to the claim it confirms or contradicts.
The cache result is recorded too, so you can tell whether a crawler was served from Vercel’s cache or reached your functions.
Every delivery is signed by Vercel with a secret WebDecoy generated for your drain alone. WebDecoy checks the signature in constant time and rejects anything unsigned or mis-signed.
A correctly signed delivery is always acknowledged, even one full of traffic that is not worth keeping. Vercel disables a drain that fails too often, and a drain that quietly switched itself off would look exactly like a site with no bots.
Two settings on your side can thin what a drain carries. WebDecoy tells you about both instead of showing you a quiet site.
The dashboard only says a site is reporting when a drain is provisioned and a project is linked. Authorizing the integration is not the same as receiving traffic, and WebDecoy never treats it as if it were.
Vercel offers log drains on Pro and Enterprise teams, and its Pro trial does not include them. On Hobby, the connect still completes, WebDecoy tells you drains are not available on your plan, and the Next.js middleware below is the way in.
Three scopes, and nothing else:
| Scope | Why |
|---|---|
| Projects, read | To list the projects you choose between |
| Project environment variables, read and write | Only to remove variables an earlier version wrote |
| Drains, read and write | To create its own drain, and delete it when you disconnect |
It cannot read your source, your deployments, your domains, or any logs beyond the drain it created.
Connecting writes nothing to your project. Earlier versions of the install set three environment variables (WEBDECOY_SITE_KEY, WEBDECOY_SCANNER_ID, and WEBDECOY_SENSOR_KEY) that nothing ever read, and the drain made them unnecessary. If your project was connected before that changed, disconnecting it removes them, and that cleanup is the only reason the environment variable scope is still requested.
Disconnect a project and WebDecoy narrows the drain to the projects that remain. On a project connected by an earlier version, it also deletes the variables that version wrote, by the ids Vercel gave them. A variable with the same name that you set yourself is left alone.
Disconnect the integration in WebDecoy and it deletes the drain and uninstalls itself from your Vercel account. We do not offer an install we cannot cleanly reverse.
The drain reports after a request is answered. When you want a decision before your route runs, or you are on a Hobby team, add @webdecoy/nextjs. It runs in Next.js middleware on Vercel, including the Edge runtime.
npm install @webdecoy/nextjs @webdecoy/node// middleware.ts
import { withWebDecoy } from '@webdecoy/nextjs';
import { NextResponse } from 'next/server';
export default withWebDecoy({
apiKey: process.env.WEBDECOY_API_KEY!,
threshold: 70,
mode: 'monitor', // switch to 'enforce' once you have read what it would block
onBlocked: (request, detection) =>
NextResponse.json({ error: 'Blocked', threat: detection.threat_level }, { status: 403 }),
});
export const config = {
matcher: ['/api/:path*', '/login', '/checkout/:path*'],
};Create the API key under Settings → API Keys in WebDecoy and add it to your project as WEBDECOY_API_KEY.
Three defaults matter on Vercel:
mode: 'enforce'. Watch first, then act.X-Forwarded-For from the right, trusting exactly one proxy by default, which is Vercel itself. A client cannot pick its own address by sending the header.To protect one route instead of a whole matcher, wrap the handler with withBotProtection.
The middleware takes the same rules as the rest of the SDK, evaluated inside the request with no round trip:
| Rule | What it does |
|---|---|
rateLimit() | Request limits per client, over a fixed or sliding window |
filter() | Expressions over request and IP facts, such as ip.vpn or ip.tor or a country list |
tripwire() | Paths no human can reach, so any request for one is automated by construction |
bots() | Act on self-identified agents by category, such as training_crawler, with exceptions |
attackSignatures() | A small, curated set of unambiguous injection and traversal payloads |
webBotAuth() | Verifies cryptographically signed agents using Web Bot Auth |
Shared rate limits. Vercel runs your middleware on many instances at once, so an in-memory counter only sees a slice of the traffic. upstashRateLimitStore() keeps the counters in Upstash Redis over HTTP, which works on the Edge runtime, and Upstash is one click away on the Vercel Marketplace:
import { rateLimit, upstashRateLimitStore } from '@webdecoy/node';
rateLimit({
max: 100,
window: 60,
store: upstashRateLimitStore({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
}),
});One bot policy. botPolicy() builds your robots.txt from the same object the middleware enforces. A robots file that disallows GPTBot while your code lets it through is a policy you believe is in force and is not. One source means the two cannot drift.
honeytokenLink() derives a hidden link from your API key, so every instance and every render agrees on the same path without coordinating. Put it in your root layout and arm it as a tripwire in middleware. Humans never see or follow it. A crawler that does has told you exactly what it is. It is a helper rather than something injected automatically, because Next.js streams App Router responses and there is no finished document for middleware to rewrite.
createCaptchaHandler() mounts WebDecoy’s proof-of-work captcha as a catch-all App Router route, paired with the @webdecoy/client widget. Scoring runs in your own deployment, with no third-party script on your page and no call out to a captcha vendor.
The SDK emits OpenTelemetry spans through the tracer you pass it, without taking a dependency on OpenTelemetry itself, so detection time shows up in the traces you already collect.
To verify an install, send the reserved test user agent to a route your matcher covers:
curl -A "WebDecoy-Test/1.0" http://localhost:3000/api/helloIt fires a real detection through the whole pipeline, works on next dev before you deploy, and appears labeled Test in the dashboard, excluded from stats and billing.
Detection tells you who is there. When you want Vercel itself to act, connect your Vercel Firewall as an enforcement target. Vercel publishes no firewall scope for marketplace integrations, so this takes a Vercel API token, and it lives under Enforcement targets on the Integrations page, separate from the marketplace install under Detection sources.
Press Test and WebDecoy reads your project’s firewall config from Vercel, writing nothing. If Vercel refuses, you see Vercel’s own reason. A 403 can mean a bad token, a token without access to the project, or a plan that does not expose the firewall, and those are three different fixes, so WebDecoy does not guess which one it is.
WebDecoy writes IP rules into your project’s firewall with one of three actions:
| Action | What Vercel does |
|---|---|
| Block | Denies the IP |
| Challenge | Serves Vercel’s challenge, so a real browser can pass |
| Log only | Records the event and changes nothing |
Apply a rule yourself. From the Vercel Firewall page in WebDecoy, block or challenge an address for a set length of time. You choose; WebDecoy carries it out. The control appears when your firewall integration is set to block or challenge; one set to log only is configured to record, not act, so it is never offered as protection. A few guardrails come with it:
Every change is proven, not assumed. Manual or automatic, each rule goes through the same path:
One caution. Disable a firewall integration rather than deleting it if you want its rules to keep expiring. Deleting it takes the API token with it, so WebDecoy can no longer remove the rules it applied. Reverse the ones you want gone first, or remove them in the Vercel dashboard afterwards.
The evidence reads the two protective actions the right way round. Traffic from an IP after a block means the rule is not holding. Traffic after a challenge means a client passed it.
Automatic rules are gated deliberately. WebDecoy withholds firewall changes driven by a score until that score has a measured false-positive rate behind it. It fails closed, because a bot product that blocks your customers is worse than none. A rule you apply yourself rests on your decision, so it is not held back.
WebDecoy’s Free plan includes 5,000 stored events a month with seven days of detail, needs no card, and does not expire. The drain stores only automated traffic, so your human visitors do not count toward it. Your paid-capability trial starts when WebDecoy verifies your first real installation, not when you create an account.
On the Vercel side, drain delivery is billed as part of your plan’s usage. That is why WebDecoy asks for request logs only and filters out build output.
From Vercel: open the WebDecoy listing and press Connect Account.
From the docs: the Vercel installation guide covers the drain, and the Next.js middleware guide covers the SDK.
The drain and the middleware complement each other. The drain sees everything Vercel’s edge answers, including the crawlers that never touch your code. The middleware decides inside the request, on the routes you choose. Run the drain for visibility, then add the middleware where a decision has to happen before the response.
Open the WebDecoy listing on the Vercel Marketplace and press Connect Account, or go to vercel.com/integrations/webdecoy/new. Choose your team and projects, let Vercel send you back to WebDecoy, match each project to a WebDecoy site, and press Connect. WebDecoy provisions a log drain and the next request your site answers is reported. There is no package to install and no redeploy.
No. The marketplace install is monitoring only. It reads the log drain Vercel sends after each request has already been answered, so it never sits in the request path and never blocks, redirects, or challenges anyone. Blocking is something you opt into separately, through the Next.js middleware in enforce mode or through rules in your Vercel Firewall.
Log drains are available on Vercel Pro and Enterprise teams. Vercel's Pro trial does not include them. On a Hobby team the connect still completes, WebDecoy tells you drains are not available on your plan, and the @webdecoy/nextjs middleware gives you detection instead.
No. A log drain carries every request your site answers, including your customers. WebDecoy keeps only requests from known crawlers and tools, requests with no user agent, and requests for crawler-only paths such as /robots.txt. Ordinary browser traffic is dropped before anything is stored, and query strings are removed from what is kept.
Three scopes: projects (read), project environment variables (read and write), and drains (read and write). It writes nothing to your project, and it cannot read your source code, your deployments, your domains, or any logs beyond the drain it created. Disconnecting removes the drain.
Yes. A user agent is only a claim. The drain passes WebDecoy the client IP that Vercel's edge saw, and WebDecoy checks each crawler claim against the operator's published IP ranges and forward-confirmed reverse DNS. Each crawler request is reported as claimed, verified, or forged.
WebDecoy's Netlify extension now protects selected paths. Install the sensor, enable clearance checks, verify the deployment, and roll out in Monitor mode.
integrationDeploy WebDecoy's edge sensor to Cloudflare in one click. Detect the crawlers a JavaScript tag can't see, and enforce before they hit your origin.
integrationFrom one script tag to a rotation-proof lockout: the setup, the visitor experience, and the closed loop across a Cloudflare Worker and AWS WAF.
integrationLike this post? Share it with your friends!
Get a personalized demo from our team.