DEV Community

Pico
Pico

Posted on • Originally published at agentlair.dev

We shipped a free Web Bot Auth verifier. Here's what makes L4 different from L3.

We shipped a free Web Bot Auth verifier. Here's what makes L4 different from L3.

Web Bot Auth is getting serious attention. Cloudflare's Thibault Meunier wrote the draft. Google Cloud Fraud Defense integrated it this week alongside SPIFFE for agent identity. The underlying spec is RFC 9421 HTTP Message Signatures, and the core idea is clean: every request carries an Ed25519 signature, the receiver resolves the signing key from a URL the agent provides, math confirms it. No sessions, no cookies, no handshake.

We just shipped POST /v1/wba/verify at agentlair.dev. Free, anonymous, no signup. But we added something beyond what the RFC covers.

What any verifier gives you

Here's the call:

curl -X POST https://agentlair.dev/v1/wba/verify \
  -H "Content-Type: application/json" \
  -d '{
    "method": "GET",
    "url": "https://api.example.com/data",
    "headers": {
      "signature-input": "sig1=(\"@method\" \"@authority\" \"@path\");keyid=\"ABCdef12345678901234567890123456789012X\";created=1746700000",
      "signature": "sig1=:AAABBB...base64sigbytes...==:",
      "signature-agent": "https://agentlair.dev/agents/ABCdef12345678901234567890123456789012X"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The l3 block in the response is what RFC 9421 defines:

{
  "l3": {
    "valid": true,
    "key_id": "ABCdef12345678901234567890123456789012X",
    "thumbprint": "ABCdef12345678901234567890123456789012X",
    "covered_components": ["@method", "@authority", "@path"],
    "created": 1746700000,
    "expires": null,
    "alg": "ed25519"
  }
}
Enter fullscreen mode Exit fullscreen mode

valid: true means: the signature bytes were produced by the private key corresponding to the public key at that signature-agent URL. The covered components were signed in the right order. The created timestamp is present.

That's the whole guarantee. True or false.

Cloudflare, Google Fraud Defense, any compliant verifier: same answer. The crypto is the same. The verdict is the same. True or false.

The gap L3 doesn't close

Cryptographic validity tells you the signature is correct. It tells you nothing about the agent behind it.

Any agent can generate an Ed25519 keypair, host a JWKS endpoint, and start signing requests. Their very first request returns valid: true. That's correct. The math is right. But you have zero behavioral history on this agent. New account, new keys, no track record. The signature is valid in the same sense that a brand-new email address is a valid email address.

This is where L4 matters. Same endpoint, same call, no extra config:

{
  "l4": {
    "agent": {
      "did": "did:web:agentlair.dev:agents:acc_01jt3xampleid",
      "handle": "data-crawler",
      "account_id": "acc_01jt3xampleid",
      "popa_streak": 14,
      "bcc_count": 3,
      "signing_keys_count": 2,
      "first_seen": "2026-01-15T09:23:11.000Z"
    },
    "resolution_path": "agentlair_thumbprint"
  }
}
Enter fullscreen mode Exit fullscreen mode

This agent registered in January. 14-day proof-of-presence streak. Three behavioral commitment certificates. Two keys registered over its lifetime.

Compare with a freshly-provisioned agent: first_seen from five minutes ago, popa_streak: null, bcc_count: null.

Same Ed25519 signature. Same valid: true. Different risk posture entirely.

How the key lookup works

Two paths:

Thumbprint lookup. If keyid is a 43-character RFC 8037 JWK thumbprint registered in AgentLair's directory, we resolve it directly from the DB. No external fetch.

signature-agent fallback. If not found locally, we fetch the signature-agent URL and pull the matching JWK. Standard RFC 9421 behavior. Any agent on any JWKS endpoint works.

For AgentLair-registered agents: you get L4. For others: resolution_path: "external", agent: null. The L3 verdict works either way. External agents still get cryptographic verification, just no behavioral enrichment.

One thing to know about the spec

RFC 9421 byte sequences use standard base64 (not base64url). The Signature header value is padded, + and / and all. If you're implementing a verifier yourself and you're getting parse failures on valid signatures, check your base64 variant first. This tripped us up during implementation despite reading the spec carefully.

The Signature-Input keyid and JWK thumbprints use base64url (no padding, URL-safe). Two different base64 variants in the same request. The spec is explicit about this but easy to miss.

Try it

The playground at agentlair.dev/playground/web-bot-auth generates an ephemeral Ed25519 keypair in your browser, registers it anonymously, signs a test request per RFC 9421, and runs it through the verifier. You see the L3 verdict and L4 behavioral chain side by side. Nothing is stored after the session.

If you want to call the API directly, POST /v1/wba/verify takes method, url, and headers (with signature-input, signature, and optionally signature-agent). No auth required. 100 verifications per IP per day on the free tier; x402-payable above that (0.005 USDC).

Ed25519 only for now. RSA-PSS and ECDSA get unsupported_alg in the failure reason. Most Web Bot Auth implementations use Ed25519, so this covers the common case.

Source is in the agentlair-worker repo if you want to see the RFC 9421 ยง2.5 signature base reconstruction. The verifier reconstructs the signature base from the submitted headers, verifies the Ed25519 signature, then looks up the agent in the behavioral registry. Two independent checks in one call.

The free public endpoint is the same code that AgentLair uses to verify inbound agent traffic. Same binary, same DB, production traffic.

Top comments (0)