OpenAgentAuth

v1.1 · Stable · open to implement

OpenAgentAuth Spec v1.1

An open contract for proving which AI agent sent a request, whether it is trusted, and what it is allowed to do. Anyone may implement it. This site is the reference service.

The standard: Ed25519 keys, the canonical signed-request string, the permission token and the four decisions. This service: the agent registry, nonce/replay store, revocation list and trust scoring. The emotes- string prefixes and X-Emotes-* headers are the v1 wire names, kept so existing signatures stay valid.

Status

Version 1 is frozen. Field names, the canonical string, decision values and flag strings will not change within v1. New optional fields may be added; breaking changes ship as /spec/v2. All keys are Ed25519; signatures and keys are standard base64 unless stated.

0. Registration

An agent registers by signing emotes-register:{handle} with its key. No account. Browser users can use /register; agents can follow /skill.md or the MCP server at /mcp.

javascript
const kp = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
const b64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)));
const handle = "my-agent";
const public_key = b64(await crypto.subtle.exportKey("raw", kp.publicKey));
const signature = b64(await crypto.subtle.sign("Ed25519", kp.privateKey,
  new TextEncoder().encode("emotes-register:" + handle)));

await fetch("https://openagentauth.com/api/public/v1/agents", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ handle, name: "My Agent", public_key, signature }),
});
// 201 created · 409 handle_taken · 401 invalid_signature

1. Signed request

For every request, the agent builds this canonical string (fields joined by a single \n, no trailing newline) and signs its UTF-8 bytes with its OpenAgentAuth private key.

Canonical string
emotes-req:v1
{METHOD}
{URL}
{BODY_SHA256}
{TIMESTAMP}
{NONCE}
  • METHOD — uppercase HTTP method.
  • URL — full absolute URL exactly as requested, including query string.
  • BODY_SHA256 — lowercase hex SHA-256 of the raw body bytes as sent (never re-serialized JSON). Empty string when there is no body.
  • TIMESTAMP — Unix time in seconds. Accepted within ±300 seconds.
  • NONCE — 8–100 random characters, unique per agent. Reuse is rejected as replay.

Agents send the proof to websites in these headers (case-insensitive):

HTTP headers
X-Emotes-Agent: my-agent
X-Emotes-Timestamp: 1760000000
X-Emotes-Nonce: 3f9c1a7e2b4d
X-Emotes-Signature: <base64 Ed25519 signature>
X-Emotes-Delegation: <token>        (optional)

2. Verify API

The website forwards what it received to OpenAgentAuth. One call, no API key.

POST https://openagentauth.com/api/public/v1/verify
{
  "agent": "my-agent",
  "request": {
    "method": "POST",
    "url": "https://shop.example.com/api/checkout",
    "body_sha256": "9f86d08...",
    "timestamp": 1760000000,
    "nonce": "3f9c1a7e2b4d",
    "amount": 100,          // optional
    "scope": "purchase"     // optional
  },
  "signature": "<X-Emotes-Signature>",
  "delegation": "<X-Emotes-Delegation>"   // optional
}
200 response
{
  "verified": true,
  "decision_hint": "allow_with_limit",
  "risk_flags": [],
  "agent": { "handle": "my-agent", "did": "did:web:openagentauth.com:a:my-agent", "trust_score": 72, ... },
  "delegation": { "scopes": ["purchase"], "max_amount": 500, "currency": "USD", "exp": 1762592000, ... },
  "limits": { "max_amount": 500, "currency": "USD" },
  "checked_at": "2026-10-12T12:00:00.000Z"
}

A malformed body returns 400 with {"error": "invalid_request"}. Verify responses are never cached.

3. Decisions & flags

allow

Proceed.

allow_with_limit

Proceed, but enforce limits.max_amount.

require_attestation

Amount is above the agent's trust cap. The agent must collect attestations.required attestations from agents with trust 50+, then retry. No human step.

block

Reject. At least one blocking flag is present.

risk_flagEffectMeaning
agent_not_foundblockNo agent is registered under this handle.
invalid_signatureblockSignature does not match the agent's public key and canonical string.
agent_revokedblockThe agent identity has been revoked.
replayblockThis nonce was already used by this agent.
clock_skewblockTimestamp is more than 300 seconds from server time.
delegation_invalidblockDelegation token is malformed or its signature fails.
delegation_unregisteredblockToken was never registered with OpenAgentAuth.
delegation_revokedblockThe grantor revoked this permission or a parent in its chain.
delegation_wrong_agentblockToken was issued to a different agent.
delegation_expiredblockToken exp is in the past.
delegation_wrong_websiteblockRequest URL host does not match the token audience.
scope_not_grantedblockrequest.scope is not in the token scopes.
over_spending_limitblockrequest.amount exceeds max_amount.
trust_too_lowblockTrust score is below 20 and the scope is not read-only.
circuit_trippedblockThe owner tripped the circuit breaker or an auto-trip rule fired.
above_trust_caprequire_attestationrequest.amount exceeds the trust cap and the agent lacks enough peer attestations.
unverifiedinfoAgent has not completed key verification.
reportedinfoAgent has open reports awaiting quorum.

Precedence: any blocking flag → block; else amount above the trust cap without enough attestations → require_attestation; else any cap or delegation limit → allow_with_limit; else allow.

Trust caps: below 20 → read-only; 20–49 → trust × 10; 50–79 → trust × 50; 80+ → no cap. Attestations required = ceil(cap ÷ 500). Trust starts at 10 and is replayed from the public event log: clean verifies, attestations and well-used permissions raise it; forged, replayed or stale requests and upheld reports lower it. Reports are settled by a quorum of 3 agents with trust 60+.

Attestation: sign emotes-attest:{from}:{to}:{scope}:{timestamp}, POST /api/public/v1/attest. Attestations earn trust only between independent parties: vouches between agents with the same owner, the same verified organization domain, or the same homepage domain are recorded but award zero points (not_counted_reason: same_owner, same_organization, same_domain). Sub-delegation: set parent to the delegation id you hold; it may only narrow scopes, budget, website and expiry. Revoking a delegation cancels everything passed on from it. Agent onboarding instructions: /skill.md.

4. Delegation token

A grantor (an agent or any key holder) issues a permission by signing a token with its Ed25519 key. Format: base64url(payload).base64url(signature), where the signed message is emotes-delegation: followed by the base64url payload.

Payload
{
  "v": 1,
  "agent": "my-agent",              // agent handle
  "grantor_key": "<base64 Ed25519 public key>",
  "scopes": ["purchase", "refund", "book", "read", "write", "message"],
  "max_amount": 500,                 // or null
  "currency": "USD",
  "audience": "shop.example.com",    // "" = any website
  "iat": 1760000000,
  "exp": 1762592000
}

Websites can check signature, expiry, audience, scopes and amount offline. Cancellation status is only known to OpenAgentAuth — call the Verify API before high-value actions.

Offline check (Node 20+)
const [p, s] = token.split(".");
const payload = JSON.parse(Buffer.from(p, "base64url").toString());
const key = await crypto.subtle.importKey("raw", Buffer.from(payload.grantor_key, "base64"), { name: "Ed25519" }, false, ["verify"]);
const ok = await crypto.subtle.verify("Ed25519", key, Buffer.from(s, "base64url"), new TextEncoder().encode("emotes-delegation:" + p));
const valid = ok && payload.exp > Date.now() / 1000 && (!payload.audience || payload.audience === host);

4.1 Blast radius and reversals

OpenAgentAuth does not prevent prompt injection; that is the model's responsibility. OpenAgentAuth contains its blast radius: a compromised agent can only do what its delegation token allows — the listed scopes, up to max_amount per request and total_budget over the token's lifetime, on the named audience, until exp. Higher amounts without enough trust return require_attestation.

max_amount is a per-request ceiling. total_budget (optional) is cumulative: OpenAgentAuth sums every allowed amount verified under the delegation, and a request that would exceed it returns block with flag budget_exhausted. Responses include delegation.spent and delegation.remaining_budget, and limits.max_amount is the lower of the per-request ceiling and the remaining budget. Sub-delegations may only lower total_budget.

Reversals use the same token format. The grantor (or a parent agent) issues a delegation with scope refund on the same audience, with max_amount no greater than the original purchase. Websites should link the refund to the original request's nonce. Revoking the parent revokes both.

4.2 Trust weighting

Activity trust counts once per distinct independent website. A website that allowed a purchase or write adds 2.5 points; a read-only website adds 0.5 (capped at 25). Activity and delegation trust halve after 60 days without signed activity and reach zero after 120. Farming cheap reads cannot buy a purchase limit.

5. 401 challenge

When an unsigned or unknown agent calls a protected endpoint, the website responds with a standard challenge. The agent signs a fresh request and retries with the headers from section 1.

Response
HTTP/1.1 401 Unauthorized
WWW-Authenticate: OpenAgentAuth realm="shop.example.com", spec="https://openagentauth.com/docs"
Content-Type: application/json

{ "error": "agent_signature_required", "spec": "https://openagentauth.com/docs" }

Use 403 for block, and 402 with the required attestation count for require_attestation.

6. Reference middleware

Express (Node.js)
import crypto from "node:crypto";
app.use(express.raw({ type: "*/*" }));
export async function openagentauth(req, res, next) {
  const h = (k) => req.get("x-emotes-" + k);
  if (!h("agent")) return res.status(401).set("WWW-Authenticate", 'OpenAgentAuth realm="' + req.hostname + '"').json({ error: "agent_signature_required" });
  const body = req.body?.length ? req.body : Buffer.alloc(0);
  const r = await fetch("https://openagentauth.com/api/public/v1/verify", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({
    agent: h("agent"), signature: h("signature"), delegation: h("delegation") || null,
    request: { method: req.method, url: req.protocol + "://" + req.get("host") + req.originalUrl,
      body_sha256: body.length ? crypto.createHash("sha256").update(body).digest("hex") : "",
      timestamp: Number(h("timestamp")), nonce: h("nonce") } }) }).then((r) => r.json());
  if (r.decision_hint === "block") return res.status(403).json(r);
  req.agent = r; next();
}
Next.js (App Router / Edge)
export async function verifyAgent(req: Request) {
  const h = (k: string) => req.headers.get("x-emotes-" + k);
  if (!h("agent")) return new Response(JSON.stringify({ error: "agent_signature_required" }), { status: 401, headers: { "WWW-Authenticate": 'OpenAgentAuth realm="' + new URL(req.url).host + '"' } });
  const raw = new Uint8Array(await req.clone().arrayBuffer());
  const hex = raw.length ? [...new Uint8Array(await crypto.subtle.digest("SHA-256", raw))].map((b) => b.toString(16).padStart(2, "0")).join("") : "";
  const r = await fetch("https://openagentauth.com/api/public/v1/verify", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({
    agent: h("agent"), signature: h("signature"), delegation: h("delegation"),
    request: { method: req.method, url: req.url, body_sha256: hex, timestamp: Number(h("timestamp")), nonce: h("nonce") } }) }).then((r) => r.json());
  if (r.decision_hint === "block") return Response.json(r, { status: 403 });
  return r; // check r.decision_hint and r.limits in your handler
}
FastAPI (Python)
import hashlib, httpx
from fastapi import Request, HTTPException

async def openagentauth(request: Request):
    h = lambda k: request.headers.get("x-emotes-" + k)
    if not h("agent"):
        raise HTTPException(401, "agent_signature_required", headers={"WWW-Authenticate": f'OpenAgentAuth realm="{request.url.hostname}"'})
    body = await request.body()
    async with httpx.AsyncClient() as c:
        r = (await c.post("https://openagentauth.com/api/public/v1/verify", json={
            "agent": h("agent"), "signature": h("signature"), "delegation": h("delegation"),
            "request": {"method": request.method, "url": str(request.url),
                "body_sha256": hashlib.sha256(body).hexdigest() if body else "",
                "timestamp": int(h("timestamp") or 0), "nonce": h("nonce")}})).json()
    if r["decision_hint"] == "block":
        raise HTTPException(403, r["risk_flags"])
    return r  # use as Depends(openagentauth)

7. Rate limits

The Verify API is never limited by IP address, because many agents share cloud IPs. Agent registration is limited to 5 per hour per network to prevent mass handle claims. Lookups and avatars are edge-cached.

8. Circuit breaker

One signed call stops an agent everywhere. Every later Verify returns block with circuit_tripped until the owner resets it. Revocation is permanent; a trip is reversible.

Trip / reset
POST https://openagentauth.com/api/public/v1/circuit/trip   {"handle","timestamp","signature","reason?"}
POST https://openagentauth.com/api/public/v1/circuit/reset  {"handle","timestamp","signature"}
signed string: emotes-circuit:{trip|reset}:{handle}:{timestamp}

GET  https://openagentauth.com/api/public/v1/circuit/{handle}
→ {"state":"closed|tripped|revoked","tripped_at","reason","rules":[...]}

Auto-trip rules are checked inside Verify (no background jobs). spend_velocity trips when allowed spend in the window plus this request exceeds the threshold. failure_burst trips when blocked requests in the window reach the threshold.

Rules
POST https://openagentauth.com/api/public/v1/circuit/rules
{"handle","timestamp","signature" /* emotes-circuit:rules:{handle}:{timestamp} */,
 "rules":[{"kind":"spend_velocity","threshold":500,"window_seconds":600},
          {"kind":"failure_burst","threshold":10,"window_seconds":300}]}

9. Receipts

After an action, the agent and the website both sign the same receipt. A receipt earns trust only when both signatures verify and the website is independent; each website counts once. Receipts are public.

Receipt string
emotes-receipt:v1
{agent}
{relying_party_host}
{action}
{amount or ""}
{success|failure|refunded}
{ts}
{nonce}

POST https://openagentauth.com/api/public/v1/receipts
{"receipt":{"v":1,"agent","relying_party","action","amount","outcome","ts","nonce"},
 "agent_signature","rp_public_key","rp_signature"}
GET  https://openagentauth.com/api/public/v1/receipts/{handle}

10. Key & operator rotation

Identity is bound to the handle, not the model. Both the current and the new key sign emotes-rotate:{handle}:{new_public_key}:{operator}:{timestamp}. A key rotation keeps trust. An operator change resets positive trust to probation (10); penalties carry over. Both are public trust events.

Rotate
POST https://openagentauth.com/api/public/v1/rotate
{"handle","new_public_key","operator","model?","timestamp","signature","new_signature"}

11. Test vectors

Ed25519 private seed is 32 bytes of 0x07. Ed25519 is deterministic, so your implementation must produce these exact signatures.

Vectors
seed_hex    0707070707070707070707070707070707070707070707070707070707070707
public_key  6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw=

sign("emotes-register:test-agent")
  HEA2rrucEJ3qEYC8llc3Wf5iGNW4dKdM/SK0zjl0lGH19M2ISmrqPNovz5sQQNcmJ6JabHthxdcYaMUPRSwDCA==

sign("emotes-req:v1
POST
https://shop.example/checkout
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1767225600
n-0001")
  R5BoEw5T1Kp5KI7/z48VTjni+kP1GI3Fp7iNwFVpS94+znE21LeuxHQYDmHiIsyR411YYjzXXjX6l0s5vuWXBA==

sign("emotes-circuit:trip:test-agent:1767225600")
  wmS/SpZG/9iKvXhh5Eoovy+79AG5P+7tuvVfonAw3WEDYdjDRrVAbdYVMJxZpQTkRQun9+EkDvkVP2XTWGLYCA==

12. Conformance

An implementation conforms to v1.1 when it: (1) reproduces the test vectors; (2) rejects timestamps outside 300 s and reused nonces; (3) returns only the four decisions; (4) lets sub-permissions only narrow scopes, budgets, website and expiry; (5) cascades revocation to every derived permission; (6) blocks tripped agents; (7) counts receipts only when co-signed by an independent website; (8) resets positive trust on operator change. The full spec is also available as Markdown.