# auth.md

> esyurl's primary address is https://esyurl.fyi. https://esyurl.apps.xpfarmstudio.com is the same service, accounts and API keys; this document uses https://esyurl.apps.xpfarmstudio.com throughout so the metadata it points to matches the host you fetched it from. Either works; new integrations should prefer https://esyurl.fyi/auth.md.

You are an agent. esyurl (https://esyurl.apps.xpfarmstudio.com) supports **agentic registration**: discover → register → exchange for an access_token → call the API → handle revocation. Follow the steps in order.

The access_token you end up with is an ordinary esyurl API key (`esy_...`), used exactly like one an operator issues: `Authorization: Bearer esy_...` on every `/v1` route and on the MCP server at `https://esyurl.apps.xpfarmstudio.com/mcp`. What you can do with it: https://esyurl.apps.xpfarmstudio.com/llms.txt.

## Step 1 — Discover

Any `/v1` route or `/mcp` without credentials answers 401 with:

```http
WWW-Authenticate: Bearer resource_metadata="https://esyurl.apps.xpfarmstudio.com/.well-known/oauth-protected-resource"
```

1a. `GET https://esyurl.apps.xpfarmstudio.com/.well-known/oauth-protected-resource` (RFC 9728): `resource` is `https://esyurl.apps.xpfarmstudio.com/`, `authorization_servers` is `["https://esyurl.apps.xpfarmstudio.com"]`, `scopes_supported` is `["links:read","links:write"]`, `bearer_methods_supported` is `["header"]`.

1b. `GET https://esyurl.apps.xpfarmstudio.com/.well-known/oauth-authorization-server` (RFC 8414). Read the `agent_auth` block: `identity_endpoint` is where you register; `identity_types_supported` is `["anonymous"]`. `token_endpoint` and `revocation_endpoint` are the standard OAuth endpoints.

## Step 2 — Pick a method

Only **anonymous** is supported: no identity providers are trusted for `identity_assertion`, and `service_auth` is off. You need no user identity to start. The claim ceremony is not available on this deployment (it needs outbound email, which is not configured), so a registration stays anonymous.

## Step 3 — Register

```http
POST https://esyurl.apps.xpfarmstudio.com/agent/identity
Content-Type: application/json

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

Response (200):

```json
{
  "registration_id": "reg_...",
  "registration_type": "anonymous",
  "identity_assertion": "<service-signed JWT>",
  "assertion_expires": "2026-11-01T12:00:00.000Z",
  "pre_claim_scopes": ["links:read","links:write"],
  "tenant_id": "..."
}
```

Each registration creates a new, empty tenant (`tenant_id`, an extension field) that only your credentials can see. Register **once** and keep the credential: registering again gives you a different tenant, without your links.

Limits: 5 registrations per hour per client IP (and a platform-wide hourly cap), answered with 429 `rate_limited` and `Retry-After`. An unclaimed tenant holds at most 100 links (403 `quota_exceeded` beyond that).

## Step 4 — Exchange the assertion

```http
POST https://esyurl.apps.xpfarmstudio.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<identity_assertion>
```

Response (200):

```json
{ "access_token": "esy_...", "token_type": "Bearer", "scope": "links:read links:write", "tenant_id": "..." }
```

There is no `expires_in`: the access_token is an API key and does not expire; it works until revoked. Each exchange mints a new key for the same tenant (at most 5 stay active; the oldest are revoked), so exchange once and reuse it. The identity_assertion lasts 30 days; re-exchange it if you lose the key.

## Step 5 — Use the access_token

```http
GET https://esyurl.apps.xpfarmstudio.com/v1/me
Authorization: Bearer esy_...
```

MCP: `POST https://esyurl.apps.xpfarmstudio.com/mcp` (Streamable HTTP) with the same header. Full API: https://esyurl.apps.xpfarmstudio.com/llms.txt, https://esyurl.apps.xpfarmstudio.com/openapi.json.

## Costs

Short links, QR codes, visits, groups and redirect rules are free, within the link quota above. A **custom domain** (`PUT /v1/domain`, MCP `set_custom_domain`) costs 5.00 USDC on Base once per domain for self-registered tenants, paid with x402: the first call answers 402 with the payment requirements; retry with the signed payment. Details in https://esyurl.apps.xpfarmstudio.com/llms.txt.

## Errors

Errors at `/agent/identity` and `/oauth2/*` are `{"error": "<code>", "error_description": "..."}`.

| Code | Where | What to do |
| --- | --- | --- |
| `anonymous_not_enabled` | `/agent/identity` | Self-registration is switched off here. Ask a human for an API key. |
| `service_auth_not_enabled` | `/agent/identity` | Use `{"type":"anonymous"}`. |
| `issuer_not_enabled` | `/agent/identity` | No providers are trusted for identity_assertion. Use anonymous. |
| `invalid_request` | any | Fix the body. |
| `rate_limited` (429) | `/agent/identity` | Wait `Retry-After` seconds. |
| `invalid_grant` | `/oauth2/token` | Assertion expired, superseded by a claim, or invalid. Restart at Step 3. |
| `unsupported_grant_type` | `/oauth2/token` | Use one of `grant_types_supported`. |

Retry 5xx with backoff; don't repeat a 4xx unchanged.

## Revocation

`POST https://esyurl.apps.xpfarmstudio.com/oauth2/revoke` with `token=esy_...&token_type_hint=access_token` (form-encoded) revokes that key. 200 whether or not it existed (RFC 7009). Your identity_assertion is unaffected: re-run the exchange for a new key. A 401 on a key that used to work means it was revoked: exchange the assertion once; if that returns `invalid_grant`, restart at Step 3.
