Live: Tor + abuse feeds refreshed every 6 hours

LayerCall

Verifying AI

Cryptographic proof rather than a score.

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.

If you would rather not write that policy yourself, /v1/agent/authorize below is the same decision expressed as rules.

Authorization: may they do this, here?

Verification answers who is this. It does not answer whether they should be allowed to do the thing they are attempting, and that has historically left one bad choice: block all automation and lose the customers whose assistants act for them, or allow it and get farmed.

What makes a real answer possible is the Signature Agent Card — a document the operator publishes alongside its keys. Its trigger field has exactly two values: fetcher, meaning a person asked for this, and crawler, meaning nobody did. Every classical bot signal reports those two identically.

curl -X POST "https://www.layercall.com/v1/agent/authorize" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{
    "method": "POST",
    "url": "https://yoursite.com/signup",
    "headers": { "signature": "...", "signature-input": "...", "signature-agent": "..." }
  }'
{
  "decision": "deny",
  "matched": "default:crawler_state_change",
  "reason": "ExampleBot declares itself an autonomous crawler and is attempting POST. Nobody asked for this request.",
  "agent": { "verified": true, "host": "examplebot.com", "trigger": "crawler", "publishes_card": true },
  "requests_last_hour": 12
}

What happens when you write no rules

An empty policy is not an open door. These defaults apply, and they are the point of the endpoint:

  • Signature present but invalid → deny. Nobody sends a broken Signature-Agent by accident. It is strictly worse than sending none.
  • Verified fetcher → allow, including on POST. Someone asked for this; refusing it refuses your customer.
  • Verified crawler attempting a state change → deny. An agent acting on nobody’s behalf has no business signing up or checking out.
  • Verified crawler reading → allow.
  • Verified, no card → review. We know who they are and not what they intend. Most operators have not published a card yet.
  • No signature → review, and we say so plainly. Identity is not a question this endpoint can answer; score the request with /v1/score/user instead.

Your own rules

Rules are evaluated in order and the first match wins, the same precedence /v1/rules uses. Free to read and write.

curl -X PUT "https://www.layercall.com/v1/agent/policy" \
  -H "X-Api-Key: tl_live_your_key" -H "Content-Type: application/json" \
  -d '{ "rules": [
    { "agent": "scraper.example", "action": "deny", "reason": "not welcome" },
    { "trigger": "crawler", "path": "/api", "action": "allow", "max_per_hour": 100 }
  ] }'

A fetcher is not a free pass

Worth being direct about the limit. Someone can tell their assistant to open fifty trial accounts, and it will honestly declare itself fetcher every single time — because it is telling the truth. The card describes what kind of automation this is; it cannot describe how much. Fifty linked signups can, which is what /v1/score/user and its linkage signals are for. Use both.

Why verification fails

reason is 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 on the IETF standards track. On 1 September 2026 the individual submission was replaced by draft-ietf-webbotauth-httpsig-protocol under a chartered webbotauth working group. It is 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: false as “unsigned” rather than “suspicious”. For unsigned automation, use /v1/score/device instead.

Verifying the signature tells you who is at the door. Whether to open it is a separate decision, and it is the one nobody else is writing about — should a customer’s AI assistant be allowed to sign up for them?