---
title: How to build a honeypot with Vercel Web Application Firewall
description: Learn how to build a honeypot with Vercel Web Application Firewall (WAF) that catches bots ignoring your robots.txt. Capture offending IPs and block them automatically with Vercel WAF Custom Rules and Persistent Actions.
url: /kb/guide/how-to-build-a-honeypot-with-vercel-web-application-firewall
canonical_url: "https://vercel.com/kb/guide/how-to-build-a-honeypot-with-vercel-web-application-firewall"
published: 2026-08-18
last_updated: 2026-08-18
authors: Nic Dillon, Phil Zona
related:
  - /docs/vercel-firewall/vercel-waf/managed-rulesets
  - /docs/botid/get-started
  - /docs/rbac/access-roles
  - /docs/frameworks/full-stack
  - /docs/rest-api
  - /docs/cron-jobs
  - /docs/vercel-firewall/vercel-waf/ip-blocking
  - /docs/vercel-firewall/vercel-waf/custom-rules
  - /docs/vercel-firewall/firewall-concepts
  - /docs/vercel-firewall/vercel-waf/rate-limiting
  - /docs/drains/reference/logs
  - /changelog/block-rate-limit-and-challenge-traffic-with-the-vercel-firewall
  - /docs/vercel-firewall
  - /docs/rest-api/reference/endpoints/security/update-firewall-configuration
  - /kb/guide/deny-traffic-from-a-set-of-ip-addresses
  - /docs/limits
  - /docs/projects/environment-variables
  - /kb/guide/how-do-i-use-a-vercel-api-access-token
  - /docs/rest-api/authentication/create-an-auth-token
  - /docs/vercel-firewall/vercel-waf
  - /changelog/clients-blocked-by-persistent-actions-now-receive-a-403-forbidden-response
  - /docs/headers/request-headers
  - /kb/guide/cloudflare-with-vercel
  - /docs/projects/environment-variables/system-environment-variables
  - /changelog/web-application-firewall-mitigated-traffic-is-free-on-vercel
  - /docs/vercel-firewall/vercel-waf/usage-and-pricing
  - /docs/vercel-firewall/vercel-waf/rule-configuration
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Bot traffic, including from crawlers that label themselves as “good bots,” is increasingly aggressive. It can overwhelm an application's resources, drive up usage costs, and degrade user experience. Vercel's [Bot Protection Managed Ruleset](https://vercel.com/docs/vercel-firewall/vercel-waf/managed-rulesets#configure-bot-protection-managed-ruleset) and [BotID](https://vercel.com/docs/botid/get-started) use signature and ML-based detection to catch known bad actors and distinguish legitimate automated traffic from malicious activity in real time.

A honeypot adds a different but complementary signal: it catches any client, sophisticated or not, that ignores your `robots.txt` directives, giving you a deterministic way to identify and act on that specific behavior alongside your existing bot defenses.

This guide shows you how to build a basic honeypot on Vercel that:

- Lures the bots that ignore your `robots.txt`
  
- Identifies their source IP address
  
- Triggers a Vercel WAF response (e.g., block, time-based block, or rate limit)
  

## Prerequisites

- A Vercel account with [project administrator or team member](https://vercel.com/docs/rbac/access-roles) access to manage Firewall rules
  
- A [full-stack framework](https://vercel.com/docs/frameworks/full-stack) project to implement a route handler
  
- Optionally, a Vercel REST API [access token](https://vercel.com/docs/rest-api#creating-an-access-token) to programmatically add IPs to the deny list instead of the dashboard
  

## How it works

A honeypot works by exposing a tempting-looking, intentionally disallowed URL on your site. Compliant bots (Googlebot, Bingbot, etc.) will skip it. Bots that ignore `robots.txt` will hit it. You can use that signal to act without affecting any of the legitimate traffic you want to keep.

Technically, it’s just a specific URL on your site that's designed to attract bots that aren't playing by the rules. The mechanism relies on two observations about the `robots.txt` file:

1. It relies on the visiting bot's voluntary compliance, which works for reputable crawlers (Googlebot, Bingbot, etc.) that treat it as a list of paths to avoid.
   
2. Malicious bots, on the other hand, treat it as a list of target paths to exploit.
   

By exposing an attractive but disallowed path, you can identify clients that ignore the rules and act on them without affecting the legitimate crawlers you want indexing your site.

This approach is most effective against scrapers and unsophisticated automated traffic. It will not catch every bot, as a more sophisticated crawler may, in principle, avoid globally disallowed URLs, but in practice, it covers a meaningful percentage of the bot traffic that drives up bills and hurts performance.

## Steps

### Step 1: Choose the trap path

To get started, pick an endpoint that:

- Looks enticing to an attacker, such as admin, internal config, exports, or auth-related endpoints
  
- Doesn't already exist in your application
  
- Is _not_ a path that real users or legitimate scanners might guess on their own. For example, avoid `/wp-admin` If you're not running WordPress, since real users and harmless directory probers often land on those URLs by reflex
  

A few examples:

- `/api/internal/config-export`
  
- `/admin/users/export`
  
- `/dashboard/analytics/raw`
  

This guide uses `/api/internal/config-export`. Substitute it with your own pick, ideally something more product-specific, like `/api/<your-product-name>/admin/export`. Generic admin/export-style paths are routinely probed by offensive security scanners (Nuclei, Burp Active Scan) and bug-bounty hunters operating under program rules, which can produce a meaningful baseline of benign hits.

> The path you choose has implications for false positives. Generic admin paths that exist in popular CMSes (WordPress, Drupal, etc.) attract a lot of curious-but-harmless requests. Project-specific paths attract only people who are crawling _your_ `robots.txt` and ignoring it. Choose accordingly.

### Step 2: Place the bait in `robots.txt`

Add a global `Disallow` for your trap path. If you're using [Next.js](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots), add this to the `app/robots.txt` file:

```plaintext
User-Agent: *
Allow: /
Disallow: /api/internal/config-export

Sitemap: <https://yourdomain.com/sitemap.xml>
```

As per Google's [robots.txt specification](https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt), Googlebot and other compliant crawlers will respect the `Disallow`. Bots that don't will hit the trap.

### Step 3: Build the bait endpoint (optional)

> Skip this step if you plan to use the no-code WAF Custom Rule because it acts on the path directly and doesn't need a real handler. You'd only need an endpoint if you want to enrich the signal (e.g., layer in JA4 or geolocation checks) or drive blocks programmatically via the REST API (Path 2).

The bait endpoint has one job: record the source IP of every request that hits it, and respond in a way that doesn't expose anything useful. The implementation is framework-agnostic: any server-side handler will do.

A few important behaviors, regardless of framework:

- **Respond with a 404 (or a redirect to your homepage).** Don't expose anything that looks like real data; the goal is identification, not interaction.
  
- **Set the** `**X-Robots-Tag: noindex**` **header on the response.** This [tells Google not to index](https://developers.google.com/search/docs/crawling-indexing/block-indexing) the URL, so even if it's accidentally crawled, it won't show up in search results.
  
- **Don't link to this path from anywhere.** The only way it should be discoverable is via your `robots.txt`.
  

Here’s an example implementation in a Next.js App Router project:

```typescript
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';

export async function GET() {
  const headersList = await headers();

  // Read x-forwarded-for first. Vercel populates this header from the
  // TCP connection and overwrites any incoming value, so the leftmost
  // entry is the real client when Vercel sits directly in front of the user.
  // Behind another CDN? See "Reverse proxies and shared egress" below.
  const ip =
    headersList.get('x-forwarded-for')?.split(',')[0]?.trim() ??
    headersList.get('x-real-ip') ??
    'unknown';

  // Implement this with your storage provider. See Step 4 for what to do with the stored IPs
  await recordIPAddress(ip);

  return new NextResponse(null, {
    status: 404,
    headers: { 'X-Robots-Tag': 'noindex' },
  });
}
```

For low-traffic sites, calling the Firewall API inline from `recordIPAddress` is fine. For higher-volume sites, write the IP to a store instead ([Redis](https://vercel.com/marketplace/category/storage?category=storage&search=redis) or similar), and drain it on a schedule with a [cron job](https://vercel.com/docs/cron-jobs) that calls `ip.insert`. This decouples bait latency from API calls, lets you deduplicate IPs, and supports the “block only after N hits” threshold described in Step 5.

### Step 4: Choose your response strategy

This is the most context-dependent decision in the guide. There's no single “right” action; it depends on your site, your audience, and your appetite for false positives. You have three reasonable options:

#### Option A: Outright IP block (most aggressive)

Add the IP to the Firewall's [IP Blocking](https://vercel.com/docs/vercel-firewall/vercel-waf/ip-blocking) list, which will block them until you remove it. This blocks all future requests from that source before they reach your application.

- **When to use it**: You're confident the bot is acting in bad faith, and the cost of a false positive is low, e.g., a personal site, a content site, or one whose audience doesn't sit behind shared corporate egress IPs.
  
- **Trade-off**: A single blocked IP can represent many real users behind a corporate NAT, VPN, or upstream reverse proxy. If your audience includes B2B or enterprise users, this is risky.
  

#### Option B: Time-based IP block via Persistent Actions

Use a [WAF Custom Rule with Persistent Actions](https://vercel.com/docs/vercel-firewall/vercel-waf/custom-rules#persistent-actions) (Pro and Enterprise). When the rule’s deny or challenge action runs, Vercel automatically blocks that IP for a window you configure (1 to 60 minutes).

- **When to use it**: You want a forgiving default that punishes bots without permanently locking out legitimate traffic if there's collateral damage from a shared IP.
  
- **Trade-off**: A determined bot will retry after the window expires. You can compensate by escalating to a longer window or a permanent block after repeated triggers.
  
- **Bonus**: Persistent Action blocks happen [before the firewall processes the request](https://vercel.com/docs/vercel-firewall/firewall-concepts#how-vercel-secures-requests), so blocked requests don't even reach your application, resulting in a small latency and resource win.
  

#### Option C: Rate limit (least aggressive)

Use a [WAF Rate Limit rule](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting) to allow N requests to the bait endpoint before taking any action.

- **When to use it**: You're worried about false positives, for example, a security scanner running internally, or an SEO tool a customer has authorized to inspect their own configuration.
  
- **Trade-off**: A single hit on the trap is already a strong signal of bad-faith behavior, so rate-limiting it gives the bot more rope than it strictly needs.
  

Log mode is still the recommended starting point. Once the log data shows only bad actors hitting the trap, move to Option B: it maps directly to the no-code path explained in Step 5 and has the lowest false-positive blast radius of the three. Promote to option A or relax to option C only when the logs justify it.

Every option above fires on the source IP of any client that requests the bait path. That can include a real user who follows a stray link, a wanted bot you have not allowed, or a shared egress IP. A single corporate VPN or NAT address can represent an entire company, so one curious employee can get every colleague blocked for the duration of the window.

If your audience includes B2B or enterprise traffic, review hits in the Firewall traffic in your project dashboard and block confirmed offenders in a separate IP blocking rule. To automate that review, forward firewall events to your SIEM or logging provider with Drains (Pro and Enterprise): matched requests arrive with `proxy.wafAction`, `proxy.wafRuleId`, and `proxy.clientIp` fields ([Log Drains reference](https://vercel.com/docs/drains/reference/logs)), so you can apply enrichment or scoring externally before deciding to block.

You can match multiple IPs in a single WAF rule condition, but each condition supports fewer than 75 IPs. If your confirmed-offender list grows beyond that, split it across multiple conditions or rules rather than appending to a single one indefinitely.

### Step 5: Implement the response

Both paths below take effect instantly: Vercel propagates Firewall changes globally [in under 300ms](https://vercel.com/changelog/block-rate-limit-and-challenge-traffic-with-the-vercel-firewall) after you publish, whether the change comes from the dashboard or the API.

There are two ways to wire the response into Vercel Firewall, depending on whether you want a configuration-only setup or a programmatic one.

#### Path 1: WAF Custom Rule with Persistent Actions (no code)

This is the simplest implementation and a good starting point for most teams.

1. On the Vercel dashboard, in your project's [Firewall](https://vercel.com/docs/vercel-firewall) tab, select **Configure** in the top-right.
   
2. Select **Add New… › Rule**.
   
3. Add a single condition: **Request Path** equals `/api/internal/config-export`.
   
4. Set the **Then** action to **Log** and set the timeframe to your chosen window (e.g., 60 minutes).
   
5. Save the rule and **Publish** the changes.
   

Then test it per Step 6 before promoting to **Deny**.

#### Path 2: Automation with REST API (code)

If you'd rather drive the deny list updates programmatically, for example, only inserting an IP after you've seen it hit the trap N times or after additional behavioral checks, call the [Update Firewall Configuration](https://vercel.com/docs/rest-api/reference/endpoints/security/update-firewall-configuration) endpoint with the `ip.insert` action.

This pattern is documented in the [Deny traffic from a set of IP addresses](https://vercel.com/kb/guide/deny-traffic-from-a-set-of-ip-addresses) guide. Here’s the adapted version for the honeypot:

```typescript
async function blockIp(ip: string) {
  const url =
    `https://api.vercel.com/v1/security/firewall/config` +
    `?projectId=${process.env.VERCEL_PROJECT_ID}` +
    `&teamId=${process.env.VERCEL_TEAM_ID}`;

  const res = await fetch(url, {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${process.env.VERCEL_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      action: 'ip.insert',
      id: null,
      value: {
        action: 'deny',
        hostname: '*',
        ip,
        notes: `Honeypot trigger from ${ip} at ${new Date().toISOString()}`,
      },
    }),
  });

  if (!res.ok) throw new Error(`Firewall API error: ${res.status}`);
}
```

A few notes on this pattern:

- **Select hostnames**: Use `hostname: '*'` to apply the block across all of your project's domains. If you only want to block on certain hosts, list them explicitly.
  
- **Rate-limit the bait endpoint itself before calling the Firewall API:** A motivated attacker who realizes the endpoint is a trap can hit it from many distinct IPs to flood your deny list with entries, leaving you with an unreviewable rule list of attacker-controlled writes. The REST API also enforces [per-minute rate limits](https://vercel.com/docs/limits#rate-limits) on IP blocking operations (60 rule creations per minute), so per-hit inline inserts can start failing under load. Pair the rate limit with a queue or KV store that deduplicates IPs and inserts each one at most once per cooldown window, and periodically prunes stale entries.
  
- **Token scoping:** Store your `VERCEL_TOKEN` in the [Vercel environment variables](https://vercel.com/docs/projects/environment-variables). When creating the token, [scope it to the specific team that owns this project](https://vercel.com/kb/guide/how-do-i-use-a-vercel-api-access-token) (not full-account scope), and set an [expiration](https://vercel.com/docs/rest-api/authentication/create-an-auth-token) so it rotates on a known cadence. Be aware that Vercel access tokens are tied to the personal account that created them. If that user leaves the team or rotates their account, the token will stop working. For production automation, many teams dedicate a shared bot account for token ownership to avoid this dependency.
  

### Step 6: Test before you publish

Following Vercel's [WAF best practices](https://vercel.com/docs/vercel-firewall/vercel-waf/custom-rules#best-practices-for-applying-rules):

1. Set your rule's action to **Log** initially.
   
2. Watch the [Firewall traffic dashboard](https://vercel.com/docs/vercel-firewall/vercel-waf) for ten minutes.
   
3. Confirm the only IPs hitting the trap are ones you expect to act on, i.e., not your own IPs, not corporate VPNs you care about, and not internal scanners.
   
4. Promote to **Deny** (or your chosen action) once you're confident. If your audience includes enterprise networks or shared egress IPs, you can keep the rule in log-only mode indefinitely and manually or via drained logs act on confirmed offenders, as described in Step 4.
   

Once a Persistent Action blocks a client, they receive a [403 Forbidden response](https://vercel.com/changelog/clients-blocked-by-persistent-actions-now-receive-a-403-forbidden-response), a handy signal to confirm the rule is working when you promote to deny.

When blocking IPs from the dashboard during testing, [reserved documentation IPs](https://datatracker.ietf.org/doc/html/rfc5737) (`192.0.2.0/24`) are useful as placeholders since they will not affect any real users.

### Best practices

#### Reverse proxies and shared egress

Vercel populates the `X-Forwarded-For` header itself, based on the TCP connection of the request it receives, and [overwrites any incoming value](https://vercel.com/docs/headers/request-headers#x-forwarded-for) to prevent IP spoofing. When Vercel is the outermost edge (the default setup), this means the leftmost `X-Forwarded-For` entry is reliably the real client IP, and you can safely use it to drive blocks.

If your project uses another CDN or reverse proxy (e.g., Cloudflare in front of Vercel), this changes significantly. Vercel will populate `X-Forwarded-For` with the IP of the upstream proxy, not the original client, and the real client IP is _not available_ on most plans. A honeypot deployed in this configuration will block the fronting proxy itself, taking down all traffic that comes through it.

Enterprise customers can purchase [Trusted Proxy](https://vercel.com/docs/headers/request-headers#custom-x-forwarded-for-ip) to opt into reading an upstream-supplied `X-Forwarded-For`; on that configuration, the proxy-supplied client IP arrives in `X-Forwarded-For` as usual, so the code in Step 3 works unchanged. Note that [`x-vercel-forwarded-for`](https://vercel.com/docs/headers/request-headers#x-vercel-forwarded-for) always carries the IP of the client that connected directly to Vercel, which, behind a proxy, is the proxy itself. Vercel [does not recommend running another reverse proxy in front of Vercel,](https://vercel.com/kb/guide/cloudflare-with-vercel) as it degrades Bot Protection and Firewall accuracy. Do not deploy this honeypot behind another CDN unless you're on Enterprise with Trusted Proxy enabled.

Even without an upstream proxy, IPs from corporate NATs, mobile carriers, and VPN providers can serve thousands of legitimate users from a single address. This is the strongest argument for time-based blocks (option B) over permanent blocks (option A).

Most IPv6 clients have a /64 allocation, so blocking a single /128 is largely cosmetic against an attacker who can rotate within their own /64. For IPv6 sources, consider expanding the block to /64. [Vercel WAF IP-blocking](https://vercel.com/docs/vercel-firewall/vercel-waf/ip-blocking) supports CIDR ranges for both IPv4 and IPv6.

#### Don't let Google index the trap

Even though `robots.txt` tells Google not to crawl the path, [Google can still surface URLs it finds via other links](https://developers.google.com/search/docs/crawling-indexing/block-indexing). Always set `X-Robots-Tag: noindex` on the bait response. Also, don't link to the trap from anywhere on your site or in any external content.

#### Proxy.ts (`middleware.ts` before Next.js 16)

If you're tempted to implement the bait endpoint inside [Next.js Proxy](https://nextjs.org/docs/app/getting-started/proxy#proxy) (formerly middleware) instead of a Route Handler, be aware that it runs on _every_ matching request. You should use one of these two approaches:

- Use a Route Handler instead (recommended; lower overhead)
  
- Configure your Proxy `matcher` to only run on the trap path.
  

#### Preview deployments

The bait endpoint and the `Disallow` will also exist on every preview deployment, where they'll be hit by Vercel's own probes, link checkers, AI assistants reviewing PRs, and your team's own browsing. With Path 2 (programmatic `ip.insert`), preview hits can enqueue legitimate insider IPs into the deny list. Gate the handler so it only acts on `process.env.VERCEL_ENV === 'production'`, scope your WAF rule's condition to the production hostname only, or both. See [Vercel system environment variables](https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL_ENV) for the full list of values.

#### False positives from internal tools

Security scanners, SEO crawlers, uptime monitors, and AI assistants running on your behalf may hit the trap if they ignore `robots.txt`. Allowlist their IPs in the Firewall before publishing.

#### Pricing

There is no per-rule charge for the honeypot: WAF Custom Rules and IP blocking are free features on all plans. Since May 2026, Vercel has also [waived CDN Requests and Fast Data Transfer for any traffic that WAF rules deny, challenge, or rate limit](https://vercel.com/changelog/web-application-firewall-mitigated-traffic-is-free-on-vercel), so the requests your honeypot mitigates incur no CDN charges.

With Persistent Actions (Option B), blocked requests are dropped before the firewall processes them and do not count toward usage. The one-metered option is option C: [WAF Rate Limiting, which](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting) bills the allowed requests that pass through the rule. For the latest pricing information, see [Usage & Pricing for Vercel WAF](https://vercel.com/docs/vercel-firewall/vercel-waf/usage-and-pricing).

### Verifying that legitimate bots aren't caught

If you're worried about Googlebot or another legitimate crawler being misidentified, for example, because a request claiming to be Googlebot hit the bait, verify it before acting. Per Google's [Verifying Googlebot guide](https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot):

1. Run a reverse DNS lookup on the IP.
   
2. Confirm the hostname ends in `googlebot.com`, `google.com`, or `googleusercontent.com`.
   
3. Run a forward DNS lookup on that hostname and confirm it resolves back to the original IP.
   

Google [does not maintain a static allowlist of individual Googlebot IPs](https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot) because the ranges change over time. However, for high-volume verification, Google does publish [JSON files of crawler IP ranges](https://developers.google.com/static/search/apis/ipranges/googlebot.json) (one each for common crawlers, special-case crawlers, and user-triggered fetchers) that you can match against directly.

For non-Google crawlers, Vercel also operates [bots.fyi](http://bots.fyi), a public directory of verified web crawlers and bots that powers Vercel Bot Protection. It's a useful reference when triaging hits from less-common crawlers (Bingbot, Applebot, ClaudeBot, etc.), where you need to confirm whether a user-agent string corresponds to a known good actor.

### Next steps

- **Multiple traps**: define several disallowed paths to increase your odds of catching different scraping patterns.
  
- **Path patterns in rules**: WAF [rule configuration](https://vercel.com/docs/vercel-firewall/vercel-waf/rule-configuration) supports prefix and regex matching, so you can trap an entire prefix (e.g., `/api/internal/*`). Just be sure that no legitimate user traffic will be impacted by these rules before publishing.
  
- **Behavioral signals**: combine the bait hit with other request features Vercel surfaces, such as [JA4 fingerprint](https://vercel.com/docs/vercel-firewall/firewall-concepts#ja4), geolocation, and user-agent, to reduce false positives further.
  
- **Layered defense**: pair the honeypot with Vercel's [Bot Management](https://vercel.com/security/bot-management) and [BotID](https://vercel.com/botid) for invisible CAPTCHA challenges on critical paths.
  

Within a few hours of publishing, you should see attempted hits on the trap path show up in the Firewall traffic graph, and any IPs that triggered your rule will appear under blocked traffic. From there, you can iterate on the response window, escalate repeat offenders, or layer in additional behavioral checks.