You need @delegus/ 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.
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.
{ "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" }
] }- A money route becomes
commerce:, so a permission can say "payees namedpurchase acme-*, up to this amount". - An
api:callroute binds the request URL itself, so a permission nameshttps:./ / pay. example. com/ accounts/ * {arg:…}and the amount fields point into the request'sbody,pathandquery, the same way the MCP tool map points into a tool's arguments.- A request that matches no route is still checked, as
api:callon its URL. OnlyOPTIONSpasses unchecked.
The middleware serves the map at /, so an agent can build exactly the action you check.
What the caller sees#
Allowed. Your handler runs with req., and the response carries a Delegus- header. Keep the receipt id with the request in your logs.
Refused. The caller gets HTTP 403 and your handler never runs:
{ "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-, Delegus- (your relying party's DID) and Delegus-, 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_ before Delegus is asked.
The agent's side#
An agent built on @delegus/ signs each request through the same map:
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/ 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.
DELEGUS_RP_KEY=dk_rp_… npx @delegus/http-gateway \
--upstream http://api:8080 --public-base-url https://pay.example.com --route-map routes.json--upstream: where your API listens. Keep it on a private network; the gateway only protects the API if it's the only way in.-: the gateway's public origin, the one agents sign for. Origin only: agents sign for the origin plus the request path, so a path here would double it, and the gateway refuses to start.- public- base- url --route-map: the same route map as above. Without one, every request is checked asapi:callon its URL.DELEGUS_: your verify key, read from the environment only.RP_ KEY
An allowed request is forwarded with exactly the bytes that were checked, plus Delegus- and Delegus-; 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_: it fails closed. A body that isn't strict JSON, or is too large, is refused with REQUEST_, and a body the map can't apply with ARGUMENTS_, both before Delegus is asked.
Other frameworks#
checkHttpRequest( 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#
- Only what goes through the check is protected. Routes your server exposes some other way aren't covered.
- Only the fields the map names are bound. Validate the rest of the request in your handler.
- The gateway adds one network hop. Node servers can use the middleware instead and skip it.