Verifying AI agents

Every other check here weighs evidence. This one proves. An agent signs its request with a private key, we verify it against a key the agent publishes, and the answer is arithmetic — the only signal in this API with no false-positive rate.

It changes the question.A growing share of legitimate traffic is automated — a real customer delegating a task to an AI assistant trips every classical bot signal there is, and refusing them is simply wrong. “Is this automated?” is becoming the wrong question. “Is this a known agent?” is one a signature can answer and a user-agent string never could.

How it works

The agent sends three headers. You forward them, plus the URL it requested, and we do the rest: fetch the agent’s public keys from /.well-known/http-message-signatures-directory, match the key by thumbprint, and verify the Ed25519 signature over the request.

curl -X POST https://www.layercall.com/v1/verify/agent \
  -H "X-Api-Key: tl_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "GET",
    "url": "https://yoursite.com/pricing",
    "headers": {
      "signature-agent": "\"https://chatgpt.com\"",
      "signature-input": "sig=(\"@authority\" \"@method\" \"@path\");created=…;expires=…;keyid=\"…\";tag=\"web-bot-auth\"",
      "signature": "sig=:base64signature:"
    }
  }'
{
  "verified": true,
  "agent": "https://chatgpt.com",
  "keyid": "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
  "purpose": "ai",
  "reason": null,
  "expires_in": 3421,
  "replay_protection": "signature-window"
}

Pass the agent’s request, not ours

The signature covers the method, authority and path the agent actually called — your URL. None of it can be inferred from the request you make to us, which is why url is required and why a wrong one produces signature_mismatch rather than a vague failure.

There is no score, on purpose

A verified signature is a fact, not a judgement. What to do with it is your policy: an assistant acting for one of your customers is usually welcome, a scraper usually is not, and both may be perfectly signed. We tell you who it is; you decide.

Why verification fails

reasonis always specific, because “not verified” on its own is not actionable.

reasonMeaning
no_signatureThe request carried no signature at all — most traffic
not_web_bot_authSigned, but tagged for a different purpose
expiredThe signature's validity window has passed
created_in_futureSigned with a clock well ahead of ours
directory_unreachableThe agent's key directory did not answer
key_not_foundThe keyid is not in the published directory
keyid_thumbprint_mismatchThe key does not hash to the id claiming it
key_expiredThe published key is outside its own nbf/exp window
signature_mismatchThe signature does not verify — wrong key, altered request, or a replay against a different path

Replay protection

replay_protection currently reports "signature-window". A captured signature cannot be replayed against a different path or method — the signature covers both — but within its own validity window (the spec recommends 24 hours or less) the same request can be repeated. Single-use nonce enforcement is not yet implemented, and the field says so rather than leaving you to assume otherwise.

In the SDKs

const result = await lc.verifyAgent({
  method: req.method,
  url: `https://${req.headers.host}${req.url}`,
  headers: req.headers,
});

if (result.verified && result.purpose === "ai") {
  // A known assistant. Serve it, log it, rate-limit it differently.
}
result = client.verify_agent(
    url=f"https://{request.host}{request.path}",
    headers=dict(request.headers),
    method=request.method,
)

The standard

Web Bot Auth is an IETF draft (draft-meunier-web-bot-auth-architecture) built on RFC 9421 HTTP Message Signatures, with Cloudflare, Google, Amazon and OpenAI behind it. Adoption is early — chatgpt.com publishes a directory today, most do not — so treat verified: falseas “unsigned” rather than “suspicious”. For unsigned automation, use /v1/score/device instead.