# How to verify which AI agent made a web request

> When an AI agent calls your site or API, how do you tell which agent it is, who runs it, and that the request arrived unchanged?

## The short answer

A website tells which AI agent made a request by checking the request's signature: the agent signs it with HTTP Message Signatures (RFC 9421) in Web Bot Auth's profile, and names a key directory where anyone can fetch the public key. Pidgeon signs in that same profile and adds the agent's address, so `POST /v1/verify` with the request returns not only that the signature holds but which agent sent it and which domain operates it. A site that already checks Web Bot Auth signatures needs nothing new to read a Pidgeon agent's.

## Why a User-Agent string is not an answer

An agent can name itself in its User-Agent header, and a site can look at the IP range a request came from. Neither proves anything. Anyone can send `User-Agent: SomeAgent/1.0`, and an IP range says which network the request left from, not which of the agents on that network made it.

A signature is different in kind. The agent holds a private key, the site fetches the public half from a place the agent's domain controls, and a request whose covered parts changed by one byte fails. The question moves from "what does this request say about itself" to "which key signed it, and who publishes that key".

## What a signed request carries

Web Bot Auth defines how an automated client signs: an Ed25519 key, a `keyid` that is the key's JWK thumbprint, the tag `web-bot-auth`, and a `Signature-Agent` header naming where the key directory lives. Pidgeon's signer follows that profile and covers more of the request, so a signature holds for this one request and not for any request to the same host:

- `@method`, `@authority`, `@path`, and `@query` when there is one.
- `Signature-Agent`, which is `https://post.acme.com` for an agent on acme.com.
- `From`, the agent's address, `support-agent@acme.com`. This is the part Post adds: one domain's key can sign for many agents, and `From` says which one.
- `Content-Digest`, a SHA-256 of the body, when there is a body.

Each signature is good for five minutes. The key directory sits at `https://post.acme.com/.well-known/http-message-signatures-directory` as a JSON Web Key Set, and lists the same keys the domain uses to sign its agents' email. One key, published once, signs the agent's mail and its HTTP requests.

## Checking a request with Pidgeon

Send the request as you received it: the method, the full URL, the headers and the body. No key is needed.

```sh
curl -X POST https://pidgeon.ai/api/v1/verify \
  -H 'content-type: application/json' \
  -d '{
    "request": {
      "method": "POST",
      "url": "https://api.yourshop.com/v1/orders",
      "headers": {
        "signature-agent": "\"https://post.acme.com\"",
        "from": "support-agent@acme.com",
        "content-digest": "sha-256=:…:",
        "signature-input": "sig1=(\"@method\" \"@authority\" \"@path\" \"signature-agent\" \"from\" \"content-digest\");created=…;expires=…;keyid=\"…\";alg=\"ed25519\";nonce=\"…\";tag=\"web-bot-auth\"",
        "signature": "sig1=:…:"
      },
      "body": "{\"sku\":\"A-1\",\"qty\":1}"
    }
  }'
```

Pidgeon finds the key by its `keyid` among the domains proved with `pidgeon id`, or, for any other signer, in the directory its `Signature-Agent` names. The answer is the same shape as for an email: a `verdict` to act on, `sender.address` when the covered `From` is at the signing domain, `operator.domain`, `body` (`intact` when the digest matched), and `signatures.request` with the `keyid`. When the agent's domain was proved with Pidgeon, the answer also carries its agent card: `kind` (`agent` or `service`), who it acts for, and its standing.

The same check is the `request` field of the MCP tool `verify_sender`, for an agent that is itself receiving calls.

## Signing as the agent

`pidgeon id` gives an agent on your own domain an identity with no account. It makes an Ed25519 key on your machine, which never leaves it, and asks you to add one TXT record and one CNAME:

```sh
npm install -g @pidgeon-ai/cli
pidgeon id init acme.com
pidgeon id issue support-agent
```

The TXT record under `_post` proves the domain is yours and names the key. The `post` CNAME makes `post.acme.com` serve the key directory and the agent's card to anyone.

The library that signs every request, `@pidgeon-ai/id`, wraps `fetch`: `signedFetch(identity, { agent: 'support-agent@acme.com' })` returns a `fetch` that adds `Signature-Agent`, `From`, `Content-Digest`, `Signature-Input` and `Signature` to each call. Because a call to an MCP server over Streamable HTTP is an HTTP request, the same `fetch` handed to an MCP client transport signs every tool call too. The library is Apache-2.0 and coming soon to npm; the `pidgeon id` commands are in the CLI today.

## Pidgeon and Web Bot Auth

Pidgeon does not replace Web Bot Auth. It is one more way to find out who holds the key. A request a Pidgeon agent signs is a Web Bot Auth request: same profile, same key directory, same `keyid`. A site that checks those signatures already reads it unchanged.

What Pidgeon adds sits beside the signature, not inside it:

- **An address.** Web Bot Auth names the operator behind a key. The covered `From` names one agent among many under that key.
- **A card.** From the address, anyone can fetch a signed card saying whether it is an agent or a service, who it acts for, which domain operates it and whether that domain was proved.
- **One identity across channels.** The key that signs the request also signs the agent's email, so the agent a site sees is the agent a support desk sees in its inbox.

## Limits

- **A signature says who, not what is allowed.** `verified` means the domain's key signed this request and nothing covered changed. Whether the agent may place the order, read the record or call the tool is your site's decision, made by your own authorisation, as it is today.
- **Coverage matters.** A signature over `@authority` alone verifies for any request to your host. Pidgeon's answer then says `body: not_checked`; insist on the components you need.
- **A directory that does not answer** gives `unsigned` with the warning `envelope_unverifiable`, rather than a guess.
- **An address in a Pidgeon account** signs its mail with a key Pidgeon holds for its domain, so it cannot sign HTTP requests this way. Request signing is for domains whose operator holds the key, through `pidgeon id`.
- **Sending the request to Pidgeon** means sending us its headers and body. Nothing sent is stored or logged, and each IP can check 300 an hour.

## Start

Read [checking a message](https://pidgeon.ai/docs/verifying.md) for the verdicts, which are the same for a request, and [agents](https://pidgeon.ai/docs/agents.md) for what a card says. To give your own agents keys, start at the [quickstart](https://pidgeon.ai/docs/quickstart.md). The draft that defines all of it is on [the protocol](https://pidgeon.ai/protocol) page.

## Questions

### How does a website verify an AI agent's request?

The agent signs the request with HTTP Message Signatures (RFC 9421) in Web Bot Auth's profile and names a key directory in Signature-Agent. The site fetches the key and checks the signature, or sends the request to Pidgeon's POST /v1/verify as request.

### Is Pidgeon an alternative to Web Bot Auth?

No. A Pidgeon agent's request is a Web Bot Auth request, with the same profile, key directory and keyid. Pidgeon adds the agent's address and a signed card saying who runs it and who it acts for.

### Does it work for MCP calls?

Yes. A call to an MCP server over Streamable HTTP is an HTTP request, so it is signed and checked the same way.

### Does a verified request mean the agent may do what it asks?

No. It means the domain's key signed the request and nothing covered changed. What the agent may do is decided by your own authorisation.
