# Authentication at Zerocost

> How agents authenticate to Zerocost: no credentials for public data, OAuth 2.0 (authorization code + PKCE) for a user's own account data, and API keys for the SDK.

## 1. Discover

Start from any protected endpoint or the metadata documents:

- Call a protected endpoint without a token, e.g. `GET https://zerocost.app/api/v1` or an account tool on `POST https://zerocost.app/mcp`. You get `401` with `WWW-Authenticate: Bearer resource_metadata="..."`.
- Protected resource metadata (RFC 9728): https://zerocost.app/.well-known/oauth-protected-resource/api/v1 (REST) and https://zerocost.app/.well-known/oauth-protected-resource/mcp (MCP). Both list `authorization_servers: ["https://zerocost.app"]` and `scopes_supported`.
- Authorization server metadata (RFC 8414): https://zerocost.app/.well-known/oauth-authorization-server. It has the authorization, token and registration endpoints, the supported scopes, grant types and PKCE methods, and an `agent_auth` block for anonymous agent registration (`identity_endpoint`: https://zerocost.app/agent/identity, `identity_types_supported`: ["anonymous"]).

## 2. Pick a method

| You need | Method | Credential |
| --- | --- | --- |
| Docs, pricing, page index, NLWeb, docs MCP | None | - |
| A user's profile, apps, earnings or API keys | OAuth 2.0 authorization code + PKCE | Bearer access token |
| Testing an integration against account endpoints | Sandbox | `Authorization: Bearer zc_sandbox` |
| Registering an agent with no user (anonymous) | `agent_auth` anonymous registration | Identity assertion, exchanged for the sandbox token |
| Serving ads and sending events from an app | SDK API key | `x-api-key` header |

Agents can register themselves anonymously (`agent_auth` `anonymous` identity type), which yields sandbox access only. Registration with a user's email or with an identity provider's assertion is not offered, and anonymous registrations cannot be claimed: real account data always requires a person approving access in the browser through the OAuth flow below.

## 3. Register

### Anonymous agent (no user)

```http
POST https://zerocost.app/agent/identity
Content-Type: application/json

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

The response has `registration_id`, `registration_type: "anonymous"`, a service-signed `identity_assertion` (valid 24 hours, see `assertion_expires`) and `pre_claim_scopes`. Exchange the assertion at the token endpoint (step 5). It only ever yields the public sandbox token, so you see demo data, never a real account. `service_auth` returns `service_auth_not_enabled` and `identity_assertion` returns `issuer_not_enabled`.

### OAuth client (acting for a user)

Register an OAuth client dynamically (RFC 7591):

```http
POST https://zerocost.app/oauth/register
Content-Type: application/json
Idempotency-Key: <unique key>

{"client_name": "My Agent", "redirect_uris": ["http://127.0.0.1:8765/callback"], "token_endpoint_auth_method": "none"}
```

- `redirect_uris`: https URLs, loopback http URLs (127.0.0.1, localhost, [::1]) or private-use schemes for native apps.
- `token_endpoint_auth_method`: `none` (public client, PKCE) or `client_secret_post` / `client_secret_basic` (you get a `client_secret`).
- With an `Idempotency-Key`, a retried registration returns the same `client_id`.
- Instead of registering, you can use a Client ID Metadata Document URL (https) as your `client_id`.

## 4. Claim (user consent)

Anonymous registrations cannot be claimed (there is no claim endpoint). To act for a real user, send the user's browser to the authorization endpoint:

```
https://zerocost.app/oauth/authorize?response_type=code&client_id=...&redirect_uri=...&scope=profile:read%20apps:read&state=...&code_challenge=...&code_challenge_method=S256
```

The user signs in to Zerocost (email and password or Google), sees your client name, the scopes and where they will be sent back, and approves or denies. You receive `code`, `state` and `iss` (or `error=access_denied`) on your redirect URI. PKCE with `S256` is required.

### Scopes

Request only what you need, space-separated in `scope`. Omitting `scope` requests all of them.

| Scope | Grants |
| --- | --- |
| `profile:read` | See your name, company, role and email. |
| `apps:read` | See your apps and ad placements, including impressions, clicks and revenue. |
| `earnings:read` | See your earnings totals and payout history. |
| `api_keys:read` | See the names, environments and status of your API keys (keys are masked). |

## 5. Exchange

Anonymous identity assertion (RFC 7523 JWT-bearer grant):

```http
POST https://zerocost.app/oauth/token
Content-Type: application/x-www-form-urlencoded

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

Returns `{"access_token": "zc_sandbox", "token_type": "Bearer", "scope": "...", "sandbox": true}`. Re-use the assertion until it expires, then register again.

Authorization code (acting for a user):

```http
POST https://zerocost.app/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=...&redirect_uri=...&client_id=...&code_verifier=...
```

The response is `{"access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "..."}`. Codes are valid for 60 seconds. Access tokens last up to one hour and there are no refresh tokens: run the flow again when one expires.

## 6. Use the access_token

Send `Authorization: Bearer <access_token>`.

| Scope | REST | MCP tool (https://zerocost.app/mcp) |
| --- | --- | --- |
| `profile:read` | `GET /api/v1/me` | `whoami` |
| `apps:read` | `GET /api/v1/me/apps` | `list_my_apps` |
| `earnings:read` | `GET /api/v1/me/earnings` | `get_my_earnings` |
| `api_keys:read` | `GET /api/v1/me/api-keys` | `list_my_api_keys` |

Everything is read-only. No scope allows writes, payouts, campaign changes or reading unmasked API keys. Reads run as the user, so Zerocost's row-level security applies on top of the scopes.

### Sandbox

To build or test against the account API without a Zerocost account, send `Authorization: Bearer zc_sandbox` to any `/api/v1/me` endpoint, `POST /api/v1/batch`, or the account tools on `https://zerocost.app/mcp`. It grants every scope but only ever returns fixed demo data: responses include `"sandbox": true` (and a `Zerocost-Sandbox: true` header on REST). It never reads or changes a real account.

## 7. Errors

| Response | Meaning | What to do |
| --- | --- | --- |
| `401` + `WWW-Authenticate: Bearer resource_metadata="..."` | No token | Start at step 1 |
| `401` + `error="invalid_token"` | Token expired, malformed or from another deployment | Run the flow again |
| `403` + `error="insufficient_scope", scope="..."` | Token lacks a scope | Re-authorize with the listed scopes |
| `400 invalid_grant` at /oauth/token | Code expired, reused by another client, or PKCE verifier mismatch | Restart from step 4 |
| `400 invalid_scope` / `invalid_request` on the redirect | Bad authorization request | Fix the parameters |
| `429` + `Retry-After` | Rate limit hit | Wait and retry |
| `400 service_auth_not_enabled` / `issuer_not_enabled` at /agent/identity | That registration type is not offered | Use anonymous, or OAuth for a real user |
| `400 invalid_grant` for the jwt-bearer grant | Identity assertion expired or invalid | Register again at /agent/identity |
| `503 temporarily_unavailable` | OAuth not configured on this deployment | Retry later |

API errors are JSON: `{"error": {"status", "code", "message"}}`. OAuth endpoint errors use the RFC 6749 shape `{"error", "error_description"}`.

## 8. Revocation

There is no revocation endpoint. Access tokens cannot be revoked individually and expire within one hour; discard tokens you no longer need. Users stop future grants by not approving the consent screen.

## Rate limits

120 requests per 60 seconds per client IP on /api/v1, /mcp, /docs/mcp, /ask, /a2a, /oauth/register and /oauth/token. Responses carry `RateLimit-Policy` and `RateLimit` (draft-ietf-httpapi-ratelimit-headers) and `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. Over the limit you get `429` with `Retry-After`. Limits are counted per edge instance, so treat them as a ceiling rather than a guarantee.

## Versioning and deprecation

Full policy: https://zerocost.app/developers/versioning

- The API version is in the URL path (/api/v1) and in the API-Version response header.
- Within v1, changes are additive only: new endpoints, new optional parameters and new response fields. Clients should ignore fields they do not recognise.
- Breaking changes ship as a new version (/api/v2). The previous version keeps working alongside it.
- Before a v1 endpoint or field is removed, its responses carry a Deprecation header (RFC 9745) and a Sunset header (RFC 8594) at least 90 days before the removal date.
- Deprecated responses also carry a Link header with rel="deprecation" pointing at migration notes.
- Deprecations are announced on this page, in /auth.md and in the OpenAPI spec (operations are marked deprecated: true).
- v1 is the current version. No endpoints are deprecated.

## SDK endpoints (API key)

The `@zerocost/sdk` SDK authenticates every request with an API key sent in the `x-api-key` header.

- Keys are self-serve: create a free account at https://zerocost.app/register, then manage keys at https://zerocost.app/startup/api-keys.
- Each key belongs to one app and carries an environment label, `development` or `production`, for your own bookkeeping. Both serve and record live traffic; there is no separate sandbox API yet. Preview ad formats without live traffic in the browser playground: https://zerocost.app/playground
- A key can be revoked from the dashboard at any time; revoked keys return 403.
- Check a key: `GET https://zerocost.app/functions/v1/validate-key` with header `x-api-key: <key>`.

Keys are publishable (they ship in client-side code) and only grant the SDK capabilities of the app they belong to: fetching ad placements, recording impressions and clicks, and submitting consented data. They cannot read earnings, change settings or access other apps.
