Which front door is this? Build an agent is for a roaming agent product. Accept AFAuth is your backend. Run a coding agent is an end user equipping Claude Code. This page is where two roles fuse: a service that distributes a client is both agent and service — your CLI consumes the agent SDK, your server consumes the server SDK.
The shape
Your CLI plays the agent role against your own backend. The user types your product’s verbs —acme deploy, acme login — and AFAuth stays invisible plumbing: the client holds a did:key and signs each request. One call provisions it, and @afauthhq/agent’s signup() adapts to whatever attestation mode your service advertises — so the same client code works whether or not you gate signup.
Provision in one call
signup() fetches your discovery doc, links to a human only if your service is attested_only (and only the first time on a machine), then sends the signed implicit-signup request and returns the binding to persist.
Load or create the identity
loadOrCreateAgent() reads the shared agent home, so if the user already ran afauth init (or any other AFAuth-aware tool), you reuse that identity — and any human link it already has. The user links at most once, ever, across every AFAuth service.Sign up
signup() is mode-agnostic: against an optional service onLink never fires; against attested_only it fires once, then the saved binding is reused.examples/cli; artidrop’s CLI is a live, deployed example.
Pick your attestation mode
This is the one decision that shapes the user experience — and it’s yours to make by economics. Both modes are first-class:attested_only— a human links once at the trust attestor before the agent gets anything. Choose it when your free tier is expensive (compute, storage, inference — anything costly you give away per user): you want a real person on the hook before granting it, so a flood of throwaway keys can’t drain you. The CLI’s first signup then includes the one-timeonLinkapproval (reused across every AFAuth service thereafter).optional— the agent signs up with just its key: no attestor, no browser. Choose it when free resources are cheap and content moderation + graded rate limits are enough anti-abuse. You can grade trust — give unclaimed keys a lower quota/rate than claimed, human-owned accounts.
signup() handles both, so your CLI code is identical either way — only whether onLink fires changes. The mode is a server-side switch; see Accept AFAuth and Attestation.
The login ladder — and no merge
A distributed client usually wants three tiers, and the cleanest mental model is a ladder the user climbs only when they want more:| Tier | How | What it is |
|---|---|---|
| Throwaway | acme publish --anonymous | keyless, unlinkable; can’t be managed |
| Guest / agent | default (optional) or after the one-time link (attested_only) | a real identity that owns and manages its own work |
| Account | acme login | a human-owned account: dashboard, recovery, cross-device |
login the single account verb, and have it fork:
- Claim — turn the current identity into an account in place (your existing owner-invitation/claim ceremony). Keeps everything.
- Sign in to an existing account — switch this machine to it. The identity’s prior work stays with the key — it is not merged.
credential-already-in-use and refuses; consumer apps warn “guest progress may not transfer.” Your claim ceremony already enforces this: claiming into an email that already has an account returns a 409 — that is the guard. Make the user choose at the fork; don’t reconcile two pools of data.
Logout: go dormant, don’t delete
The ladder has a step down that most clients get wrong. What shouldacme logout do to a self-provisioned identity?
The wrong answer is rm ~/.afauth/key.json. That key is shared across every AFAuth-aware tool on the machine, and it owns the guest’s work — delete it and you orphan their artifacts and sign their other tools out. Logging out is about no longer using an identity, not destroying it. Possession ≠ use.
So make logout write a dormant marker, not erase anything:
- Drop any stored bearer key and persist a
signed_outflag. The client now resolves to anonymous — it signs nothing — until an explicitlogin. - Leave
~/.afauth/untouched.acme login --afauthlifts the marker and the same did:key is active again, intact, still owning its artifacts.
| Order | Resolves to | Trigger |
|---|---|---|
| 1 | anonymous | --anonymous (per-call escape hatch) |
| 2 | bearer | explicit key arg → $ACME_API_KEY → stored key |
| 3 | anonymous | sticky logout marker |
| 4 | afauth | default — sign as the did:key |
logout. For the same reason, when logout runs with $ACME_API_KEY set, say so — you can’t unset their environment, and that key keeps forcing bearer auth until they do.
Don’t rebrand the plumbing
The user knows your product, not AFAuth:- The account verb is
acme login(or fold it into your flow) — neverafauth init. Sending a user to a second, unfamiliar tool is friction at the worst moment. - Don’t mirror AFAuth’s subcommands (
acme trust link, …). Fold the human step intologin/signup; the user’s vocabulary stays at your verbs. - Name “AFAuth” at most once, as reassurance. The one unavoidable AFAuth-branded moment is the trust attestor’s approval page — and only for
attested_onlyservices. Pass a clearlabelso it shows what’s being linked.
~/.afauth/, afauth trust status / forget operate on your CLI’s identity with no extra surface from you.
Notes & gotchas
- Pin
@afauthhq/agent@^0.6.1. (0.6.0 fails to load on a clean install — it re-exported a helper from a@afauthhq/corethat hadn’t shipped it.) - Signing multipart uploads:
fetchserializes aFormDatawith a boundary you can’t see up front, so a pre-computed Content-Digest won’t match. Serialize it yourself first, then sign over the bytes:Signing JSON bodies needs none of this — pass the JSON string straight tosignRequest. - Auth resolution in your request layer: an explicit key → bearer; else a stored/env key → bearer; else sign with the did:key;
--anonymous→ no auth. - Tests: point
$AFAUTH_HOMEat a temp dir so signing never touches a developer’s real~/.afauth.
Next steps
Accept AFAuth on your service
The backend half — verify signed requests, host the claim page, pick your attestation mode.
@afauthhq/agent
signup(), the /node persistence helpers, and the signing surface.Attestation
attested_only vs optional + graded trust — and when each fits.Invite and claim
The deferred human-ownership ceremony your
login claim path drives.