Skip to main content
Web Bot Auth is Cloudflare’s authentication protocol that uses HTTP Message Signatures (RFC 9421) to verify bot identity. Combined with FACT credentials, it provides a complete solution for agent authentication.

Overview

FACT uses a two-layer trust model:
LayerPurposeWhen Applied
CredentialsProves WHO the agent is + safety propertiesAt credential issuance
Web Bot AuthProves each REQUEST comes from that agentOn every HTTP request
┌─────────────────────────────────────────────────────────────┐
│                    FACT TRUST STACK                         │
├─────────────────────────────────────────────────────────────┤
│  Layer 1: AgentCredential (issued by Beltic)               │
│  • Agent identity and version                               │
│  • Safety scores (harmful content, prompt injection, etc.)  │
│  • Capabilities and tools                                   │
│  • Developer KYB tier                                       │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: Web Bot Auth (per-request signatures)            │
│  • Signs HTTP headers with Ed25519                          │
│  • Proves request origin                                    │
│  • Prevents impersonation                                   │
└─────────────────────────────────────────────────────────────┘

Setup

1

Generate keys

Generate an Ed25519 key pair for HTTP signing:
beltic keygen --alg EdDSA --out private.pem --pub public.pem
2

Host key directory

Your agent must host a key directory at a .well-known URL:
https://your-agent.com/.well-known/http-message-signatures-directory
import express from 'express';
import { signDirectoryResponse, generateKeyDirectory } from '@belticlabs/kya';

const app = express();

app.get('/.well-known/http-message-signatures-directory', async (req, res) => {
  const directory = generateKeyDirectory({ publicKeys: [publicJwk] });

  const signed = await signDirectoryResponse(directory, {
    privateKey,
    keyId: thumbprint,
    authority: req.hostname
  });

  res.set(signed.headers);
  res.send(signed.body);
});
3

Include in AgentCredential

When applying for an AgentCredential, include the Web Bot Auth fields:
{
  "agentId": "...",
  "agentName": "PaymentsAgent",
  "httpSigningKeyJwkThumbprint": "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U",
  "keyDirectoryUrl": "https://payments-agent.example.com/.well-known/http-message-signatures-directory",
  ...
}
4

Sign requests

Add signature headers to every HTTP request. Use the Ed25519 key pair you generated (for example from generateWebBotAuthSetup); on Node.js the SDK will accept a KeyObject directly and convert it to a Web Crypto CryptoKey under the hood.
// Use the CLI to sign HTTP requests
// The SDK focuses on verification; use beltic http-sign for signing

// Example: Generate signature headers with CLI, then make request
// beltic http-sign --method POST --url "https://api.coinbase.com/v1/transfers" \
//   --key private.pem --key-directory "https://my-agent.com/.well-known/http-message-signatures-directory" \
//   --body '{"amount": "100.00", "currency": "USD"}' --format curl

// Or use the headers output programmatically:
const { execSync } = require('child_process');
const headers = execSync(`beltic http-sign --method POST --url "https://api.coinbase.com/v1/transfers" --key private.pem --key-directory "https://my-agent.com/.well-known/http-message-signatures-directory" --body '{"amount": "100.00", "currency": "USD"}'`);

// Parse and add headers to your fetch request

Signed Request Format

A signed HTTP request includes three special headers:
POST /v1/transfers HTTP/1.1
Host: api.coinbase.com
Content-Type: application/json
Signature-Agent: "https://my-agent.com/.well-known/http-message-signatures-directory"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent" "content-digest");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";created=1735689600;expires=1735689660;nonce="abc123";tag="web-bot-auth"
Signature: sig1=:jdq0SqOwHdyHr9+r5jw3iYZH6aNGKijYp/EstF4RQTQdi5N5YYKrD+mCT1HA1nZDsi6nJKuHxUi/5Syp3rLWBA==:
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:

{"amount": "100.00", "currency": "USD"}
HeaderPurpose
Signature-AgentURL to your key directory
Signature-InputComponents signed + metadata
SignatureEd25519 signature
Content-DigestHash of request body (when present)

Verifying Requests

Servers can verify incoming signed requests:
import { verifyHttpSignature, fetchAgentCredential } from '@belticlabs/kya';

app.post('/api/action', async (req, res) => {
  // Verify HTTP signature
  const result = await verifyHttpSignature(
    {
      method: req.method,
      url: `https://${req.hostname}${req.path}`,
      headers: req.headers,
      body: req.body
    },
    {
      fetchKeyDirectory: async (url) => {
        const response = await fetch(url);
        return response.json();
      }
    }
  );

  if (!result.valid) {
    return res.status(401).json({ 
      error: 'Invalid signature',
      details: result.errors 
    });
  }

  // Optionally fetch and verify AgentCredential
  const { credential } = await fetchAgentCredential(result.signatureAgentUrl);
  
  if (credential) {
    // Parse and validate the credential
    // Check safety requirements, etc.
  }

  // Process request
  // ...
});

Cloudflare Integration

If your platform uses Cloudflare, verified agents can be automatically recognized:
  1. Submit for Verification: Register your key directory with Cloudflare’s Bot Submission Form
  2. Select Request Signature: Choose “Request Signature” as the verification method
  3. Automatic Recognition: Cloudflare sets cf.bot_management.verified_bot = true
Test your signatures against Cloudflare’s endpoint:
curl https://crawltest.com/cdn-cgi/web-bot-auth \
  -H 'Signature-Agent: "https://my-agent.com/.well-known/http-message-signatures-directory"' \
  -H 'Signature-Input: sig1=...' \
  -H 'Signature: sig1=:...:' 
ResponseMeaning
200 OKKey known, signature valid
401 UnauthorizedKey unknown or invalid signature
400 Bad RequestMalformed headers

Security Best Practices

Key Management
  • Store private keys in HSM/KMS in production
  • Rotate keys annually
  • Never include private key (d parameter) in directory

Short Expiry Windows

Keep signature validity short to prevent replay attacks:
const headers = await signHttpRequest(request, {
  // ...
  expires: Math.floor(Date.now() / 1000) + 60  // 60 seconds
});

Always Include Required Components

At minimum, sign these components:
  • @authority - Target host
  • signature-agent - Key directory URL
For requests with bodies, also include:
  • content-digest - Body hash

Key Binding

Always verify that the signing key is bound to a valid AgentCredential:
// After verifying signature
if (credential.httpSigningKeyJwkThumbprint !== result.keyId) {
  throw new Error('Key not bound to credential');
}