# auth.md

You are an agent. This service lets a person give you access to their Moatlight account. There are two ways: **agent identity** (`service_auth`: you name the person's email, they type a short code) and **OAuth 2.0 with PKCE** (they approve in a browser you send them to). Follow the steps in order.

Moatlight measures whether ChatGPT and Google AI Overview name a business. You need a login only for reports and account data; the public endpoints and the sandbox (https://moatlight.com/api/v1/sandbox) need none.

## Step 1 — Discover

### 1a. Fetch the Protected Resource Metadata

```http
GET https://moatlight.com/.well-known/oauth-protected-resource
```

It lists `resource`, `authorization_servers`, `scopes_supported` and `bearer_methods_supported`. You can also learn it from any 401: the `WWW-Authenticate: Bearer resource_metadata="https://moatlight.com/.well-known/oauth-protected-resource"` header points to it (RFC 9728). Probe: `GET https://moatlight.com/api` answers 401 with that header.

### 1b. Fetch the Authorization Server metadata

```http
GET https://moatlight.com/.well-known/oauth-authorization-server
```

(The issuer is `https://moatlight.com`, the site origin, so the document is at the standard RFC 8414 location. The endpoints themselves live under `https://moatlight.com/api/auth`.) It lists `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `revocation_endpoint`, `scopes_supported` and `code_challenge_methods_supported`.

It also has an `agent_auth` block: `skill` (this file), `identity_endpoint` (https://moatlight.com/agent/identity), `claim_endpoint` (https://moatlight.com/agent/identity/claim) and `identity_types_supported` (`["service_auth"]`). There is no `events_endpoint`: it only serves `identity_assertion`, which is not offered.

## Step 2 — Pick a method

| Method | Supported |
|---|---|
| `service_auth` (you know the person's email; they type a code) | **Yes.** Use this when you cannot receive a browser redirect. |
| OAuth 2.0 authorization code with PKCE (S256), open client registration | **Yes.** Use this when you can (MCP clients, apps with a callback URL). |
| `identity_assertion` (ID-JAG, `urn:ietf:params:oauth:token-type:id-jag`) | No |
| `anonymous` | No. There is no anonymous account; use the public endpoints or the sandbox instead. |

A person must be involved once either way: they sign in and approve. A payment (the Early Access pass) also needs a person in a browser; you cannot pay.

## Step 3 — Register

### Method A: service_auth

```bash
curl -X POST https://moatlight.com/agent/identity \
  -H 'Content-Type: application/json' \
  -d '{"type":"service_auth","login_hint":"person@example.com","scope":"account:read reports:read"}'
```

`login_hint` is the email of the Moatlight account. `scope` is optional (default: all scopes below); ask only for what you need. The answer:

```json
{
  "registration_id": "reg_...",
  "registration_type": "service_auth",
  "claim_url": "https://moatlight.com/agent/identity/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "...",
  "post_claim_scopes": ["account:read", "reports:read"],
  "claim": { "user_code": "ABCD-2345", "expires_in": 600, "verification_uri": "https://moatlight.com/claim?attempt=...", "interval": 5 }
}
```

Keep `claim_token` secret. Registrations are limited to 10 per hour per address. Other types answer `400 anonymous_not_enabled` or `issuer_not_enabled`.

### Method B: OAuth client registration

Use an HTTPS client_id metadata URL (CIMD), or register your app once (RFC 7591). No approval and no secret are needed for a public client.

```bash
curl -X POST https://moatlight.com/api/auth/oauth2/register \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "My agent",
    "redirect_uris": ["https://example.com/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "account:read reports:read reports:write agent-readiness:read agent-readiness:scan offline_access"
  }'
```

Keep the `client_id` from the answer. Ask only for the scopes you need:

- `account:read`: Read your plan, limits and whether you hold the Early Access pass.
- `reports:read`: Read your reports: status, visibility numbers, competitors and the ranked plan.
- `reports:write`: Start a report for a website and confirm its setup. Uses your free report allowance.
- `agent-readiness:read`: Read the stored agent readiness scan of a report (Early Access pass).
- `agent-readiness:scan`: Run an agent readiness scan for a report (Early Access pass).

## Step 4 — Claim ceremony

The person claims the access for you by signing in and approving it.

### Method A: service_auth

Show the person `claim.user_code` and `claim.verification_uri`. They open the link, sign in to Moatlight with the account named in `login_hint`, check the scopes and type the code. Only continue if the person is there with you: the code must come from you, never from a third party.

- The code works for 10 minutes and for 5 tries. If it expired, `POST https://moatlight.com/agent/identity/claim` with `{"claim_token":"clm_..."}` for a new one (the old code stops working).
- The person can press Cancel on that page; polling then answers `invalid_grant`.
- Send `"agent_name": "My agent"` so the person sees what is asking (it is shown as unverified). More than 5 registrations per hour for one email are refused (`429 rate_limited`).
- The claim_token is valid 30 minutes.

Poll the token endpoint every `interval` seconds until they approve (RFC 8628 style):

```bash
curl -X POST https://moatlight.com/api/auth/oauth2/token \
  -d 'grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=CLAIM_TOKEN'
```

Answers while waiting: `400 authorization_pending` (keep polling), `400 slow_down` (add 5 seconds), `400 expired_token` (get a new code, or start again if the claim_token expired too), `400 invalid_grant` (used, unknown or revoked).

### Method B: OAuth

Make a PKCE pair (`code_verifier`, `code_challenge` = base64url(SHA-256(verifier))) and a random `state`. Build the authorization URL:

```
https://moatlight.com/api/auth/oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT&scope=SCOPES&state=STATE&code_challenge=CHALLENGE&code_challenge_method=S256&resource=https%3A%2F%2Fmoatlight.com
```

Open that URL for the person (or show it). They sign in to Moatlight if needed, see what you asked for on a consent screen, and press Allow. The browser then returns to your `redirect_uri` with `?code=...&state=...`. Check `state`. If the person denies, you get `error=access_denied`. A code lives 10 minutes and works once.

## Step 5 — Exchange the assertion

### Method A: service_auth

The first successful poll returns the tokens and your `identity_assertion`:

```json
{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "account:read reports:read", "identity_assertion": "eyJ...", "assertion_expires": "..." }
```

The claim_token works once. Keep the `identity_assertion`: it is valid for 24 hours. When the access_token expires, trade the assertion for a new one (RFC 7523). There is no refresh token.

```bash
curl -X POST https://moatlight.com/api/auth/oauth2/token \
  -d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=IDENTITY_ASSERTION'
```

For the MCP server add `&resource=https://moatlight.com/mcp`; the token is then bound to that server. When the assertion expires or is refused (`400 invalid_grant`), start again at Step 3.

### Method B: OAuth

```bash
curl -X POST https://moatlight.com/api/auth/oauth2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&code=CODE&redirect_uri=REDIRECT&client_id=CLIENT_ID&code_verifier=VERIFIER&resource=https%3A%2F%2Fmoatlight.com'
```

The answer has `access_token` (a JWT, valid one hour), `token_type: "Bearer"`, `expires_in`, `scope` and, with `offline_access`, a `refresh_token`. Refresh with `grant_type=refresh_token&refresh_token=...&client_id=...`. For the MCP server use `resource=https://moatlight.com/mcp` instead.

## Step 6 — Use the access_token

```http
GET https://moatlight.com/api/v1/me
Authorization: Bearer ACCESS_TOKEN
```

The same token works at the MCP server (https://moatlight.com/mcp). Each endpoint needs one scope; see https://moatlight.com/docs and https://moatlight.com/openapi.json. The free plan covers reports. The agent readiness endpoints need the Early Access pass and answer `402 needs_early_access_pass` with an `upgrade_url` for a person to open.

## Errors

Errors on the API are JSON: `{"error":{"code","message","hint"}}`.

| Status | Code | What to do |
|---|---|---|
| 401 | `unauthorized` | No token. Read the `WWW-Authenticate` header and start at Step 1. |
| 401 | `invalid_token` | Expired or wrong audience. Exchange the assertion or refresh, or run Step 4 again. |
| 403 | `insufficient_scope` | Ask the person to approve the missing scope (`required_scope`). |
| 402 | `needs_early_access_pass` | A person must buy the pass: `upgrade_url`. |
| 429 | `rate_limited` | Wait `Retry-After` seconds. |

Token endpoint and `/agent/identity` errors are OAuth style, `{"error","error_description"}`: `invalid_grant`, `authorization_pending`, `slow_down`, `expired_token`, `invalid_client`, `invalid_request`, `invalid_target`, `unsupported_grant_type`, `anonymous_not_enabled`, `issuer_not_enabled`, `invalid_claim_token`, `claimed_or_in_flight`, `claim_expired`, `rate_limited`.

## Revocation

Revoke a token you no longer need (RFC 7009):

```bash
curl -X POST https://moatlight.com/api/auth/oauth2/revoke \
  -d 'token=REFRESH_OR_ACCESS_TOKEN&token_type_hint=refresh_token&client_id=CLIENT_ID'
```

An access_token from `service_auth` ends after one hour and its identity_assertion after 24 hours; the person is asked again after that. The person can disconnect an agent or an app in Settings at any time: an agent's tokens stop working at once (`401 invalid_token`), an app's refresh token stops and its last access token runs out within the hour.
