AI visit tracking
Geoffy can show you which AI systems read your pages (your real product pages, not only the files Geoffy serves) and which shoppers arrive from an AI assistant. It takes one small middleware, and it is entirely optional: nothing else in these guides depends on it.
It needs @geoffy/headless 0.3.1 or later.
Add the middleware
Section titled “Add the middleware”Reports are signed with your revalidate secret, and without it nothing is sent. Copy it
from your site’s settings in Geoffy into GEOFFY_REVALIDATE_SECRET. It is the same secret a
Next.js site uses for its revalidate route. On Astro, signing these reports is its only use.
import { createGeoffyAiVisitMiddleware } from "@geoffy/headless/astro-middleware";
export const onRequest = createGeoffyAiVisitMiddleware({ siteKey: import.meta.env.GEOFFY_SITE_KEY, secret: import.meta.env.GEOFFY_REVALIDATE_SECRET,});Pass the secret as shown. On its own the package can only look in process.env, which an
edge runtime does not have and which does not always carry your .env values. Passing it
through import.meta.env works everywhere.
Already have an onRequest? Combine the two with sequence() from astro:middleware.
The report is handed to the platform’s waitUntil where your adapter provides one, so it
finishes after the response.
What it reports
It reports a request when either of these is true:
- The user agent names a known AI agent: GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot, Google-Extended, Amazonbot, CCBot, bingbot and others.
-
The visitor arrived from an AI assistant. The
refereris, orutm_sourcenames, chatgpt.com, chat.openai.com, perplexity.ai, gemini.google.com, copilot.microsoft.com, claude.ai or grok.com.
Only GET and HEAD requests count, and prefetches are ignored.
Every other request, which is every ordinary page view, makes no network call at
all. The decision is made on your server, so your shoppers' visits never leave it.
What it sends, and what it never does
For each reported request it sends the time, the page's URL without its
query string, and the assistant's host or utm_source.
- For a request that claims to be an AI crawler, it also sends the user agent and, only where your platform vouches for one, the visitor's address. That is what lets Geoffy check the claim against the vendor's published address ranges.
- For a shopper arriving from an assistant, it sends neither: no user agent and no address.
- It reads no cookies and sets none, so it needs no consent banner.
A crawler sweep
Anyone can send a crawler's user agent, so each server process makes at most 60 calls a minute to Geoffy. The limit is on calls, not on visits. An AI crawler can read hundreds of your pages in a minute, so a visit that arrives after the budget is spent waits for the next call instead of being thrown away, and one call carries up to 50 visits. Your server makes the same small number of calls either way.
Crawler visits and assistant referrals have separate budgets and separate queues, so a flood of claimed crawlers can neither spend the calls nor fill the queue your shoppers depend on. Each queue holds up to 500 visits. In a burst big enough to fill it, or when a visit has waited more than five minutes, the oldest are given up on. Their count travels with the next report, so the figures Geoffy shows you say they are a minimum instead of presenting a short number as a total.
New AI agents
The list of AI agents ships with the package and is topped up from Geoffy once a day, so a new crawler is recognised without an upgrade. If the update cannot be read, the built-in list applies.
The visitor’s address
Section titled “The visitor’s address”| Where your site runs | Default |
|---|---|
| Vercel, with visitors reaching Vercel directly | The address Vercel sets for the request. Nothing to configure |
| Vercel behind another CDN or proxy | Vercel sees the CDN’s address, so crawler visits cannot be confirmed. Pass clientIp with the address your CDN vouches for |
| Anywhere else | None. Visits are reported without an address |
The default never reads X-Forwarded-For. Its first entry is whatever the client chose to
send, so trusting it would let anyone claim to be a crawler from a vendor’s address range.
If your platform gives you an address you can trust, pass it. On Astro the function also receives the middleware context:
createGeoffyAiVisitMiddleware({ siteKey: import.meta.env.GEOFFY_SITE_KEY, secret: import.meta.env.GEOFFY_REVALIDATE_SECRET, clientIp: (request, context) => context.clientAddress,});Options
Section titled “Options”| Option | Default | |
|---|---|---|
siteKey | none | Required. Without it nothing is sent |
secret | GEOFFY_REVALIDATE_SECRET from process.env | Signs each report. Without one nothing is sent |
origin | as everywhere else in this package | |
clientIp | see above | (request, context) => string | null | undefined |
exclude | static files | Extra paths never reported. A string is a path prefix, a RegExp is tested against the path. Added to the defaults, never instead of them |
sample | 1 | Report this fraction of matching requests, 0 to 1 |
timeoutMs | 3000 | Give up on a report after this long. It never delays your response |
Without the middleware
Section titled “Without the middleware”The namespace endpoint already tells Geoffy which AI
agent asked for a file, by the agent’s name (GPTBot). A browser’s user agent is never
passed on. So even without the middleware, Geoffy sees the agents that read your product
text versions and your guides, as often as your cache fetches them from Geoffy.
The site-wide text endpoints can do the same, but only when you ask, and only on an endpoint rendered on request:
import { createGeoffyTextEndpoint } from "@geoffy/headless/astro";
export const prerender = false;export const GET = createGeoffyTextEndpoint( { siteKey: import.meta.env.GEOFFY_SITE_KEY }, "llms.txt", { forwardCrawler: true },);What only the middleware can show is who reads your own product pages, and who arrives from an assistant.