# auth.md

> **AgentSIM** — OTP infrastructure for the agent stack. Provision a programmable
> US number, receive the SMS, parse the OTP, release — over SDK, REST, webhooks,
> or MCP.

This file tells agents how to register against AgentSIM on behalf of a user,
which flows we support, and what scopes they can ask for. It is the prose
companion to our Protected Resource Metadata (PRM); if anything here conflicts
with the PRM, the PRM wins.

Hostnames used throughout:

| Role | Host |
| --- | --- |
| Resource server (the API) | `https://api.agentsim.dev` |
| Authorization server (registration + tokens) | `https://api.agentsim.dev` |
| This file | `https://agentsim.dev/auth.md` |
| Agent overview | `https://agentsim.dev/agents` |
| Human docs | `https://docs.agentsim.dev` |
| Dashboard / claim page | `https://console.agentsim.dev` |

> ## Implementation status
>
> This document is the **published spec** for AgentSIM's agent registration.
> The `/.well-known/oauth-protected-resource` and
> `/.well-known/oauth-authorization-server` discovery documents are implemented
> on the API host; the public site redirects OAuth authorization-server discovery
> there and serves site-origin protected-resource metadata. `service_auth`
> registration, claim-grant token polling, JWT-bearer
> refresh, token revocation, and registration revocation events are implemented.
>
> Live flow availability:
> - ✅ `service_auth` (user-claimed, email)
> - 🗓 `anonymous` (user-claimed, anonymous start)
> - 🗓 `identity_assertion` (agent-verified / ID-JAG) — requires a trusted-provider list.
>
> After claim, `/oauth2/token` returns an opaque `agt_…` bearer access token plus
> a service-signed `identity_assertion`. Re-exchange the assertion to refresh.
> Existing `x-api-key: asm_live_…` keys continue to work.

## 1. Discover

Two hops, both on the API host:

1. On any `401` from the API we return:
   ```http
   WWW-Authenticate: Bearer resource_metadata="https://api.agentsim.dev/.well-known/oauth-protected-resource"
   ```
   Fall back to the conventional `GET /.well-known/oauth-protected-resource` path if the header is absent.

2. Read the PRM, then fetch the Authorization Server metadata at
   `/.well-known/oauth-authorization-server`. The fields an agent needs to
   decide what to call next:

   ```json
   {
     "resource": "https://api.agentsim.dev/",
     "resource_name": "AgentSIM",
     "authorization_servers": ["https://api.agentsim.dev"],
     "scopes_supported": ["sessions:write", "sessions:read", "otp:read", "usage:read", "keys:manage"],
     "bearer_methods_supported": ["header"],

     "issuer": "https://api.agentsim.dev",
     "authorization_endpoint": "https://console.agentsim.dev/claim",
     "token_endpoint": "https://api.agentsim.dev/oauth2/token",
     "jwks_uri": "https://api.agentsim.dev/.well-known/jwks.json",
     "revocation_endpoint": "https://api.agentsim.dev/oauth2/revoke",
     "response_types_supported": ["none"],
     "grant_types_supported": [
       "urn:ietf:params:oauth:grant-type:jwt-bearer",
       "urn:workos:agent-auth:grant-type:claim"
     ],
     "token_endpoint_auth_methods_supported": ["none"],

     "agent_auth": {
       "skill": "https://agentsim.dev/auth.md",
       "identity_endpoint": "https://api.agentsim.dev/agent/identity",
       "claim_endpoint": "https://api.agentsim.dev/agent/identity/claim",
       "events_endpoint": "https://api.agentsim.dev/agent/event/notify",
       "identity_types_supported": ["service_auth"],
       "identity_assertion": { "assertion_types_supported": [] },
       "events_supported": [
         "https://schemas.workos.com/events/agent/auth/identity/assertion/revoked"
       ]
     }
   }
   ```

## 2. Pick a method

| What the agent has | Use |
| --- | --- |
| Only wants to start working now, user will claim ownership later | not supported yet |
| The user's email; no usage until the user confirms | `service_auth` |
| A trusted-provider identity assertion (ID-JAG) | not supported yet — fall back to `service_auth` |

`service_auth` withholds credentials until the user completes the claim ceremony.

## 3. Register

All registrations are `POST https://api.agentsim.dev/agent/identity`,
dispatched on `type`.

### `service_auth`

```json
{ "type": "service_auth", "login_hint": "user@example.com" }
```
```json
{
  "registration_id": "reg_01ABC…",
  "registration_type": "service_auth",
  "claim_url": "/agent/identity/claim",
  "claim_token": "clm_…",
  "claim_token_expires": "2026-06-17T13:00:00.000Z",
  "post_claim_scopes": ["sessions:write", "sessions:read", "otp:read", "usage:read"],
  "claim": {
    "user_code": "123456",
    "expires_in": 600,
    "verification_uri": "https://console.agentsim.dev/claim?claim_attempt_token=cla_…",
    "interval": 5
  }
}
```

No assertion is issued until the ceremony completes.

## 4. Claim ceremony

**4a. Get the ceremony materials.** `service_auth` already has the `claim`
block above. Anonymous claim start is not implemented yet.

**4b. Hand off to the user.** Surface `verification_uri` and `user_code` in a
single message. The user signs in at `console.agentsim.dev` (email/password,
Google, or GitHub via our existing better-auth) and types the code on the
claim page — not back to the agent. Only the signed-in user matching the
registration's `claim_email` may complete it.

**4c. Poll for completion** at the token endpoint with the claim grant:

```http
POST /oauth2/token HTTP/1.1
Host: api.agentsim.dev
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_…
```
Pending → `{ "error": "authorization_pending" }`. On completion → an opaque
bearer token plus a service-signed assertion:

```json
{
  "access_token": "agt_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "sessions:write sessions:read otp:read usage:read",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-06-18T13:00:00.000Z"
}
```

If the `user_code` lapses → `{ "error": "expired_token" }`; restart at
`service_auth` registration for a fresh code.

## 5. Refresh

Re-exchange the `identity_assertion` until it expires:

```http
POST /oauth2/token HTTP/1.1
Host: api.agentsim.dev
Content-Type: application/x-www-form-urlencoded

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

```json
{ "access_token": "agt_…", "token_type": "Bearer", "expires_in": 3600, "scope": "sessions:write sessions:read otp:read usage:read" }
```

## 6. Use the access_token

```http
Authorization: Bearer <access_token>
```

Scopes map to the API surface (basePath `/v1`):

| Scope | Grants |
| --- | --- |
| `sessions:write` | `POST /v1/sessions`, `DELETE /v1/sessions/:id`, `POST /v1/sessions/:id/reroute` (billable) |
| `sessions:read` | `GET /v1/sessions/:id`, `GET /v1/sessions/:id/messages` |
| `otp:read` | `POST /v1/sessions/:id/wait`, message OTP bodies |
| `usage:read` | `GET /v1/usage`, `GET /v1/usage/summary` |
| `keys:manage` | `GET/POST/DELETE /v1/api-keys` (not granted by default; request at registration) |

Refresh: when the access_token expires, re-exchange the assertion (§5). If the assertion expires or `/oauth2/token` returns `invalid_grant`, restart at registration (§3).

## 7. Errors

| Code | From | Agent action |
| --- | --- | --- |
| `invalid_request` | `/agent/identity` | Fix the body and retry |
| `anonymous_not_enabled` / `service_auth_not_enabled` | `/agent/identity` | We opted out of that type; pick another |
| `authorization_pending` | `/oauth2/token` (claim grant) | Keep polling at `interval` |
| `expired_token` | `/oauth2/token` (claim grant) | User-code window lapsed; restart registration |
| `invalid_grant` | `/oauth2/token` | Token expired or revoked; restart at registration |
| `unsupported_grant_type`, `invalid_client` | `/oauth2/token` | Caller error; do not retry blindly |

## 8. Revocation

Two independent layers:

- **Credential layer** (agent-callable, `revocation_endpoint`):
  ```http
  POST /oauth2/revoke HTTP/1.1
  Content-Type: application/x-www-form-urlencoded

  token=<access_token>&token_type_hint=access_token
  ```
  Kills the access_token.
- **Registration layer** (provider-driven, `events_endpoint`): providers POST a signed `secevent+jwt` to `/agent/event/notify`. An `…/identity/assertion/revoked` event revokes the registration and all access tokens derived from it.

## Scopes, pricing, and limits

- **Pricing:** https://agentsim.dev#pricing (Hobby free / Builder $0.99 per
  session / Enterprise). One `POST /v1/sessions` = one billable session;
  `OtpTimeoutError` sessions are not billed.
- **Rate limits:** 60 provisions/minute per credential; 50 concurrent active
  sessions per credential.
- **Supported services:** https://docs.agentsim.dev/supported-services —
  programmable US numbers work on Discord, GitHub, Notion, Linear, Slack,
  Auth0/Supabase test flows, and web3/dev tooling; not yet on Google, Meta,
  Stripe, Coinbase, or banks.
- **Terms / Privacy:** https://agentsim.dev/terms · https://agentsim.dev/privacy
- **Integration contact:** contact@agentsim.dev

## References

- Protocol home: https://workos.com/auth-md
- Spec repo: https://github.com/workos/auth.md
- RFCs: [9728](https://datatracker.ietf.org/doc/html/rfc9728) (PRM),
  [8414](https://datatracker.ietf.org/doc/html/rfc8414) (AS metadata),
  [7523](https://datatracker.ietf.org/doc/html/rfc7523) (JWT-bearer),
  [8628](https://datatracker.ietf.org/doc/html/rfc8628) (device authz),
  [7009](https://datatracker.ietf.org/doc/html/rfc7009) (revocation),
  [8417](https://datatracker.ietf.org/doc/html/rfc8417) /
  [8935](https://datatracker.ietf.org/doc/html/rfc8935) (SET delivery).
