Skip to content
Get started
Documentation menu

Crawler analytics

This page lists how Hatamoto records requests from AI crawlers and assistants, how it checks that they are genuine, how long it keeps them, and every place you can read the results. To read the reports in the admin, follow See AI crawler visits.

Overview

Hatamoto records every request from a known AI crawler or assistant, with the operator, the purpose, the response status and, for pages, the entity that was fetched. It also counts human visits that an AI assistant sends you. Nothing leaves your server.

WhatStored whereKept
One row per crawler requesthatamoto_agent_hits, monthly partitionsabout 90 to 120 days
Daily totals per agenthatamoto_agent_dailyindefinitely
Daily pageviews and AI-referred visitshatamoto_analytics_dailyindefinitely

For people, Hatamoto keeps almost nothing: a daily pageview total and, for visits that arrive from an AI assistant, a daily count per assistant. It stores no cookie, no visitor identifier and no landing path for those visits. Crawlers are counted per request, because a crawler is a machine and there is no visitor to protect.

Data flow

The server renders the whole public site itself, so every request already carries the real User-Agent. A middleware records the hit in process after the handler has answered, with the real status, byte count and duration.

pages, robots.txt, sitemaps, llms.txt, feeds
   -> capture middleware -> hit writer -+-> hatamoto_agent_hits   (monthly partitions)
      (non-blocking, batched)           +-> hatamoto_agent_daily  (daily rollup)

The agent registry

The agent registry, backend/internal/agents/registry.json, is the single source of truth: user-agent patterns, operator, purpose, robots.txt token, the published IP-range sources and the AI referrer list. It is embedded in the program and lists 43 agents. A request whose user agent matches no registry agent is not recorded.

PurposeMeaningExamples
trainingCollecting content for model training.GPTBot, ClaudeBot, CCBot, Google-Extended
search_indexBuilding an AI search index.OAI-SearchBot, Claude-SearchBot, PerplexityBot
user_fetchFetching a page because a user asked an assistant.ChatGPT-User, Claude-User, Perplexity-User
other_aiOther AI-related crawling.GoogleOther
searchClassic search engines.Googlebot, Bingbot
socialLink previews.Slackbot, Twitterbot
seoSEO tools.AhrefsBot, SemrushBot
archiveWeb archives.Internet Archive

"AI" in the reports means the first four purposes. Some agents, mostly user-triggered fetchers, are flagged as ignoring robots.txt, and the admin marks them. The full token list per purpose is in robots.txt and llms.txt.

What is captured

PathSource
Pages, /robots.txt, /sitemap.xml, /sitemap-<group>.xml, /llms.txt, /feed.xml, /atom.xmlRecorded in process by the server, with the real status.
/api/* and /media/*Not recorded by the server. Media can be added from an access log, see Access-log ingest.

Each source records a path once, so counts are never doubled. A HEAD request to a captured route is recorded like a GET.

Verification

Anyone can forge a user-agent string, so a background job checks each hit against the operator's published method and stores the result in hatamoto_agent_hits.verification.

StateMeaning
pendingNot checked yet, or a DNS lookup failed and will be retried. The default on insert.
verifiedThe IP is inside the operator's published ranges, or forward-confirmed reverse DNS matches: the PTR name ends in an operator suffix and resolves back to the IP.
spoofedIt claims to be the agent, but the IP fails the operator's check.
unverifiableThere is nothing to check against: the agent publishes no ranges or reverse DNS, the hit has no IP, the ranges could not be downloaded, or the hit stayed pending for 24 hours.

pending is never final. Hits still undecided after 24 hours become unverifiable, which keeps the 30-day IP truncation safe. Hits older than 7 days are no longer examined.

IP ranges come from the operators' own JSON lists: Google, Bing, OpenAI (three lists), Perplexity (two lists), Anthropic, CCBot, DuckDuckGo, Apple and Ahrefs. They are downloaded only by a background job, never at request time, with a 20 second timeout and a 4 MiB cap, and stored in the database so a restart does not forget them. A failed download keeps the previous ranges. If a list can never be fetched, the agents that depend on it end up unverifiable, not spoofed.

The IP of a hit is the client IP the server sees: the rightmost untrusted X-Forwarded-For entry. Behind a CDN that is the CDN's address. See Access-log ingest.

Background jobs and retention

The jobs run in the in-process worker. Turn one off with the comma-separated environment variable WORKER_DISABLE_JOBS, using these names:

JobSchedulePurpose
agent_ranges_refreshAbout 90 seconds after start, then dailyDownload operator IP ranges.
agent_verifyAbout 2 minutes after start, then every 5 minutesVerify up to 500 distinct (agent, IP) pairs per run, oldest first.
agent_hits_maintenanceDailyTruncate client IPs older than 30 days.
agent_hits_partitionDailyKeep monthly partitions ahead and drop old ones.
  • IP truncation. After 30 days a client IP is cut to its /24 (IPv4) or /48 (IPv6) network.
  • Raw hits. Monthly partitions are created two months ahead and three are kept, so raw hits live roughly 90 to 120 days.
  • Rollup. The daily rollup hatamoto_agent_daily is kept, so reports over any range stay complete.
  • Startup safety net. On every start the server creates the current and next month's partitions before it accepts hits, so nothing lands in the default partition, which is never trimmed. It logs a warning if that default partition already holds rows.

With HATAMOTO_WORKER=off (meant for multi-replica setups where one replica runs the worker) hits stay pending, IPs are not truncated and old partitions are not dropped. Capture itself keeps working, but restart such a replica at least monthly. See Multi-replica and health.

Never blocking a request

Recording a hit never delays a response. The writer stores hits in batches of up to 100, about every second. It uses a bounded queue of 2000 hits, a non-blocking send, a drop counter and at most one warning per 30 seconds. Shutdown drains buffered hits for up to 5 seconds. The admin overview and the API report the number of dropped hits since the process started.

AI referral attribution

A person who clicks a link inside an AI assistant is counted in the daily table hatamoto_analytics_daily, with kind = 'ai_referral' and the assistant as the key, for example chatgpt. The same table holds the daily pageview total (kind = 'pageview'), fed by one cookie-less POST /api/v1/track beacon per page load, sent by the default theme's script.

  • Where. After a page route has answered, a GET that returned 200 with an HTML content type to a user agent that is not a registry agent is classified. Sitemaps, feeds, robots.txt, error pages and HEAD requests are never counted.
  • Classification. utm_source (lowercased) is checked first, then the host of the Referer. A referrer matches when its host equals a listed host or is a subdomain of it, so gemini.google.com never matches www.google.com, and evilchatgpt.com never matches chatgpt.com.
  • No dedupe. Each qualifying page load counts once. Navigation inside your site carries your own referrer and is not counted.
  • Privacy. Only the assistant name and the day are stored. The landing path, referrer, IP address and user agent are discarded. Counts are held in memory and written about every 30 seconds and on shutdown.

The ai_referrers list names nine assistants:

KeyReferrer hostsutm_source values
chatgptchatgpt.com, chat.openai.comchatgpt.com, chatgpt
perplexityperplexity.aiperplexity.ai, perplexity
claudeclaude.aiclaude.ai, claude
geminigemini.google.comgemini.google.com, gemini
copilotcopilot.microsoft.comcopilot.microsoft.com, copilot
grokgrok.comgrok.com, grok
meta_aimeta.aimeta.ai, meta_ai
deepseekchat.deepseek.comchat.deepseek.com, deepseek
mistralchat.mistral.aichat.mistral.ai, mistral

To check it locally, with DATABASE_URL set:

curl -s -o /dev/null -H 'Referer: https://chatgpt.com/' -H 'User-Agent: Mozilla/5.0' http://localhost:8090/
sleep 35
psql "$DATABASE_URL" -c "SELECT day, kind, key, count FROM hatamoto_analytics_daily ORDER BY day DESC LIMIT 3;"

The admin screens

AI analytics in the sidebar (/admin/analytics/crawlers) has three tabs: Crawlers, Referrals (/admin/analytics/referrals) and Pageviews (/admin/analytics/pageviews). Every view except Robots policy has a date range, the last 30 days by default and at most 366 days.

Crawlers viewShows
OverviewAI fetches, the verified share, spoofed fetches, visits from AI assistants and pageviews; fetches over time grouped by operator or purpose; the verification split; the top agents.
AgentsEvery agent seen, with operator, purpose, fetches and verification. One agent's page adds its activity, daily fetches, most fetched pages, response status classes, its robots.txt name and a note when it may ignore robots.txt.
PagesThe ten most fetched pages, every page fetched, and published pages no AI agent has fetched.
First crawlsPages first fetched in the range, which agent fetched them and how long after publishing.
ErrorsFetches with status 400 or above, grouped by page and status. A 404 row has a Create redirect action.
SpoofedRecent hits that failed verification, with the claimed agent and the masked network.
Robots policyThe policy per purpose. See robots.txt and llms.txt.

Referrals shows visits from AI assistants, pageviews, the AI share of traffic and visits by assistant. Any signed-in admin can read these screens; only an owner can change the robots policy. The old addresses under /admin/analytics/agents, and /admin/analytics itself, redirect to /admin/analytics/crawlers.

The post editor has an AI crawlers card on its SEO and AEO tab: assistant fetches of the post in the last 7 and 30 days, when it was first crawled (only for posts published after tracking began), and the last fetch and status per AI agent.

Admin API

All routes are under /api/v1/agent-analytics and need a signed-in admin session. Errors are {"error": "..."}, arrays are never null, and no response contains an IP address: the spoof log returns ip_masked, the /24 or /48 network.

Dates are from and to as YYYY-MM-DD in UTC, inclusive, defaulting to the 30 days ending today. A bad date, from after to, or a span over 366 days is 400. Paged routes take page (at most 10000) and per_page (default 50, maximum 200).

RouteReturns
GET /overview?group=operator or group=purposeTotals, verification split, AI-referred visits, pageviews, a daily series, top agents and dropped_hits.
GET /pagesTop fetched paths and published entities never fetched.
GET /agents, GET /agents/{id}Agents seen in the range, and one agent's detail.
GET /errorsStatus 400 and above, grouped by path and status.
GET /spoofedThe newest spoofed hits with ip_masked.
GET /entities/{id}The editor card data for one entity.
GET /robots-policy, PUT /robots-policyRead the policy. Replace it (owner only, otherwise 403). See robots.txt and llms.txt.

Plugin hooks

Two asynchronous actions let a plugin react, and neither carries an IP address or user agent. See Hooks and events.

  • agent.hit fires for every stored hit with the agent id, operator, purpose, verification (normally pending), method, path, entity id, source, status, bytes, duration and time.
  • agent.first_crawl fires once per entity on its first served (status 200) AI fetch, with the agent, operator, purpose, the time of the fetch and the publish time.

Command line

Three read-only commands print the same data as JSON. They read the database directly, so they need DATABASE_URL.

hatamoto analytics crawlers --from 2026-01-01 --to 2026-01-31 --limit 20
hatamoto analytics first-crawls --limit 20
hatamoto analytics referrals --from 2026-01-01
CommandPrints
crawlersThe top pages and the per-agent totals.
first-crawlsPages by first AI crawl.
referralsVisits per day and assistant.
FlagDefaultMeaning
--from29 days before --toFirst day, YYYY-MM-DD, UTC.
--totoday, UTCLast day, YYYY-MM-DD.
--limit20Rows, 1 to 100. Not used by referrals.

The full command list is in the CLI reference. The same data is available to AI agents through the MCP tools.