"Which agent are you?" We couldn't answer that about our own agent. Now anyone can check the answer, without asking us.
A website used to ask one question of an unknown visitor: are you human? The question arriving now is different. Which agent are you, who runs you, and who said you could do this?
On 27 September we asked that question of our own buying agent, the one TheBaby uses to hire other agents on outside marketplaces. Here is the header it had been sending on every request since we built it:
User-Agent: Mozilla/5.0 tb-extmkt/0.1
That is a browser disguise. The answer to anonymous agents is not a better CAPTCHA for bots. It is a signed statement a stranger can check without calling you, and most agents, ours included, didn't send one. We run an identity product for agents, and our own agent was showing up to other people's marketplaces pretending to be a web browser. This post covers what we changed, how you can check it without trusting us, and what happened when we tested it with real money.
What we already had, and why it wasn't enough
We weren't starting from zero. Since 2 July our fleet has published a signing key at /.well-known/http-message-signatures-directory for Web Bot Auth, the RFC 9421 HTTP-signature scheme Cloudflare uses for verified bots. BlindOracle agents carry ERC-8004 passports. And every time one of our agents hands work to another, a hook writes a ProofOfDelegation record (kind 30014). There are 785 of them.
The problem was that last one. Each delegation record is signed with HMAC-SHA256, a shared-secret signature. Only someone holding the secret can check an HMAC, and anyone holding the secret can also forge one. So the only party who could verify our delegation proofs was us.
A correction: our earlier delegation whitepaper described these proofs as verifiable by third parties. With HMAC they weren't, and the zero-knowledge bridge it mentioned is not live. We've added a dated note to that page pointing here.
What we built: two credentials anyone can check
Both are W3C Verifiable Credentials signed with Ed25519 (eddsa-jcs-2022). The public key sits in our did:web document at craigmbrown.com/.well-known/did.json, under a key that does nothing else: #delegation-1, kept separate from the key that signs our audit credentials.
| Credential | What it says | Lifetime |
|---|---|---|
| AgentDelegationCredential (a grant) | Which agent this is, which operator runs it, who authorized it, what it may do (allowed actions, denied actions, actions that need a human to confirm, spend caps per call and per day, venues), and where its audit trail is kept. | 6 hours for our buyer. A withdrawn permission expires on its own. |
| AgentDelegationAttestation | A public, redacted copy of one of our internal HMAC delegation records. It's issued only after the HMAC re-verifies. The canary token is withheld, and the delegated prompt appears only as a SHA-256 hash, so it can be disclosed later and matched. | Permanent. |
This is part of the real grant our buyer presented on a live purchase the same night (signature omitted):
"agentName": "thebaby-extmkt-buyer",
"authorizedBy": {
"id": "did:web:craigmbrown.com",
"role": "human operator",
"attestation": "issuer-asserted ... not a third-party proof of human presence at signing time"
},
"permissions": {
"allow": ["discover", "purchase"],
"deny": ["act_for_any_principal_but_issuer", "purchase_above_maxUsdPerCall"],
"requiresHumanConfirmation": ["new_payment_rail", "raise_spend_caps"],
"venues": ["mcp"],
"maxUsdPerCall": 0.03,
"maxUsdPerDay": 1.0
},
"validFrom": "2026-09-27T01:38:11Z",
"validUntil": "2026-09-27T07:38:11Z"
How it travels with a request
The buyer now sends three things on every request:
- An honest
User-Agent:TheBabyFleet/1.0 (+https://craigmbrown.com/llms.txt) agent=thebaby-extmkt-buyer. - The grant, in an
Agent-Delegationheader. - A Web Bot Auth signature whose covered components include that header.
There is a cost, because the grant rides on every request: it adds about 2.5 KB of headers. We accepted that trade because a reference (a URL to fetch) would make every seller call us back, and "checkable without us" was the point. If a seller rejects the size, a reference is the fallback.
That third point matters. The HTTP signature covers the grant, so a grant copied off one request can't be attached to another. If someone replays the request to a different host, or swaps in a different grant, the signature fails.
Check it yourself, without us
You need the credential and our public did.json. You don't need an account, a key, our SDK, or any call to us. This is the whole checker we used to confirm it. It imports none of our code:
import json, sys, hashlib, urllib.request, base58
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
vc = json.load(open(sys.argv[1])); proof = vc.pop("proof")
issuer = vc["issuer"]["id"] # did:web:craigmbrown.com
url = "https://" + issuer[len("did:web:"):] + "/.well-known/did.json"
doc = json.load(urllib.request.urlopen(url))
vm = next(m for m in doc["verificationMethod"] if m["id"] == proof["verificationMethod"])
assert proof["verificationMethod"] in doc["assertionMethod"]
key = base58.b58decode(vm["publicKeyMultibase"][1:])[2:]
jcs = lambda o: json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
opts = {k: proof[k] for k in ("type","cryptosuite","proofPurpose","verificationMethod","created")}
Ed25519PublicKey.from_public_bytes(key).verify(
bytes(base58.b58decode(proof["proofValue"][1:])),
hashlib.sha256(jcs(opts)).digest() + hashlib.sha256(jcs(vc)).digest())
print("verified")
Change 0.03 to 100 and it raises InvalidSignature. The full field reference, both header formats and a verifier for the HTTP signature are on the Proof of Delegation developer page.
Proving it with money
Tests don't show whether a seller will accept any of this. So we made a real purchase: the buyer bought our own news-scanner service through the MCP payment rail for $0.02 USDC on Base over x402, with the new identity on all 53 requests. The seller side, our gateway, checked the signature against the buyer's published key directory and checked the grant against did.json, using public documents only, the way an outside site would. Both verified, and the grant was bound to the request. The payment settled in transaction 0x4492f2b1…7874.
Watch it happen
These two animations replay the real purchase from 27 September. The first is what the machines exchanged. The second is the same purchase as the agent's owner would see it in a chat. Every value shown is from the run: the caps, times, the key IDs, the seller's checks and the on-chain transaction.
Machine view: agent to agent
Human view: the same purchase, as a chat with your agent
The chat wording illustrates the real run, and every number in it is real. The last exchange (the $25.00 audit, at its live quoted price) shows how the cap applies; it wasn't run live. The buyer refuses any price above its cap.
Who else is signing?
Once our gateway could verify identities, we recorded what every visitor presented. Of the first 579 calls: 272 were anonymous, 242 declared themselves as bots in their User-Agent but signed nothing, and 65 carried a signature. 61 of the 65 were ours.
The other four came from BrickBlueBot, the crawler for a registry of agents, MCP servers and x402 endpoints. It publishes its own Ed25519 key directory and signs its requests. Our first verifier marked it unverified, because BrickBlue's signature also covers the request method (@method), a standard RFC 9421 component our code didn't handle yet. The mistake was ours, and we fixed it the same hour. So far one outside agent identifies itself this way. That's a small number, but it isn't zero.
What this does not prove
- That a human was present. The signature proves the holder of our domain key issued the grant unchanged. "Authorized by a human operator" is our assertion, signed. It isn't a third-party proof of a person at a keyboard, and the credential says so in its own text.
- That our domain is honest.
did:webis self-asserted: no certificate authority vouches for the key. If craigmbrown.com goes down, nobody can check the credentials, even though they're still valid. - That a stop reaches you when our site is down. When we first published this post, the only way to withdraw a grant was to let it expire. Later the same day we added a signed status list: a grant, or every grant to one agent, can now be stopped early, and a stop lifts only when we sign a resume order. The list lives on the same domain as our key, though. A verifier that can't reach it gets "status unknown", not "still valid". Anchoring the list somewhere we don't control is the next step. The check is in the developer reference, section 7a.
- That Cloudflare treats us as a verified bot. Our Web Bot Auth key is published and correctly formed, but not yet registered with Cloudflare.
We won't call this KYC, and we won't put biometrics on a chain to fill the human-presence gap. Our identity policy rules that out.
If you run agents
Three things are worth doing this month, with or without us:
- Give your agent an honest User-Agent that names it and links to a page about it.
- Publish a Web Bot Auth key directory and sign your requests. BrickBlue shows it's a small job.
- Put what the agent may do in something a stranger can check: a signed, expiring statement of the agent, the operator, the authorizer and the caps. The developer page shows our format.
If you sell to agents, start logging what they present, even before you enforce anything. We learned more from one night of that log than from a quarter of assuming.
What we checked, and what we didn't
Checked: the live did.json and key directory; the on-chain receipt for the purchase; our gateway's identity log (data/inbound_agent_identity.jsonl, first ~40 minutes of traffic on 27 September); the checker above, run against the real grant.
Not checked: whether any outside marketplace reads the Agent-Delegation header today; the full picture of who signs requests on the wider web (our log covers our own gateway only); whether BrickBlue's signatures verify end to end after our fix (their next visit will show it).
Would change the conclusion: an outside seller rejecting a 2.5 KB request header.