A2A 1.0 · Ed25519 receipts · SOC 2 in progress
Build October 26, 2026Status
Field note6 min read

Your agent is a bot to every gate it meets

A legitimate agent solved the challenge, sent a clean message, and still got blocked at bot 90. We gave it an Ed25519 passport and ran the same request again. Here is the live evidence, the four proofs, and what identity does not buy.


TL;DR. An AI agent submitted a contact form from a cloud container. It solved the proof-of-work, sent a clean message, and was blocked anyway — decision: block, reason: bot, score 90. The gate was right: the request was automation. The missing piece was not a better user agent string. It was a way for the site to verify which automation was calling and who stood behind it.

We gave the agent an Ed25519 key, signed the same request, and ran it again. The site returned decision: allow, reason: verified_agent_clean, and still computed spam_probability: 0.0166. The bot score stayed 90. Identity moved the request past the automation block; it did not buy a clean content score.

This post explains the four proofs AgentDraft now ships, the live run, and the boundary we deliberately kept.

The wall

Most agent traffic looks the same to a bot gate: a cloud IP, no mouse movement, no keystrokes, a form filled faster than a person can type. That is enough to score the session as automation before the content is ever classified.

The usual fixes are bad:

  • Whitelist the IP. Cloud ranges are shared and rotate. You end up trusting whoever rents the next address.
  • Whitelist the User-Agent. It is a string the client chooses. No verification.
  • Turn off the bot check. Now the gate cannot stop scrapers either.

The gate needs an identity it can verify without calling the agent's vendor on every request. That is what Web Bot Auth is for.

What the gate can actually verify

Web Bot Auth is an IETF profile built on RFC 9421 HTTP Message Signatures. The agent signs the request with a private key. The site fetches the matching public key from a directory named in the Signature-Agent header. If the signature verifies, the site knows the request came from the holder of that key — not from a string the client made up.

AgentDraft publishes one directory per agent:

GET https://api.agentdraft.io/v1/agents/{agent_id}/http-message-signatures-directory
Content-Type: application/http-message-signatures-directory+json

The response contains the public JWK plus a small amount of agent metadata: agent_id, reply_mailbox, and human_delegated. The private JWK is shown once when the key is minted and is never stored by AgentDraft. Signing happens in the agent's runtime; AgentDraft is not on the request path.

The signed profile is deliberately narrow:

("@method" "@authority" "@path" "signature-agent";key="sig1" "content-digest")

That binds the request method, host, path, key directory, and body digest. A signature cannot be replayed against a different URL or a different body.

Four proofs, one key

A single Ed25519 hierarchy now answers four questions that used to require four vendors or a shared secret.

1. A human approved this exact payload. On Team+, resolving an approval or consent request mints an approval_receipt JWT. It carries the reviewer as sub, the agent as the RFC 8693 act claim, a RAR entry with evidence_sha256, and one audience. A downstream service verifies it offline against /.well-known/jwks.json. No call back to AgentDraft, no shared secret.

2. This agent is acting for that human, for this audience. POST /v1/identity/exchange takes the agent's avs_live_… key and returns a shorter-lived token narrowed to one audience and a subset of scopes. An orchestrator can hand a sub-agent a token for one deploy target without handing over the root key.

3. This agent is discoverable and signed. The A2A 1.0 card at /.well-known/agent-card.json is JCS-canonicalized and carries a detached Ed25519 JWS. A calling agent can verify the provider before it sends a task, then use SendMessage / GetTask on the same JSON-RPC endpoint. The 0.2.0 card and tasks/send surface remain for older clients.

4. This request came from this agent, and this MCP client is this agent. The Web Bot Auth directory covers outbound HTTP. The MCP Client ID Metadata Document at /v1/agents/{id}/oauth-client.json covers OAuth: the URL is the client_id, token_endpoint_auth_method is private_key_jwt, and the jwks_uri points at the same signing directory. One key, two protocols.

The live run

We ran the comparison on a throwaway SiftFy Content Gate site in enforce mode, with the action policy set to allow for verified agents. The content was a normal two-night availability question.

The unsigned request solved the challenge and was blocked before inference:

{
  "decision": "block",
  "reason": "bot",
  "bot": { "score": 90, "reasons": ["automation"] },
  "challenge_valid": true
}

The signed request, same content and same client, was allowed:

{
  "decision": "allow",
  "reason": "verified_agent_clean",
  "spam_probability": 0.01661246360668239,
  "bot": {
    "score": 90,
    "reasons": ["automation"],
    "verified_agent": {
      "valid": true,
      "protocol": "ietf-webbotauth-httpsig-00",
      "agent_id": "agt_01KS87N2E4FFK6T1D8PJQ0M8C3",
      "keyid": "TxhUp5dmtVsb1L9j_IN-WGeTPeW8DhXHSVVWoGsGCco",
      "human_delegated": true
    }
  },
  "verification_id": "sf_ver_rS5CXDApIdrGx1eEmF-vhw"
}

Two details matter more than the green allow:

  • The bot score stayed 90. The signature bypassed the bot block, not the scoring. The site can still see that the request was automation.
  • The classifier still ran. spam_probability is present because the content decision was made after inference. A prompt-injected agent does not get a free pass on the text.

The code

Mint the key once in Dashboard → Agents → Signing. The private JWK is shown once. Then sign the outbound request:

from agentdraft import agent_signature_fields, sign_request

headers = sign_request(
    method="POST",
    url="https://forms.example.com/contact",
    private_jwk=private_jwk,   # the one-time value from the dashboard
    agent_id="agt_01...",
    body=body,
)

# Send the four headers with the request. If the site uses SiftFy,
# forward them to siteverify as agent_signature:
agent_signature = agent_signature_fields(
    headers, method="POST", authority="forms.example.com", path="/contact"
)

The TypeScript SDK exposes the same pair. The signed headers are Signature, Signature-Input, Signature-Agent, and Content-Digest.

For MCP, the client identity is just a URL:

https://api.agentdraft.io/v1/agents/{agent_id}/oauth-client.json

Use it as the OAuth client_id. If the authorization server supports Client ID Metadata Documents, it fetches the document and validates the private_key_jwt assertion against the agent's signing directory. No dynamic registration.

What identity does not do

It does not make an agent trustworthy. It makes an agent identifiable. That is a different claim, and the difference is the point.

A verified signature can skip a bot block only when the site's action policy opts in. Denylist, reputation, velocity, and the content classifier still run. A compromised agent with a valid key can still send spam; the site will see exactly which agent did it and can revoke the key or deny the agent.

We also kept the private key off the server. AgentDraft generates the pair, returns the private half once, and stores only the public half. Rotating the key replaces the public JWK immediately and invalidates the old kid. There is no hosted signer and no second key hierarchy to operate.

How to get it

Signed receipts, delegation, Web Bot Auth keys, and MCP CIMD are included on Team ($25/mo) and above. The public JWKS and the signed A2A 1.0 card are available on every tier.

If you are building an agent that submits forms, calls a customer's API, or connects to a remote MCP server, the question is no longer whether the site can tell you are a bot. It can. The question is whether it can tell which bot you are.


Field Notes

Liked this? One short note every other Tuesday.

Conflict-engine post-mortems, new endpoints, the rare opinion. No tracking pixels.

Double opt-in — you'll get a confirmation link. Unsubscribe in one click.

Read this beat regularly? Add AgentDraft as a preferred source on Google →