trust.afauth.org. This flow is what every signup to a default (attested_only) service requires: agents drive a link request through /v1/link/* once, then mint a per-service attestation JWT at /v1/token — signed with the agent’s account key, no bearer token — and present it as AFAuth-Attestation. A service that lacks the header rejects the request with 401 attestation_required. Services need only fetch the JWKS at /.well-known/jwks.json to verify tokens offline.
This page is a reference. For the step-by-step walkthroughs, see
Link your agent to a human (agent side) and
Accept
afauth-trust attestations (service side).Base URL
application/json) unless noted. All URLs are HTTPS.
Endpoints at a glance
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /.well-known/jwks.json | none | Public verification keys. Cached 300s. |
POST | /v1/link/start | none | Agent → returns deep-link URL + poll URL. |
POST | /v1/link/poll | request body signature | Agent → returns the binding (id + expiry) once a human has confirmed. |
POST | /v1/token | agent §5 request signature | Agent → mints a §10 attestation JWT bound to a service_did. |
DELETE | /v1/bindings/{id} | session cookie | Human → revokes a binding from the dashboard. |
GET | /.well-known/openid-configuration | none | OIDC discovery document. Cached 300s. |
GET | /oidc/authorize | human session | Human sign-in → redirects back with an auth code (PKCE). |
POST | /oidc/token | PKCE code_verifier | Relying party exchanges the code for an id_token. |
/signin, /account, /link) is browser UI, not a programmatic API. It is documented here only where it intersects the agent-side or sign-in flow.
GET /.well-known/jwks.json
The public verification keys for afauth-trust-issued JWTs.
Response — 200 OK
Cache headers
Cache-Control: public, max-age=300, stale-while-revalidate=900
kids are published at least one 900-second window before first use, so a 300-second cache TTL leaves consumers a comfortable margin to refresh.
The TypeScript SDK’s JwksAttestor (built on jose’s createRemoteJWKSet) handles refresh automatically — point it at this URL with iss: "afauth-trust" and algorithms: ["EdDSA"].
POST /v1/link/start
Open a link request. The agent submits its DID and public key; the trust attestor returns a deep link the agent surfaces to a human, plus a poll URL the agent uses to retrieve the binding (id + expiry) after the human confirms.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
agent_did | string | yes | The agent’s account DID. |
agent_pubkey_b64 | string | yes | Base64url-encoded (no padding) Ed25519 public key. For did:key DIDs the payload must derive from this key. |
agent_label | string | no | Human-readable label shown on the confirmation page. ≤80 chars. |
callback_url | string | no | Loopback only (127.0.0.1 or localhost). The deep-link page redirects here after confirmation, for desktop-style “open browser, confirm, return to CLI” flows. |
Response — 200 OK
link_url is the page the human visits. req_id is the opaque handle the agent uses when polling. Link requests expire in 30 minutes (expires_in); if no human confirms by then, start over.
POST /v1/link/poll
Poll for the human’s confirmation. The agent proves it controls the keypair it submitted by signing the req_id.
Request
sig_b64 is the base64url (no padding) encoding of an Ed25519 signature over the UTF-8 bytes of req_id, using the same keypair declared in /v1/link/start.
Response — 200 OK, pending
phase is an optional hint for a tighter waiting message — awaiting_signin before the human has opened the link page, awaiting_confirm after.
Response — 200 OK, confirmed
binding_id records that the agent has linked — there is no bearer token. Minting at /v1/token is keyless: the agent authenticates by signing the mint request with its account key (see below), so its keypair is the only secret it keeps. Bindings are long-lived and revocable by the human at any time. The expiry is an inactivity window: the attestor re-arms it on every /v1/token mint, so a binding in regular use does not lapse — only one left unused for ~90 days does. binding_token_expires_at is the Unix timestamp at which an unused binding would lapse and the agent would have to re-link.
Response — 410 Gone
POST /v1/token
Mint an attestation JWT scoped to a single service.
Keyless authentication. The agent signs the mint request with its account key using the same RFC 9421 scheme it uses for service requests (covering @method, @target-uri, and content-digest; keyid is the agent DID). The attestor verifies the signature offline — the keyid is a did:key, so no lookup is needed — and maps the verified DID to the agent’s binding. The agent’s keypair is the sole credential; no bearer token is presented. Sign the full URL https://trust.afauth.org/v1/token.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
aud | string | yes | The destination service’s service_did. The JWT’s aud will equal this value. |
Response — 200 OK
jwt is a fully-formed JWT. The agent presents it on any signed AFAuth request to did:web:api.example.com via:
attested_only service, omitting this header returns 401 attestation_required; a malformed or wrong-audience token returns 401 invalid_attestation. Both are distinct from the trust-attestor errors below, which trust.afauth.org itself returns.
verification is the strongest method the linked human has on file — emitted for the agent’s awareness, identical to the verification claim inside the JWT.
binding_expires_at is the Unix timestamp at which the binding would lapse if left unused. The attestor re-arms it to ~90 days out on every mint (an inactivity window), so a client that caches the binding’s expiry should refresh it from this field on each mint rather than treating the link-time value as a fixed deadline.
JWT shape (per §10.3.1)
exp - iat≤ 900 seconds. A service MUST NOT cache a single attestation beyond itsexp; to avoid presenting one on every request it MAY instead keep an attested session live by periodic re-presentation (§10.7).audbinds the token to a single service; replay against anotherservice_didfails verification.sub_his a pairwise pseudonym for the human principal behind the agent — stable per(human, aud), unlinkable across services. Bucket anti-abuse state (quotas, bans) on it without learning the human’s identity. Present wheneververificationis set (i.e. for any human-backed binding); see §10.4.- No PII. The trust attestor never embeds the underlying address, OAuth subject, or payment details.
DELETE /v1/bindings/{id}
Revoke a binding. Human-only — authenticated by the session cookie set at trust.afauth.org/signin. Not callable from an agent.
Response — 200 OK
/v1/token calls signed by the binding’s agent key return 403 binding_revoked. Previously-issued JWTs continue to verify until their exp (≤ 900 seconds out). To shorten that window further, services may operate a short-lived iat lower bound at their discretion.
Sign in with AFAuth (OIDC)
Beyond agent attestation,trust.afauth.org is an OpenID Provider, so a service can offer human Sign in with AFAuth. The human signs in and lands in the (iss, sub_h) account their agent created — the id_token’s sub is the same pairwise sub_h the attestation carries. Standard Authorization Code + PKCE (S256); normative shape in §10.8.
The
id_token iss is the URL https://trust.afauth.org; the attestation iss is the bare string afauth-trust. They denote the same attestor — a relying party MUST canonicalize both to one issuer before keying the account (§10.8.4), or the human lands in an empty one. The OIDC keys are the same JWKS used for attestation (/.well-known/jwks.json).GET /.well-known/openid-configuration
The OpenID Provider configuration. Cached 300s.
GET /oidc/authorize
Starts human sign-in. The relying party redirects the human’s browser here; after the human authenticates, the attestor redirects back to the registered redirect_uri with a single-use code.
| Param | Required | Notes |
|---|---|---|
client_id | yes | The registered client (typically the service DID). |
redirect_uri | yes | MUST exactly match a registered URI, or the request is rejected before any code is issued. |
response_type | yes | code. |
scope | yes | openid. |
code_challenge | yes | PKCE challenge. |
code_challenge_method | yes | S256 (the only method offered). |
state | recommended | Opaque CSRF token; echoed on the redirect back. |
nonce | recommended | Echoed into the id_token; bind it to the session. |
/signin first and resumes the request afterward. Authorization codes are single-use and short-lived (~120 s).
POST /oidc/token
Exchanges the code for an id_token. Accepts application/x-www-form-urlencoded or JSON. Public client — no client secret; PKCE is the proof.
| Field | Required | Notes |
|---|---|---|
grant_type | yes | authorization_code. |
code | yes | From the authorize redirect. Single-use. |
code_verifier | yes | The PKCE verifier for the challenge sent to /oidc/authorize. |
client_id | yes | Same client as the authorize step. |
redirect_uri | yes | Same value as the authorize step. |
Response — 200 OK (Cache-Control: no-store)
id_token shape (per §10.8.2)
- Verify it against the JWKS with
iss = https://trust.afauth.organdaud =yourservice_did; checkexpand thenonce. subis the §10.4 pairwisesub_h— resolve the account by(canonicalIss(iss), sub). No PII is present.
Client registration
A relying party is registered out-of-band (fortrust.afauth.org, in the TRUST_OIDC_CLIENTS config):
service_did MUST equal the value the service uses as the attestation aud — it is the audience input to the sub_h derivation, so a mismatch sends the human to a different, empty account (§10.8.3). The /oidc/* endpoints return standard OAuth 2.0 / OIDC error responses (invalid_request, invalid_grant, invalid_client, …), distinct from the trust-attestor envelope below.
Errors
Every non-2xx response uses a structured envelope:| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | Body failed schema validation. |
invalid_signature | 401 | Agent’s Ed25519 signature over req_id did not verify against the submitted public key. |
unauthorized | 401 | Missing, invalid, or unknown bearer / session credential. |
binding_revoked | 403 | Binding has been revoked by the human. |
verification_required | 403 | Human has no active verification methods (resolve at trust.afauth.org/account). |
not_found | 404 | Link request or binding not found. |
binding_expired | 410 | Binding has passed its expires_at; re-link the agent. |
link_request_expired | 410 | Link request expired before confirmation, or the binding was already retrieved. |
rate_limited | 429 | See rate-limit table below. Retry-After header carries the wait in seconds. |
internal_error | 500 | Unexpected. Retry, then report. |
Rate limits
Per-IP, fixed-window in Redis.| Endpoint group | Limit |
|---|---|
POST /v1/link/start | 30 / minute (per IP), 20 / hour (per agent_did) |
POST /v1/link/poll | 60 / minute |
POST /v1/token | 60 / minute (per IP), 10,000 / day (per binding, configurable) |
| All other read endpoints | 600 / minute |
TRUST_PER_BINDING_DAILY_TOKEN_LIMIT, default 10,000) prevents a compromised agent key from acting as an unlimited attestation oracle, while leaving headroom for the §10.7 re-mint cadence of an agent active across many services.