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.

Protocol 01

Spec v0.1

An identity your agent can prove.

A namespace, a keypair, and a JWKS. Any server verifies the agent's tokens by fetching a public key — no registration on your side, no shared secret, no account for the agent to create.

agent@host
$npx interagentic init acme-worker  generating Ed25519 keypair…  POST /acme-worker/register → challenge  signing challenge with private key✓ acme-worker registered   key       key_V1StGXR8 · EdDSA  private   ~/.interagentic/private.pem (0600)  public    https://id.interagentic.dev/acme-worker/jwks.json

Anatomy

The whole identity is one URI.

It names the broker that vouches for the agent and the namespace the agent holds there. Nothing else is needed to verify a token.

interagentic://id.interagentic.dev/brave-fox-a3f2

▔▔ broker — serves the public keys▔▔ namespace — first come, first served

The broker is swappable

id.interagentic.dev is the default, not the protocol. Any domain that serves the three endpoints is a broker, and a service decides for itself which brokers it trusts.

The namespace is a handle

Lowercase, hyphenated, 1–63 characters, matching ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$. Claimed once, by whoever asks first — like a domain.

The private key never moves

It is generated on the machine and written to ~/.interagentic/private.pem with mode 0600. The broker only ever sees the public half and a signature proving you hold the other one.

Registration

Claiming a namespace takes two requests.

A challenge proves the agent holds the private key for the public key it just submitted. Nothing is issued until that signature checks out.

  1. 1
    agentbroker

    Submit the public key

    If the namespace is taken by a different key, the broker answers 409 and the agent picks another name.

    POST /acme-worker/register { "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA…" }

  2. 2
    brokeragent

    Receive a challenge

    A single-use nonce, valid for five minutes.

    200 OK { "challenge": "3f9c1e…", "expiresIn": 300 }

  3. 3
    agentbroker

    Sign it back

    The signature is over the raw challenge bytes, using the key just submitted.

    POST /acme-worker/register/verify { "challenge": "3f9c1e…", "signature": "MEUCIQ…" }

  4. 4
    brokereveryone

    The public key goes live

    From this moment the JWKS is world-readable, and any service can verify the agent's tokens without ever talking to us about it.

    201 Created GET https://id.interagentic.dev/acme-worker/jwks.json

id.interagentic.dev/acme-worker/jwks.json
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "alg": "EdDSA",
      "use": "sig",
      "kid": "key_V1StGXR8",
      "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
    }
  ]
}

Why a challenge at all

Without it, anyone could claim a namespace by pasting someone else's public key — and then that person's signatures would validate against a name they do not control.

Authentication

A signed token, and a key you fetch yourself.

The agent signs a short-lived JWT. Your server resolves the broker from the issuer, fetches the JWKS, and verifies. That is the entire integration.

Authorization: Bearer …
{
  "alg": "EdDSA",
  "kid": "key_V1StGXR8"
}
.
{
  "iss": "interagentic://id.interagentic.dev/brave-fox-a3f2",
  "sub": "interagentic://id.interagentic.dev/brave-fox-a3f2",
  "aud": "api.example.com",
  "iat": 1774051200,
  "exp": 1774051500,
  "jti": "01JQ8F3K…"
}
verify.ts
import { jwtVerify, createRemoteJWKSet } from "jose";

// interagentic://<broker>/<namespace>
const { host, pathname } = new URL(token.iss.replace("interagentic:", "https:"));
const jwks = createRemoteJWKSet(
  new URL(`https://${host}${pathname}/jwks.json`),
);

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

// payload.sub is the caller. There is nothing to look up.

Claims

issstringrequired
The agent's full identity URI. The broker host is taken from here, so a service never has to be told where to look.
substringrequired
The acting identity. Equal to iss unless the agent is acting on behalf of a group it belongs to.
audstringrequired
The service the token is for. Audience-scoping is what stops a token captured by one service from being replayed against another.
expnumberrequired
At most five minutes out. Tokens are cheap to mint, so there is no reason for a long-lived one to exist.
jtistringrequired
Unique per token, so a service that cares can reject a replay within the expiry window.

Human linkage

Is there a person behind this agent?

Often that is the only thing your service needs to know. The broker will tell you — but it tells you about your relationship with that human, and nobody else's.

  1. 1
    servicebroker

    Ask, signed as your domain

    The request is authenticated with a JWT signed by your domain key and expiring within five minutes. That signature is what scopes the answer to you.

    GET /brave-fox-a3f2/metadata.json Authorization: Bearer <jwt signed by api.example.com>

  2. 2
    brokerservice

    Get a per-service view

    humanId is derived from your root domain plus the broker's internal user id. The same person is a different, stable id at every service — so two services comparing notes cannot tell they are talking about the same human.
  3. 3
    serviceagent

    No human yet? Send the agent a link

    The URL is deterministic, so the agent can build it without asking anyone. Permissions you want are declared right in the query string.

    403 Forbidden Link: <https://id.interagentic.dev/brave-fox-a3f2/link?service=api.example.com &permissions=orders:read,orders:write>; rel="approve"

  4. 4
    humanbroker

    One approval, once

    The human signs in, sees exactly which permissions are being requested and by whom, and approves. The agent repeats its original call and it goes through.
id.interagentic.dev/brave-fox-a3f2/metadata.json
{
  "namespace": "brave-fox-a3f2",
  "linked": true,
  "humanId": "h_9c3a1f7e5b2d8046",
  "permissions": ["orders:read", "orders:write"],
  "keyRotatedAt": "2026-09-17T08:14:02Z"
}

What you cannot see

Not the person's name, email, or broker id. Not the permissions they granted to anyone else. Not which other services they use. You get a stable handle for your own relationship, and that is all.

Optimisation

Or collapse it into one token.

Two extra round-trips per call adds up. A broker may let an agent pre-fetch an audience-scoped token that already carries the human id and the active permissions.

The agent asks its broker once; the service verifies one signature and reads everything it needs from the claims. Because the token is audience-scoped and short-lived, it is worth nothing to anyone else — and because the broker signs it, the service does not have to trust the agent's account of its own permissions.

This one is optional

A broker that does not implement it is still a conforming broker. Services should treat the token as a fast path and keep the metadata call as the fallback.
broker-signed token
{
  "alg": "EdDSA",
  "kid": "broker_2f91"
}
.
{
  "iss": "https://id.interagentic.dev",
  "sub": "interagentic://id.interagentic.dev/brave-fox-a3f2",
  "aud": "api.example.com",
  "human": "h_9c3a1f7e5b2d8046",
  "permissions": ["orders:read", "orders:write"],
  "exp": 1774051500
}

Key rotation

A stolen key should stop working on its own.

Rotation is not a fixed treadmill. A service states how fresh a key it is willing to accept, and the agent rotates on demand.

the service sets the policy
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Interagentic realm="api.example.com",
                  error="key_too_old", max_key_age=900

A standard WWW-Authenticate challenge, so it survives contact with ordinary HTTP tooling. The agent reads max_key_age, rotates, and retries — one extra round trip, no new status code to learn, and no global policy forcing every agent on the network to churn keys it does not need to.

rotation is a hash chain
{
  "keyId": "key_8Hq2Lm4P",
  "fingerprint": "SHA256:xK9c…",
  "previous": "SHA256:aB3f…",
  "rotatedAt": "2026-09-17T08:14:02Z"
}

Every key records the fingerprint it replaced. If a token ever arrives signed by a fingerprint that has already been rotated out, two copies of that key exist — so the broker locks the namespace immediately and only the linked human can unlock it.

Idempotent by construction

Rotating to a key that is already current returns the same result instead of chaining again. A retried request after a timeout cannot skip a link.

Race-proof on the server

Concurrent rotations for one namespace serialise behind a mutex; losers get 429 with a retry hint rather than a corrupted chain.

Safe on the client

The SDK takes a file lock, merges concurrent rotation requests into one, and write-ahead-logs the pending key — so a process killed mid-rotation recovers instead of locking you out.

Roll your own carefully

The dangerous move during recovery is retrying with the old key. A rotated-out fingerprint is exactly the signal that triggers a lockout, so a naive retry loop locks the namespace it was trying to save. The SDK tries the pending key first for this reason.

Every protocol here is a spec you can implement yourself.

There is no gatekeeper. Run your own broker, publish your own service manifest, or point the CLI at a network that has nothing to do with us. The specs are the product.

$npx interagentic init
Read the specsGitHub