See which AI agents are really hitting your site.
Posterna checks each AI agent and crawler against what its operator publishes, labels the request, and applies your rules for that agent. It runs inside your app, with no proxy, CDN or DNS change.
npx posterna init- GET/docs/getting-startedGooglebotverified (IP)allow
- GET/pricingDuckAssistBotverified (signature)allow
- GET/blog/changelogGrokBotunverifiableallow
- GET/api/v1/itemsGPTBotclaimed, failedblock (403)
- POST/signupno agent namedunknown automationchallenge (recommended)
- GET/a browserhumanallow
What it does
Verify
Checks Web Bot Auth signatures, published IP ranges and reverse DNS in your own process. Requests never leave your server.
Decide
One agents.yaml sets what each operator gets, by purpose and path: allow, throttle, block, or charge (coming soon). On sign-up routes your app gets a recommendation and acts on it with the friction it already has.
See
Optional counters show which operators visit, what they read, how much of it is verified, and whether a good agent was ever refused. Never IP addresses, URLs or page contents.
Safe by default
It starts in monitor mode: it labels and counts, and blocks nothing until you switch to enforce. Verified search engines stay reachable whatever your rules say. Enforcement switches itself off when your proxy settings look wrong. If anything inside Posterna fails, the request goes through.
Who it's for
- Docs, content and API sites under heavy crawler load. See each operator's share and what it reads, then throttle, block, or charge (coming soon) by purpose.
- Apps where agents sign up or call your API. See which agents arrive and which can prove who they are.
- Teams that want the decision in their own code, on the host they already use.
What it is not
- Not an agent login. It checks requests, not accounts, and works alongside agent sign-in tools.
- Not fraud or bot detection. Traffic that does not say who it is gets labeled unknown automation. It is never scored or fingerprinted.
- Not a proxy or CDN. Nothing sits between your visitors and your app.
What can be verified today
Verification depends on what each operator publishes. Today 5 operators sign their requests and 8 more publish IP ranges or reverse DNS rules. Everything else is labeled declared but unverifiable, so you still see it. The signer board shows who signs; its keys are refreshed every hour and the directory every day.
Install
Create your policy
It writes agents.yaml in monitor mode and prints the code for step 2.
npx posterna initAdd Posterna to your app
npm install @posterna/expressimport express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ clientIpHeader: "x-real-ip", publicHost: "www.example.com", // your site's hostname }), );import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ clientIpHeader: "x-vercel-forwarded-for", publicHost: "www.example.com", // your site's hostname }), );import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ trustedProxies: 2, // 3 with Cloudflare in front publicHost: "www.example.com", // your site's hostname }), );import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ trustedProxies: 3, // check with posterna doctor; use 2 if it reports a forged address publicHost: "www.example.com", // your site's hostname }), );3comes from a Render employee's demonstration, not Render's documentation (Render).import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ trustedProxies: ["10.0.0.0/16"], // replace with your load balancer subnets' CIDRs publicHost: "www.example.com", // your site's hostname }), );The load balancer checks each target every 30 seconds by default, on
/: give health checks their own path.import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ clientIpHeader: "x-real-ip", trustedProxies: ["127.0.0.1/32", "::1/128"], publicHost: "www.example.com", // your site's hostname }), );Bind Node to
127.0.0.1and add theproxy_set_headerlines to nginx.import express from "express"; import { posterna } from "@posterna/express"; const app = express(); app.use( posterna({ // Set clientIpHeader or trustedProxies for your host: // https://posterna.com/docs/hosts#settings-by-host publicHost: "www.example.com", // your site's hostname }), );Find your host's setting in Hosts and proxies.
Add it before your routes.
npm install @posterna/nextimport { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ clientIpHeader: "x-real-ip", publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ clientIpHeader: "x-vercel-forwarded-for", publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ trustedProxies: 2, // 3 with Cloudflare in front publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ trustedProxies: 3, // check with posterna doctor; use 2 if it reports a forged address publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };3comes from a Render employee's demonstration, not Render's documentation (Render).import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ trustedProxies: ["10.0.0.0/16"], // replace with your load balancer subnets' CIDRs publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };The load balancer checks each target every 30 seconds by default, on
/: give health checks their own path.import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ clientIpHeader: "x-real-ip", trustedProxies: ["127.0.0.1/32", "::1/128"], publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };Bind Node to
127.0.0.1and add theproxy_set_headerlines to nginx.import { withPosterna } from "@posterna/next"; export const proxy = withPosterna({ // Set clientIpHeader or trustedProxies for your host: // https://posterna.com/docs/hosts#settings-by-host publicHost: "www.example.com", // your site's hostname }); export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], };Find your host's setting in Hosts and proxies.
Then wrap your Next.js config in
withPosternaConfig, as the quickstart shows.Deploy and check
Set a temporary
POSTERNA_DEBUG_TOKENon the server, deploy, and run doctor from your machine. Fix any FAIL; each one names its fix. Then remove the token.POSTERNA_DEBUG_TOKEN=<token> npx posterna doctor --url https://www.example.com
On WordPress? The WordPress plugin labels and counts the same way, in monitor mode only, and shows the results in wp-admin.
What do AI agents get from your site today?
The probe fetches your site as a browser and as the AI and search agents below, and shows what each one got.
Googlebot, Bingbot, GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Perplexity-User
Built on open standards
RFC 9421 HTTP Message Signatures and the IETF Web Bot Auth drafts, verified with Cloudflare's open-source web-bot-auth libraries. Agent coverage builds on the community ai.robots.txt list. The operator directory is open data (CC BY 4.0).
Run an agent or crawler? Get it verified.