Private beta

Request beta access

Interagentic is in private beta. Leave your email and we'll send you an access key when there's room.

Interagentic
Guides

Verify an agent

Check who is calling, whether a human stands behind them, and what that human allowed — without storing anything.

Three questions, in increasing order of cost. Most services only need the first.

1. Who is calling?

Verify the token against the broker's key set. Nothing is stored on your side.

import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(
  new URL("https://id.interagentic.dev/brave-fox-a3f2/jwks.json"),
);

const { payload } = await jwtVerify(token, jwks, {
  audience: "api.example.com",
  maxTokenAge: "5m",
});

// payload.sub === "interagentic://id.interagentic.dev/brave-fox-a3f2"

In practice you derive the JWKS URL from the token's iss rather than hardcoding it — see publishing a service.

Always check aud. Without it you accept tokens minted for someone else.

2. Is a human behind it?

Ask the broker, authenticated as your own domain:

const assertion = await new SignJWT({})
  .setProtectedHeader({ alg: "ES256", kid: DOMAIN_KEY_ID })
  .setIssuer("api.example.com")
  .setAudience("id.interagentic.dev")
  .setIssuedAt()
  .setExpirationTime("2m")
  .sign(domainPrivateKey);

const res = await fetch(
  "https://id.interagentic.dev/brave-fox-a3f2/metadata.json",
  { headers: { authorization: `Bearer ${assertion}` } },
);

const { linked, humanId, permissions } = await res.json();

This requires your domain to publish its own key set at /.well-known/interagentic/jwks.json. That is how the broker knows the request really comes from you, and it is what scopes the answer to you.

humanId is stable for your domain and unrelated to the identifier the same person has at any other service. Use it as your user key.

3. What did they allow?

permissions in the same response contains only the grants made to you:

{ "linked": true, "humanId": "h_9c3a…", "permissions": ["orders:read"] }

If a permission you need is missing, refuse with a link:

if (!permissions.includes("orders:write")) {
  const url = new URL(
    "https://id.interagentic.dev/brave-fox-a3f2/link",
  );
  url.searchParams.set("service", "api.example.com");
  url.searchParams.set("permissions", "orders:read,orders:write");

  return Response.json(
    { error: "user_required", authorizationUrl: url.toString() },
    { status: 403, headers: { Link: `<${url}>; rel="approve"` } },
  );
}

The agent hands that link to its human and retries. Ask for the narrowest set you can work with — a person reading the approval screen is deciding whether to trust you, and a long list is a reason to say no.

Skipping a round trip

If the broker supports it, the agent can present a broker-signed token that already carries human and permissions. Verify it against the broker's own key set and skip step 2 entirely:

const brokerJwks = createRemoteJWKSet(
  new URL("https://id.interagentic.dev/.well-known/jwks.json"),
);

const { payload } = await jwtVerify(brokerToken, brokerJwks, {
  audience: "api.example.com",
});

// payload.human, payload.permissions — signed by the broker, not the agent

Treat this as a fast path. Keep metadata.json as the fallback, because a conforming broker is allowed not to implement it.

Caching

ThingCache forWhy
JWKSminutes to hoursKeys rotate; jose refetches on unknown kid
metadata.jsonsecondsPermissions are revoked in real time
Token verificationneverTokens already expire in five minutes

Do not cache a permission decision past a few seconds. Revocation that takes an hour to apply is not revocation.

Demanding a fresher key

If your service handles something sensitive, state a maximum key age and let the agent rotate:

const rotatedAt = Date.parse(metadata.keyRotatedAt);
if (Date.now() - rotatedAt > 15 * 60 * 1000) {
  return new Response(null, {
    status: 401,
    headers: {
      "WWW-Authenticate":
        'Interagentic realm="api.example.com", error="key_too_old", max_key_age=900',
    },
  });
}

A conforming client rotates and retries once. Choose the number deliberately: frequent rotation shortens the window a stolen key is useful for, and lengthens the time your callers spend rotating instead of working.

On this page