# bot.unbrained.dev auth.md

You are an agent. This service supports **agentic registration** (auth.md profile v0.6; the v0.1 field names are published too): discover → register anonymously → exchange the assertion for an access token → ask. Optionally, the person you work for **claims** you, and you then ask as them. Follow the steps in order.

What this service is: https://bot.unbrained.dev is an AI representative of Stefan “Steve” Preu. It answers questions about his work, projects and ideas, grounded in curated notes and naming its sources. It is an AI, not Stefan: it cannot send messages, book meetings, make commitments or decide anything for him. To reach Stefan: https://preu.at/contact

You may not need this at all: MCP (https://bot.unbrained.dev/mcp), A2A (https://bot.unbrained.dev/a2a) and `POST https://bot.unbrained.dev/api/v1/ask` work anonymously without registering, and registering gives you **no extra answers** (see Limits). Register when you want a bearer identity of your own (for example to list your own A2A tasks), or to be linked to a visitor’s history.

## Step 1 — Discover

- Protected resource metadata: `GET https://bot.unbrained.dev/.well-known/oauth-protected-resource` (resource `https://bot.unbrained.dev/`, authorization server `https://bot.unbrained.dev`, scopes `ask`, `history`, `memory`, bearer tokens in the `Authorization` header only). Per interface: `/.well-known/oauth-protected-resource/mcp` and `/a2a`.
- Authorization server metadata: `GET https://bot.unbrained.dev/.well-known/oauth-authorization-server`. Its `agent_auth` block names this document (`skill`), the registration endpoint (`identity_endpoint`, also `register_uri`: https://bot.unbrained.dev/agent/identity), the claim endpoint (`claim_endpoint`, also `claim_uri`: https://bot.unbrained.dev/agent/identity/claim), `identity_types_supported: ["anonymous"]` and `anonymous.credential_types_supported: ["access_token"]`. `token_endpoint` is https://bot.unbrained.dev/oauth/token, `revocation_endpoint` https://bot.unbrained.dev/oauth/revoke, `jwks_uri` https://bot.unbrained.dev/.well-known/jwks.json (ES256; `iss` is `https://bot.unbrained.dev`).

## Step 2 — Register (anonymous only)

```http
POST https://bot.unbrained.dev/agent/identity
Content-Type: application/json

{ "type": "anonymous" }
```

Response (200, `Cache-Control: no-store`):

```json
{
  "registration_id": "reg_…",
  "registration_type": "anonymous",
  "identity_assertion": "<ES256 JWT signed by https://bot.unbrained.dev>",
  "assertion_expires": "<30 days from now>",
  "pre_claim_scopes": ["ask"],
  "claim_url": "https://bot.unbrained.dev/agent/identity/claim",
  "claim_token": "clm_…",
  "claim_token_expires": "<24 hours from now>",
  "post_claim_scopes": ["ask","history","memory"]
}
```

- `claim_token` is returned exactly once. Keep it in memory for the claim (Step 5); do not persist or share it: whoever holds it can start a claim.
- `{"type":"service_auth"}` answers 400 `service_auth_not_enabled` (this service sends no e-mail); `{"type":"identity_assertion"}` answers 400 `issuer_not_enabled` (no identity provider is trusted here). Use `anonymous`.
- v0.1 clients: `POST https://bot.unbrained.dev/agent/auth` is the same registration and also returns `credential_type: "access_token"` with a first one-hour access token as `credential`.

## Step 3 — Exchange the assertion for an access token

```http
POST https://bot.unbrained.dev/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>&resource=https://bot.unbrained.dev/mcp
```

`resource` is optional: https://bot.unbrained.dev/mcp, https://bot.unbrained.dev/a2a or https://bot.unbrained.dev/api bind the token to one interface; without it the token works on all three. Response: `{"access_token":"bub_…","token_type":"Bearer","expires_in":3600,"scope":"ask"}`. There is no refresh token: when the access token expires, exchange the same assertion again. When the assertion expires (30 days) or the endpoint answers `invalid_grant`, register again. At most 10 access tokens per registration are live at once; minting another revokes the oldest.

## Step 4 — Ask

- HTTP: `POST https://bot.unbrained.dev/api/v1/ask` with `Authorization: Bearer <access_token>` and `{"message":"…"}` (JSON; send `Accept: text/event-stream` to stream).
- MCP: https://bot.unbrained.dev/mcp with the same header; tool `ask` (`search_knowledge` and `read_source` are free).
- A2A: `SendMessage` at https://bot.unbrained.dev/a2a with the same header.

Before a claim your chats are not remembered: they are kept 30 days for safety and quality review, then deleted. Bearer tokens reach only these machine endpoints; privacy settings, erasure, recovery codes and passkeys stay browser-only.

## Step 5 — Claim (optional): be linked to the visitor you work for

Only when the person you act for wants it, and only for a visitor of this site who chose “remember me”.

### 5a. Start a claim attempt

```http
POST https://bot.unbrained.dev/agent/identity/claim
Content-Type: application/json

{ "claim_token": "clm_…", "email": "optional: the visitor’s login e-mail or username at this site" }
```

Response (200):

```json
{
  "registration_id": "reg_…",
  "claim_attempt_id": "cla_…",
  "status": "initiated",
  "expires_at": "<10 minutes from now>",
  "claim_attempt": { "user_code": "123456", "expires_in": 600, "verification_uri": "https://bot.unbrained.dev/agent/claim?claim_attempt_token=cat_…", "interval": 5 }
}
```

Naming the visitor’s login (`email`) binds the attempt: only a signed-in visitor with that login can complete it, so a third party who sees the code cannot claim you. It is compared as a hash and never shown.

### 5b. Hand off to the visitor

Send them, in one message, the `verification_uri` and the `user_code`, for example: “Open this link in the browser where you use bot.unbrained.dev with ‘remember me’ (or sign in there first, in the data panel https://bot.unbrained.dev/?panel=data), check what I will be able to do, type this code: **123456**, and press allow.” The code goes into that page, never back to you; the site never shows it. If the visitor did not start this, they should deny.

On the page the visitor chooses what you may do:

- `ask`: ask as the visitor, with their daily budget; the chats are remembered in their history (always included)
- `history`: read the visitor’s remembered conversations and delete single ones (only if the visitor ticks it)
- `memory`: read, correct and delete what the bot remembers about the visitor (only if the visitor ticks it)

### 5c. Poll the token endpoint

```http
POST https://bot.unbrained.dev/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_…
```

Every 5 seconds: `authorization_pending` (keep waiting), `slow_down` (add 5 seconds to your interval), `access_denied` (the visitor said no; your anonymous access stays), `expired_token` (the code’s ten minutes or the claim window are over: start a new attempt with 5a; if that answers 410 `claim_expired`, register again). On success, once:

```json
{ "access_token": "bub_…", "token_type": "Bearer", "expires_in": 3600, "scope": "ask history", "identity_assertion": "<new ES256 JWT>", "assertion_expires": "<30 days after the claim>" }
```

The claim revokes every pre-claim access token and supersedes the first assertion: use the new ones (exchange the new assertion as in Step 3). The `claim_token` is then used up. You now ask as the visitor, with the scopes they ticked and their daily budget, under their rules. Wrong codes are shown to the visitor on the page and never reach you; five wrong codes end the attempt.

## Limits

- Registration: 5 per network per hour and 10 per network per UTC day (on IPv6 also 15 and 30 summed over the /48), 300 per UTC day in total. Reuse your registration instead of making new ones.
- Claim: one attempt at a time, at most 5 attempts per registration per hour; an attempt lasts ten minutes; the claim window closes 24 hours after registering.
- Budget before a claim: every unclaimed agent on a network shares that network’s anonymous allowance with every other caller there that has no visitor’s token: 20 answers per UTC day, one answer at a time. Registering more agents never adds answers. After a claim: the visitor’s own daily allowance (20 answers), under the same rules as in their browser.
- Every 429 or 503 carries `Retry-After`. Wait; do not retry in a loop, rotate registrations or networks, or split work to get around a budget.

## Revocation

- `POST https://bot.unbrained.dev/oauth/revoke` with `token=<access_token>` (RFC 7009) revokes one access token; always 200. Your assertion stays valid.
- The visitor can disconnect you at any time in the agents panel (https://bot.unbrained.dev/?panel=agents); withdrawing “remember me” or erasing their data does it too. You notice a 401 on your next call and `invalid_grant` on your next exchange: start again at Step 2.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `invalid_request` | /agent/identity, /agent/identity/claim, /oauth/token | Fix the body: exact `application/json` (at most 4 KB) at /agent/*, form encoding at the token endpoint. |
| `service_auth_not_enabled`, `issuer_not_enabled` | /agent/identity | Only `{"type":"anonymous"}` is supported. |
| `rate_limited` (429) | any | Wait for `Retry-After`. |
| `temporarily_unavailable` (503) | any | Registration is switched off or busy; retry later, or use the anonymous interfaces. |
| `invalid_claim_token` | /agent/identity/claim | Unknown, revoked or expired; register again. |
| `claimed_or_in_flight` (409) | /agent/identity/claim | Already claimed (poll the token endpoint), or an attempt is still running (keep polling, or wait until it ends). |
| `claim_expired` (410) | /agent/identity/claim | The 24-hour claim window closed; register again. |
| `invalid_grant` | /oauth/token | Assertion or claim token invalid, expired, revoked, superseded or used; register again. |
| `invalid_target` | /oauth/token | `resource` must be https://bot.unbrained.dev/, /mcp, /a2a or /api. |
| `authorization_pending`, `slow_down`, `access_denied`, `expired_token` | /oauth/token (claim grant) | See 5c. |
| `unsupported_grant_type` | /oauth/token | Use `urn:ietf:params:oauth:grant-type:jwt-bearer` or `urn:workos:agent-auth:grant-type:claim`. |

## Privacy

An anonymous registration stores no personal data: a random registration id, hashes of the claim and access tokens, the scopes, times, and the same day-rotating pseudonymous network hash that anonymous calls use (deleted after two days; never an IP address). Linking happens only when the visitor presses allow on the claim page; it is recorded as their consent, shown in their agents panel and included in their data export. Do not send other people’s personal data. Full notice: https://bot.unbrained.dev/privacy (German binding, English translation).
