DelegusDocsv0.3 rc1

Docs/Check an agent’s authority in front of any API

Guides

Check an agent's authority in front of any API

Most businesses an AI agent deals with don't run MCP. They run an ordinary web API: a payments endpoint, an orders endpoint, an accounts endpoint. The same check fits there. Before your handler runs, Delegus checks that the company behind the agent allowed this exact request, and your server gets a signed receipt for the answer. A refused request never reaches your code.

You need @delegus/sdk 0.3.1 or later, and a verify key from signing up.

Add it to your server#

For a Node server, the check runs inside your app. For an API in any other language, run the gateway in front of it (below).

Express-style servers take one line after the body parser. Fastify takes a preHandler hook.

ts
import { Delegus, fastifyDelegusHttp } from "@delegus/sdk";
const delegus = new Delegus({ apiKey: process.env.DELEGUS_RP_KEY!, publicBaseUrl: "https://pay.example.com" });

// Express style
app.use(express.json());
app.use(delegus.http({ routeMap }));

// Fastify
fastify.addHook("preHandler", fastifyDelegusHttp(delegus, { routeMap }));

publicBaseUrl is the address agents call and sign for. Behind a proxy, it's the public one.

Say what each route does: the route map#

A permission talks about actions: pay this payee up to this amount, read these accounts. The route map tells Delegus which action each of your routes is.

json
{ "version": 1, "routes": [
  { "id": "payments.create", "method": "POST", "path": "/payments",
    "action": "commerce:purchase", "resource": "payee:{arg:/body/payee}",
    "amount": { "value": "/body/amount", "currency": "/body/currency", "unit": "minor" } },
  { "id": "accounts.read", "method": "GET", "path": "/accounts/{id}", "action": "api:call" }
] }

The middleware serves the map at /delegus-routes.json, so an agent can build exactly the action you check.

What the caller sees#

Allowed. Your handler runs with req.delegus = { receipt, action, route }, and the response carries a Delegus-Receipt-Id header. Keep the receipt id with the request in your logs.

Refused. The caller gets HTTP 403 and your handler never runs:

json
{ "error": "DENY", "message": "…",
  "delegus": { "decision": "DENY", "reason": "AMOUNT_EXCEEDS_AUTHORITY",
               "receipt_id": "drc_…", "receipt_url": "…" } }

The reason is one of the DENY reason codes. A refusal is signed too, so it leaves its own receipt.

No credentials. HTTP 401 with Delegus-Verify: required, Delegus-Relying-Party (your relying party's DID) and Delegus-Routes: <origin>/delegus-routes.json;sha256=…, which tells an agent where to find the map and which version you're checking.

A request signed for a different method or URL is refused with PROOF_BINDING_MISMATCH before Delegus is asked.

The agent's side#

An agent built on @delegus/sdk signs each request through the same map:

ts
const headers = await agent.httpHeaders({ grant, method: "POST", url: "https://pay.example.com/payments",
                                          rpDid, body, routes: routeMap });
await fetch("https://pay.example.com/payments", { method: "POST",
  headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify(body) });

The agent needs a permission signed by the company it acts for. Your own permission signer, in two commands shows how to make one.

Any language: the gateway#

@delegus/http-gateway runs the same check as a reverse proxy in front of an API written in anything. Every request is checked before your API sees it, and only an allowed one is forwarded.

text
DELEGUS_RP_KEY=dk_rp_… npx @delegus/http-gateway \
  --upstream http://api:8080 --public-base-url https://pay.example.com --route-map routes.json

An allowed request is forwarded with exactly the bytes that were checked, plus Delegus-Receipt-Id and Delegus-Decision: ALLOW; copies of those headers sent by the client are dropped, so your API can trust them. A refusal is the same 403 as above. If Delegus can't be reached, the gateway answers 403 SERVICE_UNAVAILABLE: it fails closed. A body that isn't strict JSON, or is too large, is refused with REQUEST_UNREADABLE, and a body the map can't apply with ARGUMENTS_UNMAPPABLE, both before Delegus is asked.

Other frameworks#

checkHttpRequest(delegus, { routes }, { method, url, headers, body }) is the check on its own. Pass the full public URL and the parsed body, and it returns the status, the headers and, on a refusal, the JSON body to send. Compile the map first with compileRouteMap.

Limits#