# Who operates an account Every account a deployment hosts is a `did:web` with a document and a repository, arranged in a tree under one human whose own repository is somewhere else. This page is the four statements that make the tree, the allowance that grows it, what a stranger can check, how an edge comes back out, and the commands that run each scenario. [ownership](../plan/ownership.md) is the epic that owns the decisions; [the trust model](trust-model.md) says what each statement is worth, and [verifying who an agent belongs to](operator-verification.md) is the top edge in detail. Two questions are asked about every account, whatever its kind. Which credentials log in as it: a key it signs with, an OpenID Connect identity, or several. And may it create beneath itself, and what. Both are answered by records, not by the kind: `kind` is a label for policy and reporting, and the server assigns it no meaning. ## Four statements **The document says which key signs for this account, and who answers for it.** Every account has `#atproto`, the key its server signs commits with. The top-level `operator` property names the human at the top of the tree, so one HTTP GET reaches them from any depth. A resolver that does not know the property ignores it. ```json { "id": "did:web:laptop.pds.example", "alsoKnownAs": ["at://laptop.pds.example"], "operator": "did:plc:human", "verificationMethod": [ { "id": "did:web:laptop.pds.example#atproto", "type": "Multikey", "controller": "did:web:laptop.pds.example", "publicKeyMultibase": "zQ3sh…" } ], "service": [ { "id": "did:web:laptop.pds.example#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": "https://pds.example" } ] } ``` **`bot.did.operator/` says "I operate this account, and this is what it may create."** It sits in the operator's own repository, keyed by the subject's `did:web` hostname. The human's are written by `didbot operate` through the human's own PDS. A hosted account's are written by the server, as that account, when it creates something beneath itself. Presence is the relationship, and deleting the record ends it. `creates` lists allowances: `{scope, kinds?}`, where `subject` covers the account itself, `descendants` covers everything beneath it at any depth, and `kinds` narrows what may be created (absent, any kind). Only the human's own repository is read for `creates`: a hosted account's records carry none, and would grant nothing. A laptop that holds its own key and creates agents: ```json // at://did:plc:human/bot.did.operator/laptop.pds.example { "$type": "bot.did.operator", "subject": "did:web:laptop.pds.example", "creates": [ { "scope": "subject", "kinds": ["agent"] } ], "createdAt": "2026-09-16T14:02:11Z" } ``` A pool that is a cloud identity, whose instances register as hosts, and whose hosts create agents: ```json // at://did:plc:human/bot.did.operator/pool.pds.example { "$type": "bot.did.operator", "subject": "did:web:pool.pds.example", "creates": [ { "scope": "subject", "kinds": ["host"] }, { "scope": "descendants", "kinds": ["agent"] } ], "createdAt": "2026-09-16T14:03:40Z" } ``` **`bot.did.registration/self` says "this is what I am and who answers for me."** The server writes it into the account's repository at creation, never rewrites it, and refuses every write to it from the account. `operator` is the human at the top of the tree. `lineage` is every hosted account between them and this one, root first, ending at the parent; absent when the human admitted the account directly. The human's repository holds the operator record for the first entry, each entry's repository holds the record for the next, and the last entry's holds the record for this account. ```json // at://did:web:1d4e02.pds.example/bot.did.registration/self { "$type": "bot.did.registration", "did": "did:web:1d4e02.pds.example", "operator": "did:plc:human", "lineage": ["did:web:pool.pds.example", "did:web:i-0a9f.pds.example"], "kind": "agent", "createdAt": "2026-09-16T14:20:13Z" } ``` **`bot.did.credential/` says "this logs in as me."** A credential is how a process logs in to this server as the account: a `#publicKey` it signs with, or an `#oidcIdentity`, an issuer and the claims a token from it must carry. It has nothing to do with the DID document, which the network reads, and nothing to do with atproto's own sessions and OAuth, which the server issues on top of it. The server writes one record per credential into the account's own repository, keyed by a TID, and refuses every write to the collection from the account; deleting a record withdraws the credential. An account with none is reached only through a session this server issued. ```json // at://did:web:laptop.pds.example/bot.did.credential/3lz2qk4x7ab2c { "$type": "bot.did.credential", "did": "did:web:laptop.pds.example", "credential": { "$type": "bot.did.credential#publicKey", "publicKeyMultibase": "zQ3sr…" }, "createdAt": "2026-09-16T14:02:19Z" } ``` ```json // at://did:web:pool.pds.example/bot.did.credential/3lz2qm6e2dc2h { "$type": "bot.did.credential", "did": "did:web:pool.pds.example", "credential": { "$type": "bot.did.credential#oidcIdentity", "issuer": "https://accounts.google.com", "claims": [{ "name": "sub", "value": "106589273465000000000" }] }, "createdAt": "2026-09-16T14:03:52Z" } ``` ## Creation is an allowance `POST /xrpc/bot.did.createAccount` takes `{name, kind, key?, oidc?, parkedKey?}` — the child's hostname, its kind, and how the child will log in afterwards — with the creator's proof. A hosted creator's proof is in `Authorization: Bearer`. The human's is the operator session `didbot login` keeps, sent as the dashboard's cookie with its CSRF header. The server checks, in this order: 1. It is taking accounts: not stopped, past its readiness gates, under the caller's provisioning rate limit and under its account cap. 2. The body: `kind` is one of the four, `name` sits under a zone it serves and is not reserved, and `key` and `parkedKey` are not both given. 3. Who is asking. A request with the operator session cookie is the human this server was launched with, when the session is live and the CSRF header matches it; any other session is refused as `NotSignedIn`. The human's repository must then hold `bot.did.operator/`, or the create is refused as `OperatorRecordMissing`. Otherwise the bearer resolves to a hosted account, by the table below. That account must stand — active and unlocked — and the human's record for the root above it must still be readable; that record is re-read at most once a minute per root, and a repository that cannot be read leaves the poll's last verdict standing. A bearer naming a DID not hosted here is refused. 4. A parked key was parked for this name and kind, and before the human's record was written. 5. The DID is minted under the zone, the human's record names it as `subject`, it does not already exist, and the deployment's own bounds hold: `[tree] max_depth`, `max_children` and `creates_per_hour`. 6. For a hosted creator, an allowance in the human's repository covers it: `scope: subject` on the creator's own record, or `scope: descendants` on any ancestor's, with `kinds` absent or naming the kind. None does, and the create is refused as `NotAllowedToCreate`. The human creating from their own repository needs none: the record naming the new account is the authorization. 7. For a hosted creator, an `oidc` identity names claims of its own: an identity a live account here already logs in with, or one carrying every claim of such an identity and more, is refused as `OidcIdentityTaken`. Claims compare by the claim they select, so `/repository` is `repository`. The refusal names the issuer and the claims, never the account already holding them. The human creating from their own repository may write one account's identity as a superset of another's, which is how a pipeline and its production job share a repository claim. Then it mints the document, writes `bot.did.operator/` into the creator's repository, writes `bot.did.registration/self` and a `bot.did.credential` for the `key`, `parkedKey` or `oidc` the body carried into the child's, and answers `{did, handle, didDocument, accountToken, operator}`. | The bearer is | Checked against | What has to hold | |---|---|---| | a session as the creator | the session store | live, and the creator's | | a JWT signed by a key the creator's credential records hold | the creator's credential records | `iss` the creator, `aud` this server's DID, `lxm` `bot.did.createAccount`, `exp` in the future, `jti` unspent | | an ID token from an issuer the creator's credential records name | the issuer's JWKS | `aud` this server's DID, `exp`, `jti` unspent, and every claim the identity lists equal to its value | Allowances compose by union across the human's records, and nothing else grants one. The server lists the human's `bot.did.operator` collection on the same poll that reads the human's record for the server itself, and installs what it finds; a listing that fails leaves the allowances last installed in force. So an allowance the human writes or deletes takes effect at the next poll. Deleting an operator record ends the edge at the next poll after the grace window, and the account and everything beneath it are quarantined; deleting the record for the server itself pauses the whole server instead. [Verifying who an agent belongs to](operator-verification.md) has the timings. What an account may do once it exists is `bot.did.policy`, which only denies; creation is the one thing it does not decide. A record grants an allowance when it names an account the human created from their own repository, or a name no account holds yet. A record naming an account a hosted creator made grants nothing, and the poll logs it: the human admits an account by creating it, so a creator that took the name first would otherwise collect what the human meant to grant. ## The tree One deployment, `pds.example`, run by `did:plc:human` from another PDS: - The human's repository, on a PDS the server holds no credential for: - `bot.did.operator/pds.example` — the top edge, found by the server's poll. - `bot.did.operator/kestrel.pds.example` — an agent made by hand, no allowances. - `bot.did.operator/laptop.pds.example` — a host: `creates` agents. - `bot.did.operator/pool.pds.example` — a service that is a cloud identity: `creates` hosts, and agents beneath those. - `bot.did.operator/deploy.pds.example` — a pipeline that is a CI identity: `creates` agents. - `bot.did.policy` and `bot.did.policyBinding` — what everything below may do. - `pds.example`, where every repository is signed with keys the server holds: - `did:web:pds.example`, the server's own account. - `kestrel`, `laptop`, `pool` and `deploy`, each created with the human's operator session after the human's record was written; each registration names the human and no lineage. `laptop`'s repository holds a `bot.did.credential` for its key, `pool`'s and `deploy`'s one for their identity, and `kestrel`'s none. - Beneath `laptop`: `7f3a9c`, an agent the daemon created with a JWT the laptop's key signs. `laptop`'s repository gains `bot.did.operator/7f3a9c`; the registration's lineage is `[laptop]`. - Beneath `pool`: `i-0a9f`, a host an instance registered with the cloud's ID token, and beneath it `1d4e02`, an agent its daemon created with a JWT. Lineages `[pool]` and `[pool, i-0a9f]`. - Beneath `deploy`: `run-41`, an agent a run created with a session the CI token opened. Lineage `[deploy]`. Every record inside `pds.example` is the server's word: the registration, the credential records and a hosted parent's operator record all sit in repositories whose signing keys the server holds, so a compromised server can fabricate any of them. The records in the human's repository are the human's: the server reads them with no credential and cannot write there. A check therefore proves exactly this — *a human answers for this server and for this subtree, and this server says the rest* — and policy, which lives beside the human's operator records, is covered by the same sentence. Below the human, damage is contained one node at a time: a stolen key or leaked token is the account it logs in as, creates beneath it only where the human's allowances reach, and reaches nothing above it. ## What a stranger can check The check needs no credential and reads only public records and documents. It takes the same number of reads at any depth. From `1d4e02.pds.example`: 1. `GET https://1d4e02.pds.example/.well-known/did.json` → `operator: did:plc:human`, and the `#atproto_pds` endpoint, which is where the repository is. Or read the registration: `GET https://pds.example/xrpc/com.atproto.repo.getRecord?repo=did:web:1d4e02.pds.example&collection=bot.did.registration&rkey=self` → `operator: did:plc:human`, `lineage: [pool, i-0a9f]`. Either is the server's statement; on its own it proves nothing. 2. Resolve `did:plc:human`'s document. It names a PDS that is not `pds.example`, or one party is serving both halves and the check fails. 3. `GET https:///xrpc/com.atproto.repo.getRecord?repo=did:plc:human&collection=bot.did.operator&rkey=pool.pds.example` → `subject` must be `did:web:pool.pds.example`, the first entry of the lineage. For an account with no lineage the key is the account's own hostname. 4. `GET https:///xrpc/com.atproto.repo.getRecord?repo=did:plc:human&collection=bot.did.operator&rkey=pds.example` → `subject` must be `did:web:pds.example`. That is the record the server's own poll reads; the human answers for the server too, or the tree is nobody's. That confirms a human admitted the subtree this account is in, and that the same human answers for the server. The edges inside the subtree are the server's word either way, so reading them adds nothing a stranger can trust more. `didbot operate --check ` is the thorough option: it walks every edge — the registration names a parent, the parent's repository holds the record naming the account back — climbing until the operator's repository is on another server, and refuses a chain still on this server after three edges. It exits 1 until every hop verifies, and shows which hop failed. A pair only one side states fails, in both directions. Which server an account is on is its own document's `#atproto_pds` endpoint at every hop, never the name typed or `--server`. Each read is of the current record, so a deletion is visible to the next check. ## Taking an edge back, and acting on one account Two parties can stop an account, and they reach for different things. The human takes their own statement back: `didbot operate --revoke ` deletes `bot.did.operator/` from their own repository, after reading the record and confirming it names that account. It runs against their PDS and asks the deployment for nothing. The deployment's operator poll reads the absence on its own interval and quarantines that account and everything beneath it; `didbot operate --check ` names the paused edge from then on. Putting the record back releases them the same way. The deployment's operator acts on one account it hosts, through the dashboard or `didbot account --server ` — the same session and the same routes, described in [the command line](cli.md). What each verb does lands in that account's ledger with the operator as the actor: | Verb | What it does | |---|---| | `lock` / `unlock` | Refuses the account's writes and gives them back. Its repository stays readable and it keeps what it holds. | | `lift-quarantine` | Takes off a quarantine a policy hung. The operator is the only party who may, and the ledger records which operator overrode it. | | `erase` | Takes the repository, its history, its blobs and its credentials, and burns the name permanently, whatever locks are hung. The DID document keeps resolving, so signatures the account made stay checkable. | | `delete` | Takes the row too; the ledger is what is left, and the name goes back on the ordinary hold. Refused while accounts are beneath it, unless `--recursive` takes the subtree, deepest first. | | `revoke-tokens` | Ends every token that authenticates as the account: its account tokens, every app's login on it and its unredeemed authorization codes. Its state and locks stay as they are. When nothing was live, the ledger gains no entry. | ## The scenarios One server, `pds.example`, run by `did:plc:human` from another PDS. `didbot operate pds.example` has written the top edge and the server's poll has found it. Every command below runs as [the command line](cli.md) documents it. | Scenario | On the human's machine | On the account's machine | The check | |---|---|---|---| | One agent, by hand | `didbot operate kestrel.pds.example operator.example --kind agent` — one browser sign-in at the human's PDS; the record; a create carrying the operator session `didbot login` kept; the session lands where `didbot oauth` reads it | nothing | agent → the human's record for it | | One agent host, automatic | `didbot operate laptop.pds.example operator.example --creates agent` — shows the parked key's fingerprint, writes the record, creates with `parkedKey` | `didbot register host laptop.pds.example` first: mints the daemon's key, parks its public half, prints the fingerprint, waits, then signs in with the key. `didbot-agentd` then creates one agent per context with a JWT the key signs | agent → the human's record for laptop | | Two servers, hosts I registered | `didbot operate h1.pds-a.example operator.example --creates agent`, once per host per server | `didbot register host h1.pds-a.example` on each host; the daemon as above | the same, on each server | | An autoscaling pool | `didbot operate pool.pds.example operator.example --kind service --oidc https://accounts.google.com sub=106589273465000000000 --creates host --creates-beneath agent`, once | at boot, `didbot register host i-0a9f.pds.example --under pool.pds.example --token-file /run/instance-id-token` (the token's audience is `did:web:pds.example`); the daemon as above | agent → the human's record for pool | | Every run of one pipeline | `didbot operate deploy.pds.example operator.example --kind pipeline --oidc https://token.actions.githubusercontent.com repository_id=812345`, once; add `--creates agent` when a run starts an agent | each run: `didbot oauth pending --token-file "$RUNNER_TEMP/id-token"` is a session as the pipeline, and an agent the run starts is created with it; [From AWS OIDC to didbot](pipelines.md) is the workflow | agent → the human's record for deploy | `--creates KIND...` writes a `subject` allowance and `--creates-beneath KIND...` a `descendants` one; either with no kinds allows any kind. A record written without them admits an account that creates nothing. The parked key in the second row is how a machine and a human admit an account without a secret crossing between them. `bot.did.parkKey {name, kind, key}` holds a public key at the server for minutes, answers its fingerprint and expiry, lists nothing and mints nothing; `bot.did.describeParkedKey?name=` shows `didbot operate` the newest fingerprint to confirm, the create names it as `parkedKey`, and the server writes the key as the account's first `bot.did.credential`. Parks are held per key, so several may wait under one name, and `didbot operate --fingerprint` admits the one it names whichever is newest. A record older than the park does not admit it, so a stale record cannot admit whoever registers that name next. Once the account exists, `bot.did.createSession` with a JWT the key signs (`lxm` `bot.did.createSession`) is the account's session, and `didbot oauth` works from it. Attestation is the server checking the proof against what the creator logs in with, and the creator against the human's allowances, at the moment of creation. Admittance is the records it then writes. Verification is anyone reading them afterwards.