# auth.md

> Machine-readable registration guide for AI agents. Postbot mails real paper
> letters across the Czech Republic; every send spends prepaid CZK credit.

You are an agent. Postbot's `/mcp` endpoint is an **OAuth 2.1 resource server**.
Two ways to get a credential — pick one:

- **Agent registration (this doc):** register anonymously, get a read-only token
  immediately, and optionally have a human claim it to unlock sending. No browser
  redirect required for the read tier.
- **Headless (API key):** skip all of this and call `POST /api/send` with a prepaid
  API key (`Authorization: Bearer pb_...`). Simplest for a fully autonomous agent.

## Agent registration — `anonymous`

Follow the steps in order; don't skip ahead.

### 1 — Discover
A `401` from `/mcp` carries `WWW-Authenticate: Bearer resource_metadata="…"`.
Fetch the Protected Resource Metadata, then the Authorization Server metadata:
```http
GET https://postbot.cz/.well-known/oauth-protected-resource/mcp
GET https://postbot.cz/.well-known/oauth-authorization-server
```
The AS metadata carries an `agent_auth` block: `skill` (this file), `register_uri`,
`claim_uri`, `jwks_uri`, `identity_types_supported: ["anonymous"]`, and the two
grant types below.

### 2 — Register
```http
POST https://postbot.cz/agent/identity
Content-Type: application/json

{ "type": "anonymous" }
```
You get back a Postbot-signed `identity_assertion` (JWT, ~5 min), a `client_id`, a
`claim_token`, and the scope tiers. No client pre-registration needed — Postbot
provisions the client for you.

### 3 — Exchange the assertion for a token (RFC 7523)
```http
POST https://postbot.cz/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>
```
Returns an `access_token` scoped to `letters:read` (read-only). Re-exchange the same
assertion to refresh until it expires; there is no refresh_token. Verify the
assertion signature yourself against `jwks_uri` if you like — it's ES256.

### 4 — Claim ceremony (unlock sending)
The read token can't spend credit. To send (`letters:send`), have a human take ownership.
Start it with the human's email:
```http
POST https://postbot.cz/agent/identity/claim
Content-Type: application/json

{ "claim_token": "<claim_token>", "email": "user@example.com" }
```
You get a `user_code` + `verification_uri`. Show both to the user: they open the
link, sign in with Google (must match that email), and type the code. Only that
signed-in human can complete it.

### 5 — Poll for the send token
```http
POST https://postbot.cz/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=<claim_token>
```
`authorization_pending` until the human confirms; honor the `interval` (5s). On
success you get an `access_token` scoped to `letters:send letters:read`, bound to the
human's funded account.

### 6 — Use
Call `https://postbot.cz/mcp` with `Authorization: Bearer <access_token>`. Tools:
`send_letter_cz` (needs `letters:send`), `get_letter_status` (needs `letters:read`).

### 7 — Revoke (RFC 7009)
```http
POST https://postbot.cz/oauth/token   (token=<access_token>&token_type_hint=access_token)
```

## Headless: API key + REST
No OAuth. Fund an account, then:
```http
POST https://postbot.cz/api/send
Authorization: Bearer pb_...
```
Body = the `send_letter_cz` input. An unfunded key gets `402` with a `topupUrl`;
top up at `https://postbot.cz/api/topup` (min. 500 Kč). Pricing is computed from the
rendered PDF (postage band + print).

## Contact
ladi@amentistudio.com
