Observe AI Agents

AI crawlers — from GPTBot to ClaudeBot to PerplexityBot — read your site every day, and most site owners never see it. Observe mode is level 1 of SilentShield's AI Agent Control: it records which AI services visit which pages, without blocking anything and without changing how your site behaves.

Visibility comes before control: only once you know who reads your content can you sensibly decide whom to allow, throttle, or block. Every sighting is therefore sorted into one of three categories:

Training
The crawler collects your content to train future AI models (e.g. GPTBot, ClaudeBot, CCBot).
AI Search
The service reads pages to answer users' questions and can cite your site as a source (e.g. OAI-SearchBot, PerplexityBot).
Agents
A bot acts on behalf of a specific user in real time, for example researching a product or filling out a form on your site (e.g. ChatGPT-User, Claude-User).

Setup by Platform

WordPress

The built-in observer arrives with an upcoming SilentShield plugin update. Until then, activate observation with a small must-use plugin — one file, no configuration:

Create the file wp-content/mu-plugins/silentshield-observer.php with this content (mu-plugins load automatically; delete the file to disable):

wp-config.phpphp
<?php
/**
 * Plugin Name: SilentShield — AI Agent Observer
 * Description: Reports AI crawler visits to SilentShield (fire-and-forget;
 * IP/UA are hashed server-side, GDPR). Delete this file to disable.
 *
 * Install: save this file as wp-content/mu-plugins/silentshield-observer.php
 * (mu-plugins load automatically — no activation needed). To force it off
 * without deleting: define('SILENTSHIELD_OBSERVER', false) in wp-config.php.
 * An upcoming SilentShield plugin update will build this in.
 */
if (!defined('ABSPATH')) exit;
if (defined('SILENTSHIELD_OBSERVER') && !SILENTSHIELD_OBSERVER) return;

const SS_OBSERVE_URL = "https://api.silentshield.io/api/v1/agent/telemetry";
const SS_API_KEY = "YOUR_API_KEY"; // publishable site key
// For the always-fresh list, cache https://api.silentshield.io/api/v1/agent/bot-directory daily.
$ss_bots = ["GPTBot","OAI-SearchBot","ChatGPT-User","ClaudeBot","Claude-User",
  "PerplexityBot","Perplexity-User","Bingbot","Applebot","Amazonbot","CCBot",
  "Bytespider","meta-externalagent","Google-Agent"];

register_shutdown_function(function () use ($ss_bots) {
  $ua = $_SERVER["HTTP_USER_AGENT"] ?? "";
  $signed = !empty($_SERVER["HTTP_SIGNATURE"]);
  $isBot = $signed;
  foreach ($ss_bots as $b) { if (stripos($ua, $b) !== false) { $isBot = true; break; } }
  if (!$isBot) return;

  if (function_exists("fastcgi_finish_request")) { fastcgi_finish_request(); }

  wp_remote_post(SS_OBSERVE_URL, [
    "timeout"  => 3,
    "blocking" => false,
    "headers"  => ["Content-Type" => "application/json", "x-api-key" => SS_API_KEY],
    "body"     => wp_json_encode(["sightings" => [[
      "ua"     => $ua,
      "ip"     => $_SERVER["REMOTE_ADDR"] ?? "",
      "path"   => strtok($_SERVER["REQUEST_URI"] ?? "/", "?"),
      "method" => $_SERVER["REQUEST_METHOD"] ?? "GET",
    ]]]),
  ]);
});

Next.js

A self-contained middleware — no SDK required. Observation runs via event.waitUntil(), off the response path, so it never delays your pages:

middleware.tstypescript
// middleware.ts — self-contained observer, no SDK required.
// Sightings are reported via event.waitUntil(), off the response path,
// so your pages never wait. Best-effort and fail-open by design.

import { NextResponse, type NextRequest } from "next/server";

const OBSERVE_URL = "https://api.silentshield.io/api/v1/agent/telemetry";
const API_KEY = "YOUR_API_KEY"; // publishable site key

// Known AI-agent UA tokens. For the always-fresh list, fetch
// https://api.silentshield.io/api/v1/agent/bot-directory once a day and cache it.
const BOTS = ["GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-User",
  "PerplexityBot", "Perplexity-User", "Bingbot", "Applebot", "Amazonbot", "CCBot",
  "Bytespider", "meta-externalagent", "Google-Agent"];

export function middleware(req: NextRequest, event: { waitUntil(p: Promise<unknown>): void }) {
  const ua = req.headers.get("user-agent") ?? "";
  const signed = !!req.headers.get("signature");
  if (signed || BOTS.some((b) => ua.toLowerCase().includes(b.toLowerCase()))) {
    const sighting = {
      ua,
      path: req.nextUrl.pathname,
      method: req.method,
      ip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
      ...(signed && {
        signature: req.headers.get("signature"),
        signature_input: req.headers.get("signature-input"),
        signature_agent: req.headers.get("signature-agent"),
        authority: req.nextUrl.host,
        scheme: req.nextUrl.protocol.replace(":", ""),
      }),
    };
    event.waitUntil(
      fetch(OBSERVE_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
        body: JSON.stringify({ sightings: [sighting] }),
      }).catch(() => {}) // best-effort, fail-open
    );
  }
  return NextResponse.next();
}

Node.js (Express)

For Express (or any other Node server), add a fire-and-forget middleware. It only reports requests whose User-Agent matches a known AI agent or that carry a Web Bot Auth signature — and it never blocks the response:

server.jsjavascript
import express from "express";
const app = express();

const OBSERVE_URL = "https://api.silentshield.io/api/v1/agent/telemetry";
const API_KEY = "YOUR_API_KEY";

// Known AI-agent UA tokens. For the always-fresh list, fetch
// https://api.silentshield.io/api/v1/agent/bot-directory once a day and cache it.
const BOTS = ["GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-User",
  "PerplexityBot", "Perplexity-User", "Bingbot", "Applebot", "Amazonbot", "CCBot",
  "Bytespider", "meta-externalagent", "Google-Agent"];

// Fire-and-forget observer — never blocks the response.
app.use((req, res, next) => {
  const ua = req.headers["user-agent"] || "";
  const signed = !!req.headers["signature"];
  if (signed || BOTS.some((b) => ua.toLowerCase().includes(b.toLowerCase()))) {
    const sighting = {
      ua, ip: req.ip, path: req.path, method: req.method,
      ...(signed && {
        signature: req.headers["signature"],
        signature_input: req.headers["signature-input"],
        signature_agent: req.headers["signature-agent"],
        authority: req.headers["host"], scheme: req.protocol,
      }),
    };
    fetch(OBSERVE_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
      body: JSON.stringify({ sightings: [sighting] }),
    }).catch(() => {}); // best-effort, fail-open
  }
  next();
});

PHP

For plain PHP, include this observer at the very top of your front controller. It sends the sighting only after the response has been delivered (fastcgi_finish_request), so the page is never slowed down:

observer.phpphp
<?php
// Drop-in observer: include at the very top of your front controller.
// It runs after the response is sent (fastcgi_finish_request) so it never
// slows down the page. Reuses the same publishable key.

const SS_OBSERVE_URL = "https://api.silentshield.io/api/v1/agent/telemetry";
const SS_API_KEY = "YOUR_API_KEY";
// For the always-fresh list, cache https://api.silentshield.io/api/v1/agent/bot-directory daily.
$ss_bots = ["GPTBot","OAI-SearchBot","ChatGPT-User","ClaudeBot","Claude-User",
  "PerplexityBot","Perplexity-User","Bingbot","Applebot","Amazonbot","CCBot",
  "Bytespider","meta-externalagent","Google-Agent"];

register_shutdown_function(function () use ($ss_bots) {
  $ua = $_SERVER["HTTP_USER_AGENT"] ?? "";
  $signed = !empty($_SERVER["HTTP_SIGNATURE"]);
  $isBot = $signed;
  foreach ($ss_bots as $b) { if (stripos($ua, $b) !== false) { $isBot = true; break; } }
  if (!$isBot) return;

  if (function_exists("fastcgi_finish_request")) { fastcgi_finish_request(); }

  $sighting = [
    "ua" => $ua,
    "ip" => $_SERVER["REMOTE_ADDR"] ?? "",
    "path" => strtok($_SERVER["REQUEST_URI"] ?? "/", "?"),
    "method" => $_SERVER["REQUEST_METHOD"] ?? "GET",
  ];
  $ch = curl_init(SS_OBSERVE_URL);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 3,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json", "x-api-key: " . SS_API_KEY],
    CURLOPT_POSTFIELDS => json_encode(["sightings" => [$sighting]]),
  ]);
  curl_exec($ch); // best-effort, fail-open
  curl_close($ch);
});

Go

A self-contained observer in a few lines — stdlib only, no SDK. Sightings are reported fire-and-forget, so no request gains latency:

main.gogo
// Self-contained observer — stdlib only, no SDK required.
// Reports AI-agent sightings fire-and-forget: never blocks a request,
// fail-open on any error. For the always-fresh bot list, cache
// https://api.silentshield.io/api/v1/agent/bot-directory daily.

const observeURL = "https://api.silentshield.io/api/v1/agent/telemetry"
const siteKey = "YOUR_API_KEY" // publishable — or os.Getenv("SILENTSHIELD_SITE_KEY")

var bots = []string{"GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot",
    "Claude-User", "PerplexityBot", "Perplexity-User", "Bingbot", "Applebot",
    "Amazonbot", "CCBot", "Bytespider", "meta-externalagent", "Google-Agent"}

func observe(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ua, signed := r.UserAgent(), r.Header.Get("Signature") != ""
        hit := signed
        for _, b := range bots {
            if strings.Contains(strings.ToLower(ua), strings.ToLower(b)) {
                hit = true
                break
            }
        }
        if hit {
            ip, _, _ := net.SplitHostPort(r.RemoteAddr)
            s := map[string]any{"ua": ua, "ip": ip, "path": r.URL.Path, "method": r.Method}
            if signed { // Web Bot Auth (RFC 9421) — forward for verification
                s["signature"] = r.Header.Get("Signature")
                s["signature_input"] = r.Header.Get("Signature-Input")
                s["signature_agent"] = r.Header.Get("Signature-Agent")
                s["authority"] = r.Host
            }
            body, _ := json.Marshal(map[string]any{"sightings": []any{s}})
            go func() { // best-effort, fail-open
                req, _ := http.NewRequest("POST", observeURL, bytes.NewReader(body))
                req.Header.Set("Content-Type", "application/json")
                req.Header.Set("x-api-key", siteKey)
                if resp, err := (&http.Client{Timeout: 3 * time.Second}).Do(req); err == nil {
                    resp.Body.Close()
                }
            }()
        }
        next.ServeHTTP(w, r)
    })
}

// net/http:  http.ListenAndServe(":8080", observe(mux))
// Gin:       srv := &http.Server{Addr: ":8080", Handler: observe(r)}
//            srv.ListenAndServe()

Test the Connection

You don't have to wait for the first real AI crawler to confirm that your integration works. Send an empty batch — it registers a heartbeat for your key without writing any fake sightings:

Terminalbash
curl -X POST https://api.silentshield.io/api/v1/agent/telemetry \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  --data '{"sightings":[]}'

The agent hub of your API key (API Keys → select a key → Agent) then switches from “not connected” to “connected — listening”. Your report stays empty until a real agent visits: SilentShield never invents data.

GDPR

Observation relies on legitimate interest (Art. 6(1)(f) GDPR). IP addresses are hashed server-side with a salt that rotates daily — raw IPs are never stored. Sightings are retained for 14 days, and only requests from known AI agents are stored at all: regular visitor traffic is never recorded.

FAQ

Does this slow down my site?

No. Every integration is fire-and-forget: Next.js uses event.waitUntil(), PHP reports only after the response has been flushed, Node and Go send in the background. And if the SilentShield API is ever unreachable, your site is unaffected — everything fails open.

What counts as an AI agent?

Only requests that match SilentShield's bot directory of known AI crawlers and agents (GPTBot, ClaudeBot, PerplexityBot, …) or that carry a Web Bot Auth signature. The always-current list is available at https://api.silentshield.io/api/v1/agent/bot-directory.

Why don't I see any data yet?

AI crawlers come on their own schedule. On most sites the first sightings appear within 24–72 hours; smaller sites can take longer. In the meantime the heartbeat in your agent hub confirms that the integration itself is working.

Next: enforce agent rules (level 2) →