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.

Open source. Express and Next.js (MIT, version 0.1.3, an early release), and a WordPress plugin (GPL, monitor only).

Example request logAn example, not live traffic
  1. GET/docs/getting-startedGooglebotverified (IP)allow
  2. GET/pricingDuckAssistBotverified (signature)allow
  3. GET/blog/changelogGrokBotunverifiableallow
  4. GET/api/v1/itemsGPTBotclaimed, failedblock (403)
  5. POST/signupno agent namedunknown automationchallenge (recommended)
  6. 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

  1. Create your policy

    It writes agents.yaml in monitor mode and prints the code for step 2.

    npx posterna init
  2. Add Posterna to your app

    Framework
    Host

    Settings for every host, with sources and dates.

    npm install @posterna/express
    import 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
      }),
    );

    3 comes 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.1 and add the proxy_set_header lines 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/next
    import { 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).*)"],
    };

    3 comes 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.1 and add the proxy_set_header lines 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.

  3. Deploy and check

    Set a temporary POSTERNA_DEBUG_TOKEN on 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.