Drop BotWatcher into your Next.js middleware — under 5 minutes.
Zero latency
Fire-and-forget fetch — never slows page loads
Server-side only
Detection logic never reaches the browser
Safe to merge
Works alongside your existing middleware
You don't have a middleware.ts yet. Create one from scratch.
You already have auth, i18n, or other logic in middleware.ts. Safe to combine.
Add to your .env.local (for local dev) and to Vercel → Project → Settings → Environment Variables (for production). Mark BOTWATCHER_API_KEY as a Secret.
BOTWATCHER_API_KEY=your-api-key-here
BOTWATCHER_DOMAIN=yourdomain.com
# Optional — defaults to https://api.botwatcher.pro/edge
# BOTWATCHER_WEBHOOK_URL=https://api.botwatcher.pro/edge.envNext.js 16+ — create src/proxy.ts and use export function proxy.
Next.js 13–15 — create src/middleware.ts and rename the export to middleware (comment in the snippet tells you where).
// Next.js 16+ → save as: src/proxy.ts (or proxy.ts at project root)
// Next.js 13–15 → save as: src/middleware.ts
import { NextResponse } from "next/server";
import type { NextFetchEvent, NextRequest } from "next/server";
export function proxy(req: NextRequest, event: NextFetchEvent) {
const apiKey = process.env.BOTWATCHER_API_KEY;
const domain = process.env.BOTWATCHER_DOMAIN;
if (apiKey && domain) {
// event.waitUntil keeps the function alive until the fetch completes
// without blocking the response to the visitor.
event.waitUntil(
fetch(process.env.BOTWATCHER_WEBHOOK_URL ?? "https://api.botwatcher.pro/edge", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({
site: domain,
path: req.nextUrl.pathname,
method: req.method,
ua: req.headers.get("user-agent") ?? "",
ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "",
country: req.headers.get("x-vercel-ip-country") ?? "",
city: req.headers.get("x-vercel-ip-city") ?? "",
region: req.headers.get("x-vercel-ip-country-region") ?? "",
referer: req.headers.get("referer") ?? "",
}),
}).catch(() => {})
);
}
return NextResponse.next();
}
// Next.js 13–15: rename the export above to `middleware` instead of `proxy`
export const config = {
matcher: [
/*
* Match all paths except:
* - _next/static (static files)
* - _next/image (image optimisation)
* - favicon.ico
*/
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};tsPush to your main branch. Vercel will pick up the new middleware automatically — no config changes needed.
Test the API endpoint directly — if this returns {"ok":true,"logged":true} your API key is valid and hits will appear in your dashboard.
curl -X POST https://api.botwatcher.pro/edge \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"site":"yourdomain.com","path":"/test","method":"GET","ua":"GPTBot/1.0","ip":"1.2.3.4","country":"US","city":"","region":"","referer":""}'
# Expected response: {"ok":true,"logged":true}bashThen visit any page on your site with a bot user-agent (or just wait — real bots will show up within hours). Check the Recent Hits tab on your dashboard.
This will not break your existing middleware.
BotWatcher calls fetch() in a fire-and-forget pattern — it doesn't await a response, never throws, and always calls NextResponse.next() unless your existing logic returns a redirect/rewrite first.
Same as the fresh install — add to .env.local and Vercel dashboard.
BOTWATCHER_API_KEY=your-api-key-here
BOTWATCHER_DOMAIN=yourdomain.com
# Optional — defaults to https://api.botwatcher.pro/edge
# BOTWATCHER_WEBHOOK_URL=https://api.botwatcher.pro/edge.envYour file is either proxy.ts (Next.js 16+) or middleware.ts (Next.js 13–15). You're going to add the BotWatcher helper to it — not replace your existing logic.
The template below shows the pattern. Replace the yourExistingMiddleware placeholder with your actual logic (or leave it as a separate function/import).
// Next.js 16+ → save as: src/proxy.ts (or proxy.ts at project root)
// Next.js 13–15 → save as: src/middleware.ts
import { NextResponse } from "next/server";
import type { NextFetchEvent, NextRequest } from "next/server";
// ─── BotWatcher reporting helper ─────────────────────────────────────────────
function reportToBotWatcher(req: NextRequest, event: NextFetchEvent) {
const apiKey = process.env.BOTWATCHER_API_KEY;
const domain = process.env.BOTWATCHER_DOMAIN;
if (!apiKey || !domain) return;
event.waitUntil(
fetch(process.env.BOTWATCHER_WEBHOOK_URL ?? "https://api.botwatcher.pro/edge", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({
site: domain,
path: req.nextUrl.pathname,
method: req.method,
ua: req.headers.get("user-agent") ?? "",
ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "",
country: req.headers.get("x-vercel-ip-country") ?? "",
city: req.headers.get("x-vercel-ip-city") ?? "",
region: req.headers.get("x-vercel-ip-country-region") ?? "",
referer: req.headers.get("referer") ?? "",
}),
}).catch(() => {})
);
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── Paste your existing middleware/proxy logic below ────────────────────────
function yourExistingLogic(req: NextRequest): NextResponse | null {
// e.g. auth checks, redirects, i18n, A/B flags…
// return NextResponse.redirect(…) or NextResponse.rewrite(…)
// return null to fall through to NextResponse.next()
return null;
}
// ─────────────────────────────────────────────────────────────────────────────
export function proxy(req: NextRequest, event: NextFetchEvent) {
// 1. Always report to BotWatcher — fire-and-forget, never blocks
reportToBotWatcher(req, event);
// 2. Run your existing logic — honour any redirect/rewrite it returns
const existing = yourExistingLogic(req);
if (existing) return existing;
// 3. Default — continue to the page
return NextResponse.next();
}
// Next.js 13–15: rename the export above to `middleware` instead of `proxy`
// Keep your existing matcher, or use this recommended one:
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
// Keep your existing matcher or use the BotWatcher recommended one below
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};tsKey rules when merging:
Push your changes. Then run the same curl test to confirm your API key works:
curl -X POST https://api.botwatcher.pro/edge \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"site":"yourdomain.com","path":"/test","method":"GET","ua":"GPTBot/1.0","ip":"1.2.3.4","country":"US","city":"","region":"","referer":""}'
# Expected response: {"ok":true,"logged":true}bashSet up? Head to your dashboard to watch bot traffic arrive.