Webclat / GTM Practice

Classify agent vs human requests in server-side GTM

Once your server container sees every request, the next problem is telling who sent each one. There is no single reliable signal - agents often do not self-identify, and spoofers borrow the names of the ones that do. The working answer is a layered classification, ordered by trust.

Landscape as of September 2026

Answer in brief

Classify each incoming request once, in the server container's request path, using signals layered by trust: a verified Web Bot Auth signature (cryptographic proof) beats a published IP range (verifiable), which beats a declared user-agent string (spoofable), which beats behavioral heuristics (probabilistic). Write the verdict to a single event parameter such as traffic_type - agent_verified, agent_declared, crawler, or human - and make every downstream tag consume that one field instead of re-deciding.

Why there is no single reliable signal

There is no standardized way to distinguish an acting agent from a human or a crawler yet - vendors like HUMAN Security and Quantum Metric combine network, fingerprint, and behavioral signals precisely because no one signal holds. Agents frequently do not self-identify via user-agent or referrer, and a significant share of requests claiming to be ChatGPT or Perplexity bots are spoofed. So the classification cannot be an if-statement on the UA. It has to be a small decision ladder, and the ladder's order is trust.

The signal ladder

LayerSignalTrustHow to read it in sGTM
1. CryptographicWeb Bot Auth signature headers (Ed25519 over RFC 9421 HTTP Message Signatures; key published at a well-known URL)Highest - proof, not a claimCheck for Signature / Signature-Input / Signature-Agent headers; verify upstream at your edge or a verifying proxy and forward the verdict as a header the container trusts
2. NetworkSource IP against the published ranges of OpenAI, Anthropic, Perplexity, GoogleHigh - verifiable, but lags new rangesResolve at the edge (the container itself should not do per-request range lookups); forward as a boolean header
3. DeclaredUser-agent tokens: ChatGPT-User, ChatGPT agent, Operator/Atlas, Perplexity-User (acting) vs PerplexityBot (crawling), Claude and Gemini agent tokensMedium - honest agents onlyRegex on the UA event field; keep the pattern list in one lookup variable so updates touch one place
4. BehavioralNo cookies ever, no consent interaction, page-fetch cadence, missing sec-fetch browser headers, headless flagsLow - probabilisticCombine two or more before concluding; a single behavioral tell misfires on privacy-conscious humans

The full directory of acting-agent user-agent strings - and how they differ from the crawler UAs your bot filters already know - is maintained at the AI agent user-agent directory. The cryptographic layer is explained end to end at Web Bot Auth explained. And when a request claims an agent name but fails layers 1-2, that is the spoofing case: verifying the request really came from ChatGPT or Claude.

Wiring it into the container

Classify once, stamp once, consume everywhere. The decision runs where the request enters - your ingest client (if you built the server-side capture path) or a transformation applied to events from the web container - and writes one parameter:

// Inside a server container variable / transformation (sandboxed JS)
const ua  = getEventData('agent_signals.ua') || getEventData('user_agent') || '';
const sig = getEventData('agent_signals.sig_verified');   // set by your edge
const ipv = getEventData('agent_signals.ip_verified');    // set by your edge

const ACTING = /(ChatGPT-User|ChatGPT agent|Operator|Atlas|Perplexity-User|Claude-User|Gemini)/i;
const CRAWLER = /(GPTBot|ClaudeBot|CCBot|PerplexityBot|Google-Extended|Bytespider)/i;

let verdict = 'human';
if (sig === 'true')            verdict = 'agent_verified';
else if (CRAWLER.test(ua))     verdict = 'crawler';
else if (ACTING.test(ua))      verdict = ipv === 'true' ? 'agent_verified' : 'agent_declared';
return verdict;   // -> written to event param traffic_type

Two routing rules downstream keep the data honest:

  • Never silently drop agent_declared. It is your largest agent bucket and, per Adobe's March 2026 measurement, AI-driven traffic converted 42% better than non-AI - the segment is worth reporting, not discarding. Whether to block rather than measure is a business decision, not a container default: the block-or-measure decision guide covers it.
  • Send crawler nowhere expensive. Indexing bots do not belong in GA4 sessions or ad-platform audiences. Log counts if you want the trend, forward nothing else.

How to verify it worked

  1. Send one request per rung. From a terminal: curl -A "ChatGPT-User/1.0" https://your-site.com/ (expect agent_declared), curl -A "GPTBot/1.0" ... (expect crawler), a plain browser visit (expect human).
  2. Read the verdicts in Preview. In GTM server Preview mode, open each request and confirm the traffic_type parameter on the outgoing event matches the expectation. A misclassified rung means your regex list or edge headers are wrong - fix there, not in the tags.
  3. Confirm the spoof case fails safe. curl -A "ChatGPT-User/1.0" from your own machine must NOT come out agent_verified - your IP is not in OpenAI's ranges. If it does, your edge is setting ip_verified unconditionally.
  4. Check the report surface. In GA4, the traffic-type parameter should be usable in a comparison; the passing result is three distinguishable slices whose counts move when you replay the curls.

Common questions

  • Can I rely on the user-agent string to detect AI agents?
    No. The user-agent is a self-declared, spoofable label: honest agents identify themselves, dishonest ones claim to be Chrome, and a meaningful share of requests claiming to be ChatGPT or Perplexity bots are spoofed. Treat the UA as one signal in a layered classification, never the whole decision.
  • What is the most trustworthy agent signal available today?
    A verified Web Bot Auth signature. The bot signs each request with an Ed25519 key and publishes the public key at a well-known URL, so the server verifies cryptographic proof instead of a claimed name. As of September 2026 it is still an IETF individual draft with partial adoption, so it identifies the signers, not the whole agent population.
  • Where in server-side GTM does classification logic live?
    In the request path before routing: either in a custom client that stamps the classification onto every event it claims, or in a transformation/variable that downstream tags read. Classify once per request, write the result to one event parameter (for example traffic_type), and make every tag consume that single field.
  • Should classified agent traffic be dropped?
    Segment it, do not drop it by default. Adobe Analytics measured AI-driven traffic converting 42% better than non-AI traffic in March 2026 - an acting agent is often shopping on a real customer's behalf. Drop only verified crawler noise; route acting agents into their own traffic type so reports can include or exclude them deliberately.

Want the classification ladder built and tested on your container?

We implement layered agent classification with the verification suite to prove each rung - spoof cases included.

Talk to a Google Tag Manager Expert