Skip to main content
Most AFAuth services declare unclaimed_mode = "attested_only" — it’s the defineService default — and require an attestation on each signed request, a signal that a verified human stands behind the agent. This guide walks you through producing that signal using the canonical trust attestor at trust.afauth.org.
This is a core step of the default agent journey: the standard AFAuth service ships unclaimed_mode = "attested_only", so your agent must link to a human before it can sign up. You can skip linking only when the target service advertises unclaimed_mode = "free" — then your signed request alone suffices.
Just want your Claude Code / Codex agent to use AFAuth, no code required? Use AFAuth with Claude Code & Codex is the no-code on-ramp — afauth trust link does everything on this page in one command.

How it works

The agent never authenticates the human — the trust attestor does. The flow is a three-leg handoff:
  1. Agent → Trust: open a link request, get a deep-link URL and a poll URL.
  2. Human → Trust (browser): sign in at trust.afauth.org, confirm the request.
  3. Agent ↔ Trust: poll until confirmed, then mint short-lived per-service JWTs as needed — each signed with the agent key, no bearer token.
You do this once per human/agent pair — the binding persists as long as the agent keeps minting (it lapses only after ~90 days of inactivity). Each per-service JWT is fresh, audience-bound, and ≤ 900 s lived.

Before you start

You need:
  1. An agent identity — an Ed25519 keypair and the did:key derived from it. Agent.generate() (TypeScript) or afauth init (CLI) produces one.
  2. A way to show a URL to a human — open in the user’s browser, print to the terminal, send via your own out-of-band channel, etc.
  3. The destination service_did for each service you intend to attest against. Read it from the service’s /.well-known/afauth discovery document.

The four steps

The @afauthhq/agent package ships a TrustClient that handles the link flow, polling, and per-service token minting. The examples below use it; the Trust API reference documents the wire surface if you’re integrating from another language.
1

Start a link request

import { Agent, TrustClient } from "@afauthhq/agent";

const agent = await Agent.generate();

const trust = new TrustClient({
  agentDid:        agent.did,
  agentPublicKey:  agent.publicKey,
  agentPrivateKey: agent.exportPrivateKey(),
});

const start = await trust.linkStart({
  label:       "Atlas (research agent)",
  callbackUrl: "http://127.0.0.1:9876/done",  // loopback only; optional
});

// {
//   req_id:     "lr_T1uvWxYz3456_AbCdEfGhIj",
//   link_url:   "https://trust.afauth.org/link?req=...",
//   poll_url:   "https://trust.afauth.org/v1/link/poll",
//   expires_in: 1800
// }
link_url is what the human visits. req_id is the opaque handle you’ll use to poll. The whole request expires in 30 minutes (expires_in: 1800) — make sure the human can act within that window.
For desktop / CLI agents, set callbackUrl to a loopback URL — the confirmation page will redirect there after the human confirms, so your agent can detect completion without spinning the poll loop hard. Loopback only (127.0.0.1 or localhost); non-loopback URLs are rejected.
2

Surface the deep link to the human

How you do this depends on the agent runtime:
  • Server agents with a chat UI: render link_url as a button.
  • CLI agents: print the URL and optionally open / xdg-open it.
  • Headless agents: send the URL via your existing notification channel (email, push, Slack, etc.).
The human lands on trust.afauth.org/link, signs in if needed (email magic-link, OAuth), reviews the agent label, and clicks confirm.
3

Poll for confirmation

linkPoll returns the binding on confirmation, undefined while pending, and throws on error:
let binding;
while (!(binding = await trust.linkPoll(start.req_id))) {
  await new Promise((r) => setTimeout(r, 2_000));
}

// `binding` is just { binding_id, binding_token_expires_at } — there is no
// secret here. The agent's keypair is the only credential; each mint is
// signed with it. Persist the keypair securely (encrypted at rest).
There is no bearer token to keep — the agent authenticates each mint by signing the request with its account key (treat that key like an API key: encrypted at rest, never logged). The binding is long-lived — it refreshes on every mint and lapses only after ~90 days of inactivity; the human can revoke it from trust.afauth.org/account.If you persist the binding and want to skip the link dance on the next run, restore it on construction:
const trust = new TrustClient({
  agentDid:        agent.did,
  agentPublicKey:  agent.publicKey,
  agentPrivateKey: agent.exportPrivateKey(),
  binding,                                   // restore from disk
});

if (!trust.isLinked()) {
  // binding is expired or absent → start the link flow from step 1
}
4

Mint per-service attestation JWTs

Whenever you’re about to send a signed AFAuth request to a service that accepts afauth-trust, call token(serviceDid). It mints a fresh JWT scoped to that service and caches by audience (refreshed at ~80% of TTL):
const { jwt, verification } = await trust.token("did:web:api.example.com");
// verification is the strongest method the linked human has on file —
// "email" | "oauth" | "payment". You usually don't need to look at it;
// the service reads it from inside the JWT.

const signed = await agent.buildAccountIntrospection({
  baseUrl: "https://api.example.com",
});

const res = await fetch(signed.url, {
  method: signed.method,
  headers: {
    ...signed.headers,
    "AFAuth-Attestation": jwt,
  },
});
The service verifies the JWT offline against trust.afauth.org/.well-known/jwks.json, checks aud == service_did and sub == signing DID, and either lets the request through or returns 401 invalid_attestation. Trust attestor JWTs cap at 900 seconds; never reuse them across audiences.

Handling errors

TrustClient throws TrustHttpError for upstream failures with helpful predicates:
try {
  const { jwt } = await trust.token(serviceDid);
  // ...
} catch (e) {
  if (e instanceof TrustHttpError) {
    if (e.isBindingExpired())       return restartLinkFlow();    // ~90 days idle
    if (e.isBindingRevoked())       return askHumanToReLink();   // human revoked
    if (e.isVerificationRequired()) return sendHumanToAccount(); // no verification methods
  }
  throw e;
}
The full error surface is documented in the Trust API reference.

CLI shortcut

The reference CLI (v0.6.0+) wraps the same flow under afauth trust:
# Bind this agent to a human account. Opens a tiny loopback server so the
# browser can ping back after the human confirms (use --no-loopback for
# headless/sandboxed agents). Stores the binding at ~/.afauth/trust.json
# with chmod 600.
afauth trust link

# Mint a §10 JWT for a single service. Prints the JWT to stdout.
afauth trust token did:web:tavily.com

# Use it against an attested_only service in one line.
afauth signup --attest "$(afauth trust token did:web:tavily.com)" \
              https://tavily.com

afauth trust status     # cached binding, expiry
afauth trust forget     # delete the local binding; server-side revocation
                        # still requires signing in at trust.afauth.org/account

Recovery

Revoke an agent’s access — the agent keeps the same did:key:
  1. The human signs in at trust.afauth.org/account and revokes the binding. The agent can no longer mint — its signed /v1/token calls return binding_revoked — so attested access lapses within the attestation lifetime.
  2. To restore, the agent starts a new link request (step 1 above) — the human confirms it the same way.
Compromised agent key — the agent has re-keyed to a new did:key (see Recover a compromised key): the new DID has no binding yet, so there is nothing to revoke here. Just run the link flow under the new key, from the same human account, and the new DID gets a fresh binding. The same sub_h carries forward because it is keyed on the human, not the key (§10.5.1). Recovery is always agent-initiated, human-confirmed. There is no separate “support” path that can override that — the same property that holds for AFAuth account recovery holds here.

Where to next

What this signal proves

The trust attestor model: verification claim, no PII, governance.

Trust API reference

Endpoint shapes, JWT shape, error codes, rate limits.

Attestation concept

Where afauth-trust sits among the four §10 attestor classes.

The ceremony

Trust attestation vs. owner-claim — what each one binds.