Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Why API keys are not enough for the agentic web

There is a quiet assumption baked into every API on the internet: the caller is a human, or software a human configured. That assumption is breaking. In the last year, APIs have started receiving traffic from a new cla

There is a quiet assumption baked into every API on the internet: the caller is a human, or software a human configured.

That assumption is breaking.

In the last year, APIs have started receiving traffic from a new class of caller — autonomous AI agents that discover services, negotiate scopes, delegate work to sub-agents, and execute transactions without a human in the loop. When one of these agents calls your API, you need to answer three questions in under 50 milliseconds:

  1. Which agent is this? — Identity
  2. Is this request genuine? — Integrity
  3. Is it allowed to do this? — Authority

Right now, most APIs answer these questions with a static bearer token. That worked when a human provisioned the token, scoped it by hand, and revoked it when the integration was retired. It does not work for agents.

This post explains why, and sketches the primitives that do.

The failure modes of static credentials

API keys fail against agents in five specific ways.

Keys are static, agents are dynamic. An agent's authority should change based on what it's doing, where it's calling, and how much it has already spent. A single string cannot express "this agent can spend up to $250 on airline bookings, only until Friday, only on api.airline.com." So teams bolt on their own scope checks, and those checks become a second system with its own bugs.

Keys leak. This is not a theoretical concern. Static secrets end up in logs, error trackers, git history, and the prompts of LLMs that were told to "just debug this for me." Once a key leaks, every caller using it is indistinguishable from the legitimate one. Rotation helps, but rotation is a manual process a human has to remember.

Keys have no attribution down a chain. When Agent A delegates a task to Agent B, and B calls your API, your logs see B's key. You have no record that A authorized B, no way to revoke A's authority and have it cascade down, and no way to prove in an audit which human or system was ultimately responsible for the action.

Replays are trivial. A captured request with a valid key can be resent indefinitely. Without a nonce and a fresh timestamp, you cannot distinguish a live request from one that was recorded three days ago and replayed by an attacker.

There is no trust signal. A brand-new key and a key that has made ten million clean calls look identical to your API. You either allow both or block both.

What agents actually need

If you were designing authentication for a world where the caller is a non-human entity that registers itself, delegates scoped authority, and behaves over time, you would build five things:

1. Cryptographic identity that agents can self-provision.
An agent should be able to generate a key pair, sign a challenge, and get a persistent identity — without an email, a credit card, or a human clicking "Allow." The identity should resolve to a public document, so any service can verify it without calling a central authority. This is what did:web was designed for.

2. Requests that prove themselves.
Every request should carry a signature over a canonical string that includes the HTTP method, the full URL, a hash of the body, a fresh timestamp, and a unique nonce. The receiving server verifies the signature, rejects anything outside a small time window, and blocks nonces it has seen before. Tampering, delayed delivery, and replay all become detectable.

3. Trust as a continuous variable, not a binary.
Instead of "trusted / untrusted," agents should have a score derived from observable behavior — clean verifications, successful delegations, peer attestations, and upheld abuse flags. That score should be computed from an append-only public event log, so anyone can replay the math and audit how a score was earned. New agents start low. Spending caps scale with trust.

4. Delegations that can only narrow.
When one agent hands work to another, it should issue a signed slip that specifies exactly what the sub-agent can do: which host, which scopes, which spending ceiling, which expiry. Critically, a delegation chain must only be able to restrict authority, never expand it. Revoking a parent delegation should kill everything downstream in one operation.

5. A single verify call.
Receiving APIs should not have to reimplement signature checking, replay protection, delegation parsing, and trust scoring. They should make one call and get back a deterministic decision.

The four decisions

A verification engine should return one of exactly four values, and no more:

Decision Meaning
allow Verified, trusted, within scope. Execute.
allow_with_limit Verified, but the caller must enforce the returned cap.
require_attestation Verified, but the action is high-value or the agent's trust is insufficient. Require N signed peer attestations before executing.
block Forged, replayed, revoked, stale, or flagged. Refuse.

The require_attestation decision is the one that makes autonomous workflows actually work. Traditionally, a high-value action triggers a human OTP or a Slack approval. That breaks the agent's execution loop. Instead, an agent can collect signed endorsements from peers it has worked with before. Trust is transitive across a network of agents, not gated on a human waking up.

A concrete design

For every request, the agent builds a canonical string:

emotes-req:v1
{METHOD}
{FULL_URL}
{BODY_SHA256}
{TIMESTAMP}
{NONCE}

It signs the UTF-8 bytes with its Ed25519 private key and sends the signature in a header. The receiving API forwards the request to a verify endpoint, which:

  1. Fetches the agent's public key by handle.
  2. Reconstructs the canonical string from the incoming request.
  3. Verifies the signature.
  4. Rejects if the timestamp is more than ±300 seconds old.
  5. Rejects if the nonce has been seen before.
  6. Walks the delegation chain (if any) and confirms none of its links are revoked, and that each link only narrows the previous one.
  7. Compares the requested action against the agent's trust score and the delegation's scope and cap.
  8. Returns one of the four decisions.

None of this requires a human in the loop. All of it is deterministic and reproducible.

Integrating it in 15 lines

The point of this design is that it should not require a rewrite of your API. Middleware in Express:

import express from "express";
import { emotesVerify } from "@emotes/verify";

const app = express();
app.use(express.json());

app.use(
  emotesVerify({
    apiKey: process.env.EMOTES_API_KEY,
    minTrustScore: 50,
    onDecision: (req, decision) => {
      req.agent = decision.agent;
      req.limits = decision.limits;
    },
  })
);

app.post("/bookings", (req, res) => {
  if (!req.agent) return res.status(401).end();
  res.json({ booked: true, by: req.agent.handle });
});

FastAPI version:

from fastapi import FastAPI, Depends
from emotes_verify import verify, Decision

app = FastAPI()

@app.post("/bookings")
def book(decision: Decision = Depends(verify(min_trust=50))):
    if decision.action == "block":
        return {"error": "blocked"}, 403
    return {"booked": True, "by": decision.agent.handle, "cap": decision.limits.max_amount}

Behind that middleware, the verification happens synchronously, in the request path, in under 50ms.

What is open and what is hosted

The specification is open. The signing format, the canonical string, the delegation format, the four decision values, the HTTP status codes — all frozen at /spec/v1 and intended to be implemented by anyone. If a competitor wants to build a compatible registry, they can. Protocols that only one company can implement do not become protocols.

The registry and verification service are hosted. That is the piece that benefits from a network effect: the more agents register, the more trust data accumulates, the more useful attestations become, and the more services that adopt the middleware as a default.

The skill file at /skill.md is a plain markdown document that an LLM can read and follow to register itself without a human at the keyboard. That file is the whole point of the design. A tool that requires a developer to manually onboard each agent is not an agent-native tool.

The open questions

This design is not finished, and I want criticism more than applause.

Trust scoring is hard to get right. A score that can be gamed is worse than no score. Right now, trust is computed deterministically from an event log, with per-agent rate limits and peer attestation requirements to prevent self-dealing. It still needs adversarial testing at scale. How would you attack it?

Attestations assume a healthy peer network. Early in a network's life, there may not be enough high-trust peers to attest. The design includes a bootstrap path, but it is one of the least battle-tested parts of the system.

Delegation depth is unbounded by default. Chains can only narrow, but a very deep chain is still very deep. Should there be a maximum depth? What is the right default?

The did:web method is a starting point, not an answer. Some ecosystems are moving toward did:key or chain-anchored DIDs. The spec is designed to be method-agnostic, but I have not yet implemented alternatives.

If you have built agentic systems and run into any of this — or if you have opinions on the trust model, the delegation format, or the verify decision set — I would genuinely like to hear them.

The spec is at agentemotes.com/spec/v1. The live feed of every verify decision (anonymised) is at agentemotes.com/feed. The MCP server is at agentemotes.com/mcp.

The agentic web is coming. It needs a trust layer. This is one attempt at one.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.