# Tweetsmash authentication

Follows the [auth.md spec](https://workos.com/auth-md). This document tells an agent how to
get a credential for the Tweetsmash API, how to use it, and what to do when it stops working.

Resource server: `https://api.tweetsmash.com/v1`
Also reachable over MCP: `https://mcp.tweetsmash.com/api/mcp`

## Discover

Start here:

| Document | URL |
| --- | --- |
| Protected resource metadata (RFC 9728) | `https://www.tweetsmash.com/.well-known/oauth-protected-resource` |
| Authorization server metadata (RFC 8414) | `https://www.tweetsmash.com/.well-known/oauth-authorization-server` |
| This skill | `https://www.tweetsmash.com/auth.md` |
| OpenAPI 3.1 description | `https://www.tweetsmash.com/openapi.json` |
| MCP server card | `https://www.tweetsmash.com/.well-known/mcp/server-card.json` |

An unauthenticated request to the API answers `401` with a `WWW-Authenticate` header pointing
back at the protected resource metadata, so the chain is discoverable from any endpoint:

```
WWW-Authenticate: Bearer realm="Tweetsmash API", resource_metadata="https://www.tweetsmash.com/.well-known/oauth-protected-resource"
```

The protected resource metadata lists `https://www.tweetsmash.com` in `authorization_servers`.
That origin's authorization server metadata carries the `agent_auth` block described below.

## Pick a method

There is exactly one method: a long-lived **API key** presented as a bearer token.

`identity_types_supported` is `["anonymous"]`. Tweetsmash runs no OAuth authorization endpoint
and no token endpoint, so there is no authorization code flow, no client credentials flow, no
`identity_assertion`, and no `id-jag` exchange. Do not attempt one. `grant_types_supported` is
deliberately empty in the authorization server metadata for this reason.

```json
"agent_auth": {
  "skill": "https://www.tweetsmash.com/auth.md",
  "register_uri": "https://www.tweetsmash.com/integrations/api",
  "claim_uri": "https://www.tweetsmash.com/integrations/api",
  "revocation_uri": "https://www.tweetsmash.com/integrations/api",
  "identity_types_supported": ["anonymous"],
  "anonymous": { "credential_types_supported": ["api_key"] }
}
```

## Register

`register_uri` is `https://www.tweetsmash.com/integrations/api`.

Registration is human-in-the-loop by design. The account holder signs in, opens that page and
generates a key. There is no dynamic client registration endpoint, and an agent cannot mint a
credential on a user's behalf.

If you are an agent and you have no key, stop and ask the user to visit `register_uri` and paste
one back. Do not try to scrape, guess or reuse a key from another account.

## Claim

`claim_uri` is the same page. The key is displayed once at creation. The user copies it and
hands it to you out of band, for example as an environment variable:

```
TWEETSMASH_TOKEN=<key>
```

The key is opaque. It carries no expiry and is not a JWT, so do not try to decode or refresh it.

## Use the credential

Send it in the `Authorization` header:

```http
GET /v1/bookmarks?limit=20 HTTP/1.1
Host: api.tweetsmash.com
Authorization: Bearer <key>
Accept: application/json
```

```bash
curl -s https://api.tweetsmash.com/v1/bookmarks?limit=20 \
  -H "Authorization: Bearer $TWEETSMASH_TOKEN"
```

For MCP, send the same value as the bearer token on the streamable HTTP transport at
`https://mcp.tweetsmash.com/api/mcp`. For the stdio package `@tweetsmash/mcp`, set
`TWEETSMASH_TOKEN` in the environment.

Confirm the key works before doing anything else:

```bash
curl -s https://api.tweetsmash.com/v1/bookmarks/count -H "Authorization: Bearer $TWEETSMASH_TOKEN"
```

A key reaches only the issuing account's own saved posts. It cannot search public X, and it
cannot post, reply, like or follow. Per-key scope selection is not available yet, so a key
reaches the whole surface listed in `scopes_supported`. Treat that list as the blast radius,
not as a least-privilege grant, and keep the key out of logs, prompts and shared context.

## Errors

Every error is JSON in the same envelope. The machine code is the top-level `code` field and
`error` is the human sentence. Branch on `code`, never on `error`.

```json
{ "status": false, "code": "UNAUTHORIZED", "error": "Unauthorized" }
```

| HTTP | `code` | What it means | What to do |
| --- | --- | --- | --- |
| 401 | `UNAUTHORIZED` | Missing, malformed or revoked key | Re-read `WWW-Authenticate`, then ask the user for a new key at `register_uri`. Do not retry the same key. |
| 402 | `SUBSCRIPTION_REQUIRED` | The account is on the free plan and the request needs a paid feature | Tell the user which capability is gated and link the top-level `upgrade_url`. Do not retry. |
| 402 | `FILTER_NOT_ALLOWED` | Filtering is a paid capability | Retry once without the filter, or link `upgrade_url`. |
| 402 | `PAGINATION_NOT_ALLOWED` | Paging past the free window is a paid capability | Stop paging, or link `upgrade_url`. |
| 402 | `VECTOR_SEARCH_NOT_ALLOWED` | Semantic search is a paid capability | Fall back to keyword search, or link `upgrade_url`. |
| 400 | `INVALID_REQUEST` | Invalid parameters | Fix the arguments against `openapi.json` and retry once. |
| 404 | `NOT_FOUND` | No such record for this account | Do not retry. |
| 429 | `RATE_LIMITED` | Too many requests | Honour `Retry-After` and back off. |
| 500 | `INTERNAL_ERROR` | Transient failure | Retry with backoff, reusing the same `Idempotency-Key` on writes. |

Every `402` also carries a top-level `upgrade_url`. Read it rather than hardcoding a pricing link.

Writes accept an `Idempotency-Key` header. Send a fresh UUID per logical operation and reuse it
across retries so a network failure cannot duplicate a record.

## Revocation

`revocation_uri` is `https://www.tweetsmash.com/integrations/api`. The account holder revokes a
key there, which takes effect immediately; the next call answers `401 UNAUTHORIZED`. Generating
a replacement does not invalidate other keys. There is no programmatic revocation endpoint, so
an agent that must drop access should discard its copy of the key and tell the user to revoke it.
