A few months back I shipped a sign-up flow with what I thought was a solid defense in depth. Cloudflare in front. A per-IP rate limit at the edge. A per-email throttle at the route handler. A captcha on the form for anyone who tripped a velocity threshold. The honeypot field that nineties spammers cannot resist. I felt clever. I went to bed.
I woke up to four hundred and twelve fake accounts created by a single Playwright session running on a residential proxy network. None of my defenses had fired. The IP changed on every request. The email looked plausible. The captcha was solved by a third-party solver in under two seconds per challenge. The honeypot was left blank, because the bot was reading the field labels and behaving like a polite human. The accounts were doing what accounts do (claiming a free trial of an AI feature, burning credits, never coming back) and my OpenAI bill had jumped enough that I started reading the changelog at 6am.
That was the morning I stopped pretending that traditional rate limiting was enough on its own. I had written about the layered defense pattern for SaaS APIs a few weeks earlier. I had not actually used all the layers. The layer I was missing was a bot detector that ran in the browser before the request hit my edge. The layer I added that afternoon was Vercel BotID.
This is the post I wish I had read the night before that wakeup. What BotID actually is, how the invisible challenge works under the hood, the Basic versus Deep Analysis tradeoff, the routes I actually wire it into, and where it fits in the stack alongside rate limiting, WAFs, and old-fashioned authentication.
What BotID Actually Is
BotID is a client-side challenge that runs in the requester's browser, returns a result to a Vercel-validated endpoint, and lets your server code decide what to do based on whether the requester looks like a human or a bot. It is invisible. There is no captcha, no checkbox, no "I am not a robot" widget. Real users see nothing. Bots either fail the challenge or pass it slowly enough that the deep analysis flags them.
The product launched as a Vercel-native feature in mid-2025 and went general availability in June 2025. The underlying machine-learning layer (the part that does the heavy lifting on suspected traffic) is powered by Kasada, the same provider that sits behind a lot of Fortune 500 anti-fraud setups. Vercel wraps that engine in an integration that you can wire up with a few lines of code and a route config.
The thing to internalise is that BotID is not a CAPTCHA replacement in the sense of "another widget you put on the page." It is closer to a runtime check that you run before doing anything expensive. It is a server-side decision based on a client-side proof. The mental model is "I will hold the work until I have decided this caller is a human."
For indie SaaS in 2026 that mental model maps cleanly onto the three places where a bot can ruin your week: account creation, AI inference endpoints, and anything that touches a credit card.
How The Invisible Challenge Actually Works
The mechanics are interesting because the design has to survive an attacker who can read all of the client code. There is no security through obscurity here. Everything that runs in the browser is, by definition, visible to anyone who wants to look.
The flow goes like this. When the browser loads a page that has the BotID client script, the script downloads a challenge payload from Vercel. That payload contains a small piece of obfuscated JavaScript that the browser has to run. The script computes a token based on a mix of inputs (some entropy from the page, some signals from the browser environment, some interaction patterns) and attaches that token as a header on subsequent requests to your protected routes.
On the server side, your route handler calls checkBotId() from the SDK. That function ships the token off to Vercel's edge, which validates that the challenge was actually solved (not replayed from a previous session, not generated by a tampered version of the script). The function returns a verdict, which your handler reads and acts on.
What makes this resilient is that the challenge code is regenerated every time. Vercel does not ship one static piece of JavaScript that an attacker can analyse once and replay forever. The obfuscation is rotated. The exact signals being collected change. The expected output format changes. By the time an attacker has reverse-engineered the current challenge format and written a bot that passes it, the next deploy has already changed the rules.
There is a terrific writeup by nullpt.rs on what reverse-engineering the BotID client actually looks like in practice. The short version is "you can do it, but you have to do it again every week, and Vercel has more engineers working on the next rotation than you have working on the next bypass." For attackers running spray-and-pray scrapers, the math stops working long before they have a viable client. For state-level actors with infinite time, it does not stop them. They are not your problem on a SaaS side project.
Basic Versus Deep Analysis
BotID ships in two modes and the choice between them is the first real decision you have to make.
Basic is free. It validates that the client-side challenge was solved correctly. It catches anything that did not run the JavaScript at all, anything that replayed a stale token, anything that submitted a token from a different origin, and a large fraction of less-sophisticated bots that try to fake the challenge response. Basic mode is enough to filter out the casual scrapers, the curl-from-a-script attackers, and the long tail of nineties-style bots that never bothered to run a JS engine.
Deep Analysis is the paid mode. It pipes the client-side signals into Kasada's ML model, which looks at thousands of behavioural fingerprints (timing patterns, mouse movement traces, browser quirks, network signals) and decides whether the requester is a real human or a sophisticated bot pretending to be one. This is the mode that catches Playwright running on residential proxies. It is also the mode that costs a dollar per thousand calls to checkBotId(), on top of the Pro plan, which is twenty dollars a month.
For an indie SaaS the math usually breaks down like this. On a public landing page where bots can crawl freely, Basic is fine and free. On a sign-up endpoint, an AI inference endpoint, or a checkout, Deep Analysis pays for itself the first time it stops a single scraper run. The dollar per thousand is a rounding error compared to one weekend of API abuse on an OpenAI-backed feature.
The pricing detail that matters: Vercel only bills for Deep Analysis when you actually call checkBotId(). Page views that load the script but never make a protected request are free. That means you can install the script everywhere and only pay for the routes you explicitly protect. If you forget to wire checkBotId() into a handler, the bill does not surprise you, but neither does the bot.
Wiring It Into A Next.js Route
The integration pattern is short enough to fit in one screen. Here is what a protected sign-up handler looks like in 2026.
export async function POST(request: Request) {
const verdict = await checkBotId();
if (verdict.isBot) {
return new Response(JSON.stringify({ error: 'forbidden' }), {
status: 403,
});
}
if (verdict.isVerifiedBot) {
if (verdict.verifiedBotCategory === 'search-engine') {
return new Response(null, { status: 200 });
}
return new Response(JSON.stringify({ error: 'forbidden' }), {
status: 403,
});
}
const body = await request.json();
return createAccount(body);
}
Three things are worth flagging about that snippet.
The first is that checkBotId() takes no arguments. It reads the request context implicitly. That keeps the call site clean, but it does mean you have to be on a Vercel-deployed handler for it to work, because the function depends on Vercel headers being present. There is a documented local development mode that lets you test on a laptop without paying for Deep Analysis. The mode is opt-in and the verdict is a fake "always human" so you do not lock yourself out of your own dev environment.
The second is the isVerifiedBot field. As of BotID v1.5.0+, Vercel ships a list of verified, legitimate bots (search engine crawlers, monitoring tools, social media preview bots) and exposes their category. A naive "block all bots" rule would also block Googlebot, which is rarely what you want. The verified-bot data lets you allow the polite bots through while still rejecting everything else. The pattern in the snippet above is the most common one I see: humans pass, search engines pass, everything else gets a 403.
The third is that the function is async and it does a network call to Vercel's edge to validate the token. That call is fast (single-digit milliseconds in my measurements) but it is not free, and on routes where you call checkBotId() you are adding a tiny latency budget. For a sign-up endpoint that is fine. For a hot API path that gets ten thousand requests per second, you probably want to be more selective.
The protected paths are configured in either vercel.json or the new vercel.ts config. You list the routes that should run the client-side script, and BotID injects it on those pages automatically. Routes you do not list do not get the script, which keeps the JavaScript footprint small on public pages where you do not need protection.
What BotID Is Not
BotID is good at one thing. It is not good at everything. Mistaking it for a complete security layer is the most common failure mode I see on indie projects.
BotID is not a rate limiter. It tells you whether the caller is a bot or a human. It does not tell you whether the caller is calling too often. A human user spamming an endpoint will pass every BotID check and still burn your budget. You still need a rate limiter on the same routes you protect with BotID. The two are stacked: BotID throws out the obvious bots, the rate limiter throws out the legitimate-looking abusers. I went into the rate limiter layer in more detail in the rate limiting your SaaS API piece, and the punch line there applies here: layer your defenses, do not pick one.
BotID is not authentication. It does not tell you who the caller is. A bot can have a valid API key. A human can have a stolen API key. BotID answers a different question. Run it alongside proper auth with passkeys or a session-based scheme, not instead of it.
BotID is not a WAF. Vercel's Web Application Firewall does pattern matching on known-bad request shapes and is a separate product. BotID slots in as one of several rules the firewall can apply. If your application is being attacked at a different layer (SQL injection attempts, path traversal, header smuggling), the WAF is the right tool and BotID will not catch any of it.
BotID does not catch every bot. A sufficiently determined attacker can still pass Deep Analysis with a real headed Chrome session running on a human-like input pattern. The cost of doing that is high, the throughput is low, and the economics of credential stuffing or large-scale scraping break long before you get there. But there is no "all bots are stopped" mode and pretending otherwise is how you ship a security theater feature.
BotID is not a guarantee for AI scrapers either. The isAiBot and verifiedBotName fields let you treat known model trainers and known AI agents as a separate category. If you want to allow them, you can. If you want to refuse them with a 402 and a "subscribe to access" message, you can do that too. But the population of AI scrapers that ignore robots.txt, do not identify themselves, and route through residential proxies is growing fast, and the verified list cannot keep up with all of them. Deep Analysis catches a lot of the unidentified ones. Some still slip through.
The Routes Where BotID Actually Earns Its Place
For an indie SaaS I run, three families of routes are protected by BotID. Everything else is left to rate limiting and auth.
Account creation routes. Sign up, sign in, password reset. These are the routes where a successful bot run creates persistent damage (fake accounts, account takeover, free-trial abuse). They are also the routes where a single human user only hits them a handful of times per week, so the latency cost of an extra checkBotId() call is irrelevant. Deep Analysis goes here.
AI inference routes. Anything where calling the endpoint costs me money downstream. The chat endpoint that hits an LLM, the image generation route that hits Replicate, the embedding endpoint that hits a vector database. These are the routes where bot abuse turns into a visible bill, fast. Deep Analysis goes here too, because the cost of one missed scraper run is much higher than the cost of a thousand checkBotId() calls.
Checkout and billing routes. Anything where a successful bot run causes either a fraudulent charge or an inventory hit. The webhook routes that complete a purchase, the route that applies a coupon code, the route that triggers a refund. Deep Analysis again, for the same reason. These routes also get the most engagement from sophisticated bots, because there is real money on the other side of a successful run.
Routes I do not protect with BotID include public landing pages (no high-value action, robots are mostly fine), public API documentation (let the AI crawlers index it), and anything where the request itself is cheap and the only reason to limit it is rate. Those routes get the rate limiter and nothing else.
The instinct that drives the choice is the same one I use for feature flags in solo developer setups: the protection has to match the cost of being wrong. If being wrong about a request costs me a tenth of a cent of compute, the cheapest filter is good enough. If being wrong costs me an OpenAI invoice, the expensive filter pays for itself.
Observability And The Dashboard You Will Actually Use
The thing nobody tells you about bot detection is that you will second-guess every block. You will see a 403 in your logs and wonder if it was a real user with a weird browser. You will see your active-user count dip after a deploy and wonder if BotID just turned on too aggressively. The only way to stay sane is to look at the data.
Vercel ships BotID metrics in two places. The Firewall tab in your project dashboard has a traffic breakdown that includes BotID verdicts as a filter. You can see, in real time, how many requests were blocked, how many were allowed, what categories the blocks fell into. The Observability Plus add-on extends this with longer retention and finer slicing.
The view I check most often is "verdicts over the last 24 hours, grouped by route." It shows me which protected routes are actually catching bots and which ones have been quiet. If a route I expected to catch bots is showing zero blocks for a week, either the protection is working perfectly (no bots are trying), or the protection is not wired in correctly (the checkBotId() call is silently failing somewhere). Both are worth knowing.
The metric I do not put much faith in is the raw block count without context. A spike in blocks is not automatically a sign of an attack. Some weeks Googlebot decides to crawl harder than usual and your verified-bot category lights up. Some weeks a popular sub-reddit links to your site and your real-user traffic looks like a bot wave. The interesting signal is "blocks that did not fall into the verified categories, on routes that handle paid actions, over time." Everything else is noise.
If you are going to wire BotID up at all, wire up a Slack alert on "blocks per hour on the AI inference route exceeded the rolling average by 3x." The same instinct that drives production observability for solo developers applies here. You do not need a full security operations center. You need a single ping that fires when something looks weird, so you can decide whether to look closer.
The Honest Tradeoffs
I want to be honest about the parts of BotID that are not perfect, because the marketing page is, predictably, the marketing page.
The first tradeoff is vendor lock-in. BotID is a Vercel-native product. If you ever move your hosting off Vercel, the integration goes with it. You can replace it with another bot detector (Kasada sells the same engine directly, hCaptcha Enterprise does something similar, Cloudflare Turnstile is a free-ish alternative for the public side of the challenge), but it is a rip-and-replace, not a portable abstraction. If portability matters to you, factor that in.
The second tradeoff is the cost ceiling on high-volume routes. A dollar per thousand calls is cheap on a sign-up endpoint that fires once per user per month. It is not cheap on an API route that fires three times per second per user. Before you slap checkBotId() on every authenticated route, run the math on monthly call volumes. Sometimes the right answer is "rate limit aggressively, only invoke BotID when the rate limit trips."
The third tradeoff is the small but non-zero false positive rate. Privacy-focused browsers, aggressive ad blockers, weird mobile environments, and edge cases like users running through a VPN can occasionally fail the challenge. The published rate is low (less than a tenth of a percent of real users in Vercel's own data), but you will see it, and you will need a way to handle it. The pattern I use is "BotID block on a sign-up route returns a 'try again' message with a link to a slower, captcha-protected fallback." Real users do the fallback, bots do not.
The fourth tradeoff is that Deep Analysis is asynchronous. The verdict you get back from checkBotId() is the result of the initial challenge plus whatever signals were collected up to that point. Some of the deeper analysis happens after the request has returned, with the verdict updating in Vercel's dashboard later. If you build a workflow that depends on "the bot was caught and immediately reversed," you will not get the precision you want. The right pattern is "use the verdict for immediate decisions, use the dashboard for retroactive investigation."
Should You Wire It Up Today
For most indie SaaS in 2026 the answer is yes, for at least the high-value routes, in Basic mode if cost is a concern and Deep Analysis if cost is not.
The install is short. You add the BotID client script to your app via the integration, you wire the route config in vercel.json or vercel.ts, you add a checkBotId() call to the half-dozen routes that matter, and you are done. The whole thing takes an afternoon. Most of the afternoon is testing in dev to make sure your local environment is not getting falsely flagged.
The audience that should not bother is anyone whose backend is not on Vercel. The product is Vercel-native. You can replicate the pattern with other providers (Kasada directly, Cloudflare Turnstile plus a custom verifier, hCaptcha Enterprise), and if you are on AWS or Cloudflare or your own infra those are the right options to look at. They are not drop-in replacements, but they cover the same ground.
The other audience that should pause is anyone whose threat model does not include bots. If you run a B2B SaaS where every user is on a signed contract and the only people calling your API are the people who paid for the seats, you have an authentication problem, not a bot problem. Wire up auth, log every request, and skip BotID. You will not need it.
For everyone in the middle, where bots are a real risk because your AI endpoint costs money or your sign-up flow gives away free trials or your checkout has anything resembling inventory, BotID is the single highest-leverage thing you can add in a day. It will not solve everything. It will not stop every bot. It will not replace your rate limiter, your auth, or your WAF. It will dramatically thin the population of attackers who can hit the routes you actually care about, and that is the part that earns its place.
What I'd Tell Past Me
If I could hand my past self one paragraph, it would be this. The traditional defenses (rate limits, captchas, honeypots, IP blocks) assume an attacker who is roughly human-shaped. The attackers in 2026 are not human-shaped. They are Playwright instances running on residential proxies, AI scrapers ignoring robots.txt, and credential stuffers cycling through hundreds of thousands of leaked passwords from last decade's breaches. The defenses you grew up with do not stop them, not because the defenses are bad, but because they were built for a different threat model.
BotID is not a silver bullet for that new threat model. There is no silver bullet. But it is the cheapest single addition to a SaaS stack that meaningfully shifts the economics of attacking you. The bots that pass it cost more to operate than the ones that do not. The attackers who can afford to pass it move on to softer targets. The math finally tips the other way.
Wire it into your sign-up, your AI inference routes, and your checkout. Leave the rest of your stack alone. Watch the dashboard for a week. Then go back to building your product, which is what you should have been doing in the first place, before the four hundred and twelve fake accounts taught you what you already suspected.
The defense was always going to have to evolve. The good news is that for once the tooling evolved with it.
Top comments (0)