> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# Agent signatures \[Verify signed agent requests]

Agent signatures use [RFC 9421 HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421) to bind an agent identity to an HTTP request. `mppx` supports Ed25519 and RSA-PSS SHA-512 signatures through two profiles:

| Profile | Trust source | Signed request data | Recommended use |
|---|---|---|---|
| [**Web Bot Auth**](/extensions/identity/agent-signatures#use-web-bot-auth) | Public key associated with a trusted HTTPS directory | Authority and `Signature-Agent` | General bot and agent identification |
| [**Trusted Agent Protocol**](/extensions/identity/agent-signatures#use-trusted-agent-protocol) | Public key provisioned by a trusted agent provider | Authority and path | Commerce agents with explicit browse or payment intent |

Each client prover signs the initial request and every automatic MPP retry to the origins in its `scope`, with a fresh nonce and timestamp per attempt. Requests that carry proofs don't follow redirects. An unsigned request or a signature that doesn't verify gets `401`, and a key your resolver doesn't trust gets `403`.

## Use Web Bot Auth

[Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture) identifies automated HTTP clients through a signed `Signature-Agent` header. Use the HTTPS-directory profile when the agent publishes its keys through a directory origin that your server trusts.

Configure the prover on the MPP client. `keyId` must be the RFC 7638 SHA-256 thumbprint of the public key registered for the bot, and `scope` lists the origins that receive the signature.

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import * as WebBotAuth from 'mppx/extensions/identity/web-bot-auth'
import type { Account } from 'viem'

declare const account: Account
declare const botPrivateKey: CryptoKey

const botIdentity = {
  keyId: 'poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U',
  signatureAgent: 'https://bot.example',
} as const

const client = Mppx.create({
  // [!code hl:start]
  identity: [
    WebBotAuth.Client.prover({
      key: botPrivateKey,
      keyId: botIdentity.keyId,
      scope: ['https://api.example.com'],
      signatureAgent: botIdentity.signatureAgent,
    }),
  ],
  // [!code hl:end]
  methods: [tempo({ account })],
  polyfill: false,
})

const response = await client.fetch('https://api.example.com/resource')
console.log(response.status)
// @log: 200
```

Create the verifier on the MPP server and require it on each protected route. The resolver receives the signed directory origin and key ID. Apply your trust policy before you return a key.

```ts twoslash [server.ts]
import * as WebBotAuth from 'mppx/extensions/identity/web-bot-auth'
import { Mppx, Store, tempo } from 'mppx/server'

declare const botPublicKey: CryptoKey

const botIdentity = {
  keyId: 'poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U',
  signatureAgent: 'https://bot.example',
} as const

// [!code hl:start]
const webBotAuth = WebBotAuth.Server.verifier({
  keyResolver({ keyId, signatureAgent }) {
    if (
      keyId !== botIdentity.keyId ||
      signatureAgent !== botIdentity.signatureAgent
    ) return undefined
    return botPublicKey
  },
  maxAge: 60,
  nonceStore: Store.memory(),
})
// [!code hl:end]

const payment = Mppx.create({ methods: [tempo.charge()] })

export const handler = payment.charge({ amount: '0.05', identity: [webBotAuth] }) // [!code hl]
```

:::warning[Trust the directory before fetching]
The verifier doesn't fetch the caller-provided `Signature-Agent` URL. Allowlist the origin before a directory lookup, prevent redirects to untrusted origins, and return an extractable public key so `mppx` can verify its JWK thumbprint.
:::

## Use Trusted Agent Protocol

[Trusted Agent Protocol](https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications/) (TAP) identifies an agent that a merchant or payment network trusts. TAP signatures include an intent: `browse` for discovery and `payment` for payment-related requests.

Use `payment` for an MPP client that accesses paid routes. The prover uses an eight-minute lifetime by default, which is TAP's maximum.

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import * as Tap from 'mppx/extensions/identity/tap'
import type { Account } from 'viem'

declare const account: Account
declare const agentPrivateKey: CryptoKey

const client = Mppx.create({
  // [!code hl:start]
  identity: [
    Tap.Client.prover({
      intent: 'payment',
      key: agentPrivateKey,
      keyId: 'agent-provider-key-1',
      scope: ['https://merchant.example'],
    }),
  ],
  // [!code hl:end]
  methods: [tempo({ account })],
  polyfill: false,
})

const response = await client.fetch('https://merchant.example/checkout')
console.log(response.status)
// @log: 200
```

On the server, resolve `keyId` only from agent providers you trust. The verifier checks the signature, request authority and path, lifetime, intent tag, and nonce. Read the verified key ID and intent with `result.identity?.get(tap)`.

```ts twoslash [server.ts]
import * as Tap from 'mppx/extensions/identity/tap'
import { Mppx, Store, tempo } from 'mppx/server'

declare const trustedAgentKeys: ReadonlyMap<string, CryptoKey>

// [!code hl:start]
const tap = Tap.Server.verifier({
  keyResolver({ keyId }) {
    return trustedAgentKeys.get(keyId)
  },
  nonceStore: Store.memory(),
})
// [!code hl:end]

const payment = Mppx.create({ methods: [tempo.charge()] })
const charge = payment.charge({ amount: '12.50', identity: [tap] }) // [!code hl]

export async function handler(request: Request) {
  const result = await charge(request)
  if (result.status === 402) return result.challenge

  console.log(result.identity?.get(tap)?.intent)
  // @log: payment
  return result.withReceipt(new Response('Accepted'))
}
```

## Store nonces

Web Bot Auth and TAP verifiers use the core `Store.AtomicStore` to retain consumed nonces and block replay. `Store.memory()` is limited to one long-lived server process. In a multi-instance deployment, provide a shared atomic store so every instance claims nonces through their expiration time.

```ts [nonce-store.ts]
import type { Store } from 'mppx'

declare const nonceDatabase: {
  insertIfAbsent(value: { expires: number; key: string }): Promise<boolean>
}
declare const sharedStore: Store.AtomicStore

export const nonceStore = {
  ...sharedStore,
  async tryClaim(key: string, expires: number) {
    return nonceDatabase.insertIfAbsent({ expires, key })
  },
} satisfies Store.AtomicStore
```

Adapt `tryClaim` to your storage client's atomic insert-if-absent operation. It returns `true` when it records a new claim and `false` when an unexpired claim already exists. If your `AtomicStore` omits this optimized method, `mppx` falls back to its atomic `update` operation. See [`Store.tryClaim`](/sdk/typescript/core/Store.tryClaim).

## IETF Specification

[HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421) — Read RFC 9421

[Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture) — Read the IETF draft
