What this lets you do
See which AI systems fetch your pages, which pages they read, and whether the crawler was really who it claimed to be. AI crawlers do not run JavaScript. They issue one HTTP GET for your HTML and leave, so the Traceten snippet never sees them. The only place a crawl is observable is your server. This guide installs a small middleware there. This is separate from, and additional to, the browser snippet. Keep both: the snippet measures people who arrive from an AI assistant, this measures the crawl that put you in that assistant’s answer.Before you start
- A site already added in Traceten
- Node 18 or later, or an edge runtime with
fetch - Admin access to your Traceten account, covering the whole account rather than a single site (only admins can create crawl tokens, see Team and roles)
1. Create a crawl token
In the dashboard, open Settings → API keys and select the AI crawler API key tab. Crawl tokens belong to a single site, so check that the site named under Crawler tokens is the one you are installing on. If your account has more than one site, switch with the site picker beside Create token. Then select Create token, give the token a name (your environment name is a good choice) and select Create. You also need your site key, shown under the new token and in the Install the middleware section. It is the same public value the snippet uses asdata-site.
Store both as environment variables:
Rotating a token
There is deliberately no single rotate button. Pressing it would break reporting the instant you did, because your deployed server still holds the old secret. Rotate in this order:- Create the replacement. Both tokens are live at once.
- Deploy it. Confirm crawls are still arriving.
- Revoke the old one, from the token list.
2. Install the package
3. Add the middleware
Pick the one that matches your stack. If your site is already behind Cloudflare, use one of the two Cloudflare options. The tracking runs in front of your origin rather than inside your app, and the crawler’s address is already correct with no proxy configuration to get right. With Cloudflare Workers your application itself never changes: you wrap the Worker that already fronts your origin and deploy that. Cloudflare Pages Functions does need a deploy, because the middleware ships as a file in your project.Request and Response objects can call trackAICrawlerFetch(request, response, config, waitUntil?) from @traceten/ai-crawl/hono. That function requires a config you have already passed through defineAiCrawlConfig; hand it a plain object and it returns without reporting anything.
Invalid config fails at startup, not during a request, with one exception. defineAiCrawlConfig throws on invalid input, and the Cloudflare Pages, Cloudflare Workers, Express and Hono factories all validate at construction whether or not you called it yourself, so a missing or malformed token throws while your server is starting. That is the failure you want: it fails your deploy rather than your visitors’ requests.
The exception is the Next.js trackAICrawlerRequest, which runs per request and therefore must never throw into one. It validates lazily instead, and invalid config logs [@traceten/ai-crawl] invalid config — crawler tracking is DISABLED once and then reports nothing at all. On Next.js, call defineAiCrawlConfig yourself at module scope, as the example does, so the failure is loud.
Getting the crawler’s IP right
Verification is only as good as the address your server reports. If it reports your own load balancer, nothing matches, every crawl comes back unverified, and no error appears anywhere. This is the single most common way this install goes quietly wrong.
Resolution order is
cf-connecting-ip (only when trusted), then x-forwarded-for (only when trustProxy is set, counting back proxyDepth entries from the right), then the socket address. If no trustworthy value can be derived, the package sends no address at all rather than guessing one.
4. Deploy and verify it worked
Deploy, then confirm the middleware is reporting:Troubleshooting
Nothing appears at all. Check the token is set in the deployed environment, not only locally. An absent token throws at startup on every adapter except Next.js, so check your boot logs first; on Next.js look for[@traceten/ai-crawl] invalid config. A token that is present but revoked, or issued for a different site, is rejected at our edge instead, and the report is dropped silently by design: the middleware never breaks a request to tell you. Set onError to see those rejections.
Seeing rejected reports
The middleware never throws and never retries, so a rejected report looks the same as a delivered one. A wrong token, or egress your platform blocks, both read as “no AI crawlers visited” for as long as they last.onError reports what happened without changing that behaviour:
e.kind is "http" when our edge answered, with the status in e.status. A 401 or 403 means the token is wrong, revoked, or issued for another site. e.kind is "network" when the request never completed, with the thrown value in e.cause; repeated network failures usually mean your platform blocks outbound requests.
Your callback runs inside a try/catch. Throwing from it cannot break the response, so the guarantee that this middleware never affects your site still holds. Wire it to your logger in staging at minimum.
“Site not found” or nothing lands despite a 200. Check siteId is the site key from the dashboard, not the site’s UUID from your browser’s address bar. They are different values and the wrong one is rejected at the edge.
Everything shows as Unverified or Spoofed. Your server is probably behind a proxy or CDN and reporting the proxy’s IP rather than the crawler’s. See Getting the crawler’s IP right. Note that a large Unverified share on its own is normal and not a fault: about half of tracked crawlers publish no way to prove their identity. See “Unverified” is normal.
URLs look wrong, or use an internal hostname. If the reported URL shows 0.0.0.0 or [::], that’s the server’s own bind address — the package already falls back to your Host header in that case, so check your reverse proxy is forwarding Host at all. If URLs instead carry a different internal hostname (containers, private networking, Host itself set to something internal), set publicOrigin to your real origin.
Crawls of /robots.txt or /llms.txt are missing. They should not be. Those paths are always reported even though .txt is otherwise filtered out. If they are absent, the middleware is likely mounted after a route that serves those files directly.
Too much traffic reported. Use disableTrainingCrawlers, disableSearchCrawlers, disableAnswerFetch or disableOtherCrawlers to skip whole categories.
What gets sent
For each matched crawl the middleware sends the request URL, method, response status, the crawler’s user agent, a timestamp, and the crawler’s IP address. It never sends cookies, request bodies, or visitor analytics. The IP is required. A crawler’s connection terminates at your server, so its address is observable only there, and it is what lets Traceten check the crawl against the evidence the vendor publishes. Traceten treats it as a claim to be verified, never as proof on its own. Where the published-range check does not match and the crawler’s claimed vendor supports reverse-DNS verification, the address is sent to Cloudflare’s public DNS resolver as part of that lookup. This happens before it is verified, on failed attempts as well as successful ones, unless a recent identical lookup is still cached. Retention depends on the verdict. Traceten keeps the raw address for 90 days in exactly two cases: it fell inside an IP range the vendor publishes for that specific crawler, or its reverse DNS forward-confirmed to the vendor’s own domain. In every other case, including where the only evidence is that the address sits somewhere in the vendor’s wider network, it is hashed (HMAC-SHA-256 under a per-site key) and the address is discarded. No network operator or country is derived from a crawler IP. Individual crawl records are deleted after 90 days. The aggregate counts behind the charts are kept indefinitely and contain no IP data. The full field list, and what happens above your plan’s crawl allowance (which differs between the free plan and the paid plans), are in crawl data and retention. Crawl hits are metered separately from your event allowance and never count against it.What it costs your server
The trade is worth stating in numbers, because this runs on every request:- Requests that are not crawls, which is nearly all of them: a method check, then a lowercase path compared against the deny-lists, and only if that passes, the user agent lowercased once and searched for the crawler tokens: 58 exact tokens, then 28 provider prefixes for anything unrecognised. All of it is in-process string work: no network call, no I/O, nothing awaited. Most requests never reach the user-agent stage at all, because assets, API routes and browser subresource fetches are rejected by the path and header checks first.
- Requests that are crawls: one
POSTto Traceten, scheduled after your response throughwaitUntilwhere the runtime provides one, bounded by a 1500 ms timeout. There is no batching; one crawl is one report, and crawl volume sits orders of magnitude below pageview volume because of the pre-filter. - Failures: silent. The package never throws per-request and never blocks a response. If Traceten is unreachable, your site does not notice.
- Bundle size: every adapter bundle is under 8 KB gzipped, enforced in CI.

