# AgentBouncer Documentation

> Verify signed AI-agent requests and apply project-specific access policies.

Documentation version: 0.1.1

Canonical portal: https://agentbouncer.io/en/docs

Canonical Markdown: https://agentbouncer.io/en/docs.md

## Documentation index

- [AgentBouncer overview](#overview): Verify signed AI-agent requests and apply project-specific access policies.
- [Quickstart](#quickstart): Create a project, copy an API key, and protect an endpoint.
- [Core concepts](#core-concepts): Understand projects, API keys, signing keys, providers, and policies.
- [Create and manage projects](#projects): Configure the trust boundary for an application or MCP server.
- [Project API keys](#api-keys): Authenticate your server when calling the AgentBouncer API.
- [REST API integration](#rest-api): Call AgentBouncer from any language or backend framework.
- [JavaScript and TypeScript SDK](#sdk): Verify signed requests and apply the final AgentBouncer policy decision.
- [Next.js App Router](#nextjs): Protect a Next.js route while preserving the external request URL.
- [Next.js without the SDK](#manual-nextjs): Send a minimal verification payload directly to the REST API.
- [Express integration](#express): Protect Express routes using the REST verification endpoint.
- [Protect MCP tools](#mcp): Apply AgentBouncer decisions before executing MCP operations.
- [Project signing keys](#project-signing-keys): Create identities for agents owned directly by your project.
- [Register an agent provider](#providers): Publish a provider identity and public-key directory.
- [Policy modes](#policies): Choose how verified identity and trust data affect access.
- [Custom policies](#custom-policies): Create fine-grained allow and deny rules.
- [Verification response](#response): Understand the fields returned by the verification API.
- [Reasons and errors](#errors): Common verification and policy decision reasons.
- [Verification events](#events): Monitor real traffic and investigate authorization decisions.
- [Security guidance](#security): Deploy AgentBouncer without leaking secrets or weakening verification.
- [Troubleshooting](#troubleshooting): Diagnose signature, URL, key-directory, and policy problems.
- [Production checklist](#production-checklist): Review the integration before enabling blocking policies.

<a id="overview"></a>

## AgentBouncer overview

Verify signed AI-agent requests and apply project-specific access policies.

AgentBouncer verifies HTTP Message Signatures sent by AI agents, resolves the signer public key, evaluates your project policy, and returns a final access decision.

> **The most important integration rule**
>
> Use allowed as the final authorization decision. verified reports the cryptographic result, while allowed reports the effective project-policy decision.

| verified | allowed | Meaning |
| --- | --- | --- |
| true | true | The signature is valid and the request is allowed. |
| true | false | The signature is valid, but the project policy denied access. |
| false | true | The request was not verified but was allowed by MONITOR_ONLY or another permissive setting. |
| false | false | The request failed verification and access was denied. |

- Verify RFC 9421 HTTP Message Signatures.
- Resolve provider and project-owned public keys.
- Apply trust tiers, trust scores, actions, tools, and custom rules.
- Protect MCP tools, APIs, checkout operations, and autonomous workflows.
- Record verification events for analytics and incident investigation.

<a id="quickstart"></a>

## Quickstart

Create a project, copy an API key, and protect an endpoint.

1. **Create a project** — Open the AgentBouncer dashboard, create a project, and enter the origin or host of the service you want to protect.
2. **Start in Monitor only** — Use MONITOR_ONLY during the first integration phase. Requests continue to run while AgentBouncer records what would have been blocked.
3. **Copy the project API key** — The API key authenticates your backend when it calls the AgentBouncer verification API. It is not an agent signing key.
4. **Send the original request URL and signature headers** — The URL must match the URL that the agent signed. Preserve Signature, Signature-Input, and Signature-Agent.
5. **Authorize with allowed** — Reject the request only when allowed is false.

### Verify a request with REST

```bash

curl -X POST https://agentbouncer.io/api/v1/verify \
        -H "Authorization: Bearer ab_live_YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "url": "https://merchant.example/api/mcp/weather",
          "method": "POST",
          "headers": {
            "signature": "sig1=:BASE64_SIGNATURE:",
            "signature-input": "sig1=(\"@authority\" \"signature-agent\");created=1760000000;expires=1760000300;keyid=\"KEY_ID\";tag=\"web-bot-auth\"",
            "signature-agent": "\"https://agent.example\"",
            "content-type": "application/json"
          },
          "expectedTag": "web-bot-auth",
          "action": "tools:call",
          "tool": "weather"
        }'

```

### Apply the final decision

```typescript

const verification = await verifyWithAgentBouncer(request);
      
      if (!verification.allowed) {
        return Response.json(
          {
            ok: false,
            error: "agent_access_denied",
            verification,
          },
          {
            status: verification.verified ? 403 : 401,
          }
        );
      }
      
      // Continue to the protected operation.
      return runProtectedTool();

```

<a id="core-concepts"></a>

## Core concepts

Understand projects, API keys, signing keys, providers, and policies.

| Concept | Purpose |
| --- | --- |
| Project | A protected application, API, MCP server, origin, or trust boundary. |
| Project API key | Authenticates your backend when calling /api/v1/verify. |
| Project signing key | Identifies and signs requests from one of your own agents. |
| Provider | A registered external AI-agent provider with a signature identity and public-key directory. |
| Provider key | A public key discovered in the provider HTTP Message Signatures directory. |
| Verification event | An immutable record of the cryptographic result, policy decision, signer, risk, and request metadata. |
| Policy | Rules that convert verification and trust information into allowed=true or allowed=false. |

> **API keys and signing keys are different**
>
> The project API key authorizes calls from your server to AgentBouncer. A signing key is held by an agent and signs the original HTTP request.

<a id="projects"></a>

## Create and manage projects

Configure the trust boundary for an application or MCP server.

1. **Create the project** — Choose a descriptive name such as Production MCP, Checkout API, or Internal automation.
2. **Set the origin** — Use the public origin that agents call. Reverse-proxy and internal deployment URLs should not replace the externally signed URL.
3. **Choose a policy** — Start with MONITOR_ONLY, review verification events, and then move to BLOCK_UNKNOWN or a stricter policy.
4. **Create project signing keys if needed** — Use project keys for internal agents, development clients, and integrations that are owned by the project.
5. **Review events** — Inspect verified, allowed, reason, signer type, provider, project key, action, tool, risk, and policy details.

> **Recommended project boundaries**
>
> Use separate projects for production and development. Consider separate projects when applications have materially different trust or authorization requirements.

<a id="api-keys"></a>

## Project API keys

Authenticate your server when calling the AgentBouncer API.

Project API keys use the ab_live_ prefix and must only be stored on your backend. Never expose them in browser JavaScript, public repositories, or client-side environment variables.

### .env.local

```bash

AGENTBOUNCER_API_KEY=ab_live_YOUR_API_KEY
              AGENTBOUNCER_PUBLIC_ORIGIN=https://merchant.example
              AGENTBOUNCER_VERIFY_URL=https://agentbouncer.io/api/v1/verify

```

- Store production keys in your hosting provider secret manager.
- Rotate a key if it was logged or committed accidentally.
- Do not send the API key to the incoming AI agent.
- Do not confuse the AgentBouncer API key with a private signing JWK.

<a id="rest-api"></a>

## REST API integration

Call AgentBouncer from any language or backend framework.

### POST /api/v1/verify

```http

POST /api/v1/verify HTTP/1.1
      Host: agentbouncer.io
      Authorization: Bearer ab_live_YOUR_API_KEY
      Content-Type: application/json
      
      {
        "url": "https://merchant.example/api/mcp/weather",
        "method": "POST",
        "headers": {
          "signature": "sig1=:...:",
          "signature-input": "sig1=(...);created=...;expires=...;keyid=\"...\";tag=\"web-bot-auth\"",
          "signature-agent": "\"https://agent.example\""
        },
        "expectedTag": "web-bot-auth",
        "action": "tools:call",
        "tool": "weather",
        "userAgent": "ExampleAgent/1.0"
      }

```

| Field | Required | Description |
| --- | --- | --- |
| url | Yes | The exact external URL whose request was signed. |
| method | Recommended | The original HTTP method. Defaults to GET. |
| headers | Recommended | Original request headers used for signature verification. Signature fields may alternatively be supplied through signature, signatureInput, and signatureAgent. |
| signature | Conditional | The Signature header value. Required if it is not included in headers. |
| signatureInput | Conditional | The Signature-Input header value. Required if it is not included in headers. |
| signatureAgent | No | The Signature-Agent structured-field value. It may alternatively be included in headers. |
| expectedTag | No | The intent tag expected by the protected endpoint. |
| action | No | A semantic operation such as tools:call, products:read, checkout, or orders:write. |
| tool | No | The MCP tool or application capability being accessed. |
| userAgent | No | The original requester User-Agent for analytics. |

> **Preserve the signed URL**
>
> Do not replace the public target URL with the AgentBouncer verification endpoint URL, an internal container URL, or a reverse-proxy URL.

<a id="sdk"></a>

## JavaScript and TypeScript SDK

Verify signed requests and apply the final AgentBouncer policy decision.

### Install

```bash

npm install @agentbouncer/sdk

```

### Create a client

```typescript

import {
            createAgentBouncer,
          } from "@agentbouncer/sdk";
          
          export const agentBouncer =
            createAgentBouncer({
              apiKey:
                process.env.AGENTBOUNCER_API_KEY!,
              publicOrigin:
                process.env.AGENTBOUNCER_PUBLIC_ORIGIN,
              verifyUrl:
                process.env.AGENTBOUNCER_VERIFY_URL,
            });

```

> **Class constructor**
>
> The SDK also exports AgentBouncer. new AgentBouncer(options) and createAgentBouncer(options) create the same client.

> **Configure the public origin behind a proxy**
>
> If request.url contains an internal deployment or reverse-proxy URL, configure publicOrigin with the externally visible origin that agents sign. You can alternatively pass targetUrl to each verify() call.

### verify()

```typescript

const verification =
            await agentBouncer.verify({
              request,
              expectedTag: "web-bot-auth",
              action: "tools:call",
              tool: "weather",
            });
          
          if (!verification.allowed) {
            return new Response(
              "Agent access denied",
              {
                status: verification.verified
                  ? 403
                  : 401,
              }
            );
          }
          
          // Continue to the protected operation.

```

### require()

```typescript

import {
            AgentBouncerDeniedError,
            AgentBouncerError,
          } from "@agentbouncer/sdk";
          
          try {
            const verification =
              await agentBouncer.require({
                request,
                expectedTag: "web-bot-auth",
                action: "tools:call",
                tool: "weather",
              });
          
            return runWeatherTool({
              agent: verification.agent,
            });
          } catch (error) {
            if (
              error instanceof
              AgentBouncerDeniedError
            ) {
              return new Response(
                "Agent access denied",
                {
                  status:
                    error.verification.verified
                      ? 403
                      : 401,
                }
              );
            }
          
            if (
              error instanceof AgentBouncerError
            ) {
              return new Response(
                "Agent verification unavailable",
                {
                  status: 503,
                }
              );
            }
          
            throw error;
          }

```

> **SDK method semantics**
>
> verify() returns the complete verification result. require() throws AgentBouncerDeniedError when allowed is false. Network, timeout, API authentication, and malformed-response failures throw AgentBouncerError.

<a id="nextjs"></a>

## Next.js App Router

Protect a Next.js route while preserving the external request URL.

### src/lib/agentbouncer-client.ts

```typescript

import {
            createAgentBouncer,
          } from "@agentbouncer/sdk";
          
          const apiKey =
            process.env.AGENTBOUNCER_API_KEY;
          
          const publicOrigin =
            process.env.AGENTBOUNCER_PUBLIC_ORIGIN;
          
          if (!apiKey) {
            throw new Error(
              "Missing AGENTBOUNCER_API_KEY"
            );
          }
          
          if (!publicOrigin) {
            throw new Error(
              "Missing AGENTBOUNCER_PUBLIC_ORIGIN"
            );
          }
          
          export const agentBouncer =
            createAgentBouncer({
              apiKey,
              publicOrigin,
              verifyUrl:
                process.env.AGENTBOUNCER_VERIFY_URL,
            });

```

### src/app/api/mcp/weather/route.ts

```typescript

import {
            AgentBouncerDeniedError,
            AgentBouncerError,
          } from "@agentbouncer/sdk";
          import {
            NextRequest,
            NextResponse,
          } from "next/server";
          import {
            agentBouncer,
          } from "@/lib/agentbouncer-client";
          
          export const runtime = "nodejs";
          export const dynamic = "force-dynamic";
          
          export async function POST(
            req: NextRequest
          ) {
            const requestId = crypto.randomUUID();
          
            try {
              const verification =
                await agentBouncer.require({
                  request: req,
                  expectedTag: "web-bot-auth",
                  action: "tools:call",
                  tool: "weather",
                });
          
              const body = await req
                .json()
                .catch(() => ({}));
          
              const city =
                typeof body.city === "string"
                  ? body.city
                  : "Berlin";
          
              return NextResponse.json({
                ok: true,
                city,
                weather: {
                  temperatureC: 21,
                  conditions: "Clear",
                },
                agent: verification.agent,
                requestId,
              });
            } catch (error) {
              if (
                error instanceof
                AgentBouncerDeniedError
              ) {
                return NextResponse.json(
                  {
                    ok: false,
                    error: "mcp_access_denied",
                    reason:
                      error.verification.reason,
                    requestId,
                  },
                  {
                    status:
                      error.verification.verified
                        ? 403
                        : 401,
                  }
                );
              }
          
              console.error(
                "AgentBouncer verification failed",
                {
                  requestId,
                  error,
                }
              );
          
              return NextResponse.json(
                {
                  ok: false,
                  error:
                    "agentbouncer_unavailable",
                  message:
                    error instanceof
                    AgentBouncerError
                      ? error.message
                      : "Unable to verify the request.",
                  requestId,
                },
                {
                  status: 503,
                }
              );
            }
          }

```

> **Do not use the old gate**
>
> Do not use if (!verification.verified || !verification.allowed). That incorrectly blocks requests allowed by MONITOR_ONLY or allowUnsigned.

<a id="manual-nextjs"></a>

## Next.js without the SDK

Send a minimal verification payload directly to the REST API.

### Minimal header collection

```typescript

import type {
                NextRequest,
              } from "next/server";
              import {
                pickVerificationHeaders,
              } from "./pick-verification-headers";
              
              function resolvePublicTargetUrl(
                req: NextRequest
              ) {
                const publicOrigin =
                  process.env
                    .AGENTBOUNCER_PUBLIC_ORIGIN;
              
                if (!publicOrigin) {
                  return req.nextUrl.toString();
                }
              
                return new URL(
                  `${req.nextUrl.pathname}${req.nextUrl.search}`,
                  publicOrigin
                ).toString();
              }
              
              export async function verifyAgentRequest(
                req: NextRequest,
                options?: {
                  expectedTag?: string;
                  action?: string;
                  tool?: string;
                }
              ) {
                const targetUrl =
                  resolvePublicTargetUrl(req);
              
                const response = await fetch(
                  process.env.AGENTBOUNCER_VERIFY_URL ??
                    "https://agentbouncer.io/api/v1/verify",
                  {
                    method: "POST",
                    headers: {
                      Authorization:
                        `Bearer ${process.env.AGENTBOUNCER_API_KEY}`,
                      "Content-Type": "application/json",
                      Accept: "application/json",
                    },
                    body: JSON.stringify({
                      url: targetUrl,
                      method: req.method,
                      headers:
                        pickVerificationHeaders(req),
                      expectedTag:
                        options?.expectedTag ?? null,
                      action:
                        options?.action ?? null,
                      tool:
                        options?.tool ?? null,
                      userAgent:
                        req.headers.get("user-agent"),
                    }),
                    cache: "no-store",
                  }
                );
              
                const result = await response
                  .json()
                  .catch(() => null);
              
                if (!response.ok) {
                  throw new Error(
                    result?.detail ||
                      result?.reason ||
                      `AgentBouncer returned HTTP ${response.status}.`
                  );
                }
              
                if (
                  !result ||
                  typeof result.verified !== "boolean" ||
                  typeof result.allowed !== "boolean"
                ) {
                  throw new Error(
                    "AgentBouncer returned an invalid response."
                  );
                }
              
                return result;
              }

```

### Call the verification endpoint

```typescript

import type { NextRequest } from "next/server";
      import { pickVerificationHeaders } from "./pick-verification-headers";
      
      export async function verifyAgentRequest(
        req: NextRequest,
        options?: {
          expectedTag?: string;
          action?: string;
          tool?: string;
        }
      ) {
        const targetUrl = req.nextUrl.toString();
      
        const response = await fetch(
          process.env.AGENTBOUNCER_VERIFY_URL ??
            "https://agentbouncer.io/api/v1/verify",
          {
            method: "POST",
            headers: {
              Authorization:
                `Bearer ${process.env.AGENTBOUNCER_API_KEY}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              url: targetUrl,
              method: req.method,
              headers: pickVerificationHeaders(req),
              expectedTag: options?.expectedTag ?? null,
              action: options?.action ?? null,
              tool: options?.tool ?? null,
              userAgent: req.headers.get("user-agent"),
            }),
            cache: "no-store",
          }
        );
      
        const result = await response.json();
      
        if (!response.ok && response.status === 401) {
          throw new Error(
            "AgentBouncer API authentication failed."
          );
        }
      
        return result;
      }

```

<a id="express"></a>

## Express integration

Protect Express routes using the REST verification endpoint.

```typescript

import type {
        NextFunction,
        Request,
        Response,
      } from "express";
      
      export async function requireVerifiedAgent(
        req: Request,
        res: Response,
        next: NextFunction
      ) {
        try {
          const protocol =
            req.headers["x-forwarded-proto"] || req.protocol;
      
          const host =
            req.headers["x-forwarded-host"] ||
            req.headers.host;
      
          const targetUrl =
            `${protocol}://${host}${req.originalUrl}`;
      
          const verifyResponse = await fetch(
            "https://agentbouncer.io/api/v1/verify",
            {
              method: "POST",
              headers: {
                Authorization:
                  `Bearer ${process.env.AGENTBOUNCER_API_KEY}`,
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                url: targetUrl,
                method: req.method,
                headers: {
                  signature: req.headers.signature,
                  "signature-input":
                    req.headers["signature-input"],
                  "signature-agent":
                    req.headers["signature-agent"],
                  "content-type":
                    req.headers["content-type"],
                  "content-digest":
                    req.headers["content-digest"],
                },
                expectedTag: "web-bot-auth",
                action: "api:call",
              }),
            }
          );
      
          const verification = await verifyResponse.json();
      
          if (!verification.allowed) {
            res.status(verification.verified ? 403 : 401).json({
              ok: false,
              error: "agent_access_denied",
              verification,
            });
      
            return;
          }
      
          res.locals.agentVerification = verification;
          next();
        } catch (error) {
          next(error);
        }
      }

```

<a id="mcp"></a>

## Protect MCP tools

Apply AgentBouncer decisions before executing MCP operations.

Set action to tools:call and tool to the MCP tool name. This makes verification events and custom policy rules specific to the operation being requested.

### MCP authorization pattern

```typescript

const verification = await agentBouncer.verify({
        request,
        expectedTag: "web-bot-auth",
        action: "tools:call",
        tool: toolName,
      });
      
      if (!verification.allowed) {
        return {
          isError: true,
          content: [
            {
              type: "text",
              text: "This agent is not allowed to call the tool.",
            },
          ],
        };
      }
      
      switch (toolName) {
        case "weather":
          return getWeather(toolArguments);
      
        case "catalog":
          return searchCatalog(toolArguments);
      
        case "checkout":
          return createCheckout(toolArguments);
      
        default:
          throw new Error("Unknown MCP tool");
      }

```

| Operation | Suggested action | Suggested tool |
| --- | --- | --- |
| Read an MCP resource | resources:read | resource name |
| List tools | tools:list | * |
| Call a tool | tools:call | tool name |
| Read products | products:read | catalog |
| Create checkout | checkout:create | checkout |
| Submit an order | orders:create | orders |

<a id="project-signing-keys"></a>

## Project signing keys

Create identities for agents owned directly by your project.

1. **Issue a project key** — Open the project Agent keys tab and create a descriptive key such as Production MCP agent or Internal CRM.
2. **Save the private JWK** — The private key is shown once. Store it in the agent secret manager.
3. **Keep the public identity** — AgentBouncer stores the public JWK, key ID, signature-agent identity, scopes, and policy configuration.
4. **Sign requests** — Use the private JWK to produce Signature and Signature-Input headers for the exact target URL.
5. **Revoke compromised keys** — Delete or revoke a project key immediately if its private JWK is exposed.

### Example environment variables

```bash

AGENT_KEY_ID=YOUR_KEY_ID
      AGENT_SIGNATURE_AGENT=https://agentbouncer.io/project-agents/PROJECT_ID/KEY_ID
      AGENT_PRIVATE_JWK='{"kty":"OKP","crv":"Ed25519","x":"...","d":"..."}'

```

<a id="providers"></a>

## Register an agent provider

Publish a provider identity and public-key directory.

1. **Create the provider** — Choose a stable slug, provider name, website, signature-agent origin, and directory URL.
2. **Publish the public JWKS** — Expose public keys at the configured HTTP Message Signatures directory.
3. **Refresh keys** — Ask AgentBouncer to load and validate the directory.
4. **Verify the provider domain** — Publish the AgentBouncer DNS TXT record or verification file.
5. **Sign production requests** — Use a private key corresponding to an active public key in the directory.

### Example JWKS document

```json

{
        "keys": [
          {
            "kty": "OKP",
            "crv": "Ed25519",
            "kid": "provider-key-2026-01",
            "alg": "EdDSA",
            "use": "sig",
            "x": "PUBLIC_KEY_MATERIAL"
          }
        ]
      }

```

### Recommended public location

```text

https://agent.example/.well-known/http-message-signatures-directory/jwks.json

```

> **Never publish the private JWK**
>
> Only public JWK properties belong in the provider directory. The private d property must remain secret.

<a id="policies"></a>

## Policy modes

Choose how verified identity and trust data affect access.

| Mode | Behavior |
| --- | --- |
| MONITOR_ONLY | Records verification and policy failures but allows the request. |
| BLOCK_UNKNOWN | Blocks unsigned requests and unknown identities. |
| ALLOW_TIER_1_ONLY | Allows only the highest-trust provider tier. |
| ALLOW_TIER_1_AND_2 | Allows trusted Tier 1 and Tier 2 providers. |
| CUSTOM | Evaluates provider, tier, project-key, action, and tool rules. Matching DENY rules take precedence over matching ALLOW rules; otherwise defaultEffect is applied. |

> **Recommended rollout**
>
> Begin with MONITOR_ONLY, inspect real traffic, define actions and tools, test project keys, and only then enable blocking.

<a id="custom-policies"></a>

## Custom policies

Create fine-grained allow and deny rules.

### Example custom policy

```json

{
        "version": 1,
        "defaultEffect": "DENY",
        "rules": [
          {
            "id": "rule_allow_tier_1_read",
            "name": "Allow Tier 1 catalog reads",
            "enabled": true,
            "effect": "ALLOW",
            "subject": {
              "type": "PROVIDER_TIER",
              "tiers": ["TIER_1"]
            },
            "actions": ["products:read"],
            "tools": ["catalog"]
          },
          {
            "id": "rule_deny_external_checkout",
            "name": "Deny external checkout",
            "enabled": true,
            "effect": "DENY",
            "subject": {
              "type": "ANY_PROVIDER"
            },
            "actions": ["checkout:create"],
            "tools": ["checkout"]
          }
        ]
      }

```

- DENY rules take precedence over ALLOW rules.
- An empty actions array matches any action.
- An empty tools array matches any tool.
- The wildcard * matches any action or tool.
- If no rule matches, defaultEffect is applied.
- Use PROJECT_KEY to target selected internal agent keys.

<a id="response"></a>

## Verification response

Understand the fields returned by the verification API.

```json

{
        "verified": true,
        "allowed": true,
        "reason": null,
        "checks": {
          "signature": "valid",
          "timestamp": "valid",
          "replay": "unknown",
          "provider": "known",
          "policy": "matched"
        },
        "agent": {
          "type": "PROJECT_KEY",
          "provider": null,
          "providerSlug": null,
          "projectKey": {
            "id": "key_id",
            "name": "Production MCP agent",
            "scopes": []
          },
          "signatureAgent": "https://agentbouncer.io/project-agents/...",
          "keyid": "..."
        },
        "request": {
          "method": "POST",
          "path": "/api/mcp/weather",
          "tag": "web-bot-auth",
          "expectedTag": "web-bot-auth",
          "action": "tools:call",
          "tool": "weather"
        },
        "risk": {
          "score": 5,
          "level": "low"
        },
        "policy": {
          "mode": "CUSTOM",
          "wouldBlockReason": null,
          "matchedRule": {
            "id": "rule_allow_weather",
            "name": "Allow weather agent",
            "effect": "ALLOW"
          },
          "defaultEffectApplied": false
        }
      }

```

> **Replay detection**
>
> A valid signature does not by itself prove that the request has not been replayed. Until replay storage or nonce validation is enabled, checks.replay is reported as unknown.

| Field | Meaning |
| --- | --- |
| verified | Whether the cryptographic identity verification succeeded. |
| allowed | The final authorization decision after applying project policy. |
| reason | The effective denial reason, or null when access is allowed. |
| checks | Individual signature, timestamp, replay, provider, project-key, and policy results. unknown means that a check was not performed or sufficient information was not available. |
| agent | Resolved provider or project-key identity. |
| risk | Calculated risk score and level. |
| policy | Policy mode, matching rule, and hypothetical block reason. |

<a id="errors"></a>

## Reasons and errors

Common verification and policy decision reasons.

| Reason | Description |
| --- | --- |
| unauthorized | Missing or invalid AgentBouncer project API key. |
| invalid_request | The verification payload is missing a required field. |
| invalid_url | url is not a valid absolute target URL. |
| no_signature | Signature or Signature-Input is missing. |
| no_keyid | Signature-Input does not contain keyid. |
| unknown_keyid | No active project or provider key was found. |
| not_yet_valid | The signature creation time is in the future. |
| expired | The signature has expired. |
| window_too_long | The created-to-expires signature window exceeds the permitted maximum. |
| bad_signature | Cryptographic verification failed. |
| unknown_provider | The key provider is not recognized. |
| provider_blocked | The provider is suspended, banned, fraudulent, or revoked. |
| provider_not_trusted | The project requires a trusted provider. |
| unsupported_tag | The signature intent tag is unsupported. |
| tag_mismatch | The actual signature tag does not match expectedTag. |
| tier_too_low | The provider does not satisfy the required tier. |
| trust_score_too_low | The provider trust score is below the project threshold. |
| project_keys_disabled | Project-owned signing keys are disabled. |
| custom_policy_denied | A matching custom DENY rule rejected the request. |
| custom_policy_no_match | No custom rule matched and the default effect is DENY. |

<a id="events"></a>

## Verification events

Monitor real traffic and investigate authorization decisions.

- Filter events by day, week, month, all time, or custom dates.
- Compare verified requests with allowed requests.
- Use allowed, not verified, when labeling an event as accepted or rejected.
- Inspect provider tier, status, trust score, abuse score, risk, action, and tool.
- Review wouldBlockReason for requests allowed by MONITOR_ONLY.
- Export event data as CSV for offline analysis.

| verified | allowed | Recommended UI label |
| --- | --- | --- |
| true | true | Allowed |
| true | false | Denied by policy |
| false | true | Allowed in monitor mode |
| false | false | Verification failed |

<a id="security"></a>

## Security guidance

Deploy AgentBouncer without leaking secrets or weakening verification.

- Keep AgentBouncer API keys on the server.
- Keep private signing JWKs in a secret manager.
- Publish only public JWK properties.
- Use HTTPS for target URLs, signature-agent identities, and key directories.
- Preserve the original externally visible URL and HTTP method.
- Do not forward Authorization, Cookie, Set-Cookie, x-api-key, or proxy credentials in verification payloads.
- Exclude reverse-proxy and observability headers unless they are intentionally signed.
- Use short signature validity windows.
- Revoke compromised project and provider keys.
- Start policy changes in MONITOR_ONLY before enforcing them.

> **Request body integrity**
>
> AgentBouncer verifies the HTTP Message Signature over the covered request components supplied to the verification API. Applications that rely on a signed Content-Digest must also ensure that the digest matches the body received by the protected endpoint.

<a id="troubleshooting"></a>

## Troubleshooting

Diagnose signature, URL, key-directory, and policy problems.

| Problem | What to check |
| --- | --- |
| Invalid Signature header | Ensure Signature, Signature-Input, and Signature-Agent are each included exactly once. |
| bad_signature | Compare the signed URL, method, authority, covered components, public key, and content digest. |
| unknown_keyid | Confirm the project key is active or refresh the provider public-key directory. |
| tag_mismatch | Compare expectedTag with the tag in Signature-Input. |
| custom_policy_no_match | Check subject type, project key ID, provider tier, action, tool, and defaultEffect. |
| MONITOR_ONLY still blocks | Make sure your application checks only verification.allowed. |
| Wrong @authority | Use the external host that the agent signed, not an internal deployment host. |

### Correct authorization gate

```typescript

if (!verification.allowed) {
        return Response.json(
          {
            ok: false,
            error: "agent_access_denied",
            verification,
          },
          {
            status: verification.verified ? 403 : 401,
          }
        );
      }

```

### Incorrect authorization gate

```typescript

// Incorrect: blocks MONITOR_ONLY requests.
      if (!verification.verified || !verification.allowed) {
        return new Response("Denied", {
          status: 403,
        });
      }

```

<a id="production-checklist"></a>

## Production checklist

Review the integration before enabling blocking policies.

- The AgentBouncer API key is stored only on the server.
- The production project uses the correct public origin.
- The application sends the exact signed target URL.
- Signature headers are forwarded exactly once.
- Sensitive and infrastructure headers are excluded.
- The authorization gate checks allowed.
- Actions and MCP tool names are populated.
- MONITOR_ONLY traffic has been reviewed.
- Known providers and project keys resolve correctly.
- Custom policies have an intentional default effect.
- Private signing keys are stored in a secret manager.
- Key revocation and rotation procedures are documented.
- Verification event exports have been tested.
