Skip to main content
The trust attestor exposes a small HTTP surface at 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

All requests and responses are JSON (application/json) unless noted. All URLs are HTTPS.

Endpoints at a glance

The human-facing surface (/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
Per §10.3.1, new 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

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

Poll again after a backoff (1–2 seconds is fine; the endpoint is rate-limited at 60/minute per IP). 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

Also returned if a confirmed binding has already been retrieved (the binding info is delivered exactly once).

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

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:
Against an 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 its exp; to avoid presenting one on every request it MAY instead keep an attested session live by periodic re-presentation (§10.7).
  • aud binds the token to a single service; replay against another service_did fails verification.
  • sub_h is 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 whenever verification is 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

After revocation, all subsequent /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. If the human has no trust session, the attestor bounces to /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.

Response — 200 OK (Cache-Control: no-store)

id_token shape (per §10.8.2)

  • Verify it against the JWKS with iss = https://trust.afauth.org and aud = your service_did; check exp and the nonce.
  • sub is the §10.4 pairwise sub_h — resolve the account by (canonicalIss(iss), sub). No PII is present.

Client registration

A relying party is registered out-of-band (for trust.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:
See Error codes for the envelope contract shared with the rest of the AFAuth surface.

Rate limits

Per-IP, fixed-window in Redis. The per-binding daily cap (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.