# Auth.md

Agent authentication for The Ranking Factory.

> How an AI agent acting for one of our customers authenticates with this service.
> Served at https://therankingfactory.com/.well-known/auth.md, and at https://therankingfactory.com/auth.md
> — one file at both addresses, because the convention is young and agents look in both places.

Last updated: 2026-09-21

## What exists: a read/write MCP server, write opt-in per connection, authorised by the customer in the app

There is an MCP (Model Context Protocol) server at:

```
POST https://therankingfactory.com/api/mcp
```

Transport: JSON-RPC 2.0 over HTTP POST ("streamable HTTP"; no server-push stream — a GET answers 405).
The machine-readable description, including the current tool and scope catalog, is at:

```
https://therankingfactory.com/.well-known/mcp/server-card.json
```

It is **read/write**: every credential reads one customer account's data (AI-visibility measurements,
evidence gaps, cited sources, published evidence, the entity) from the moment it exists, and can also
change it once the customer grants the separate `write` scope — off on every route until they tick it. See
**Writes** below.

## How an agent gets access

Two ways, both authorised by the customer in person, both on every plan (Starter included) and in the
trial, both writable only once the customer ticks a box that starts unticked, and both only ever seeing the
account of the person who authorised them.

### 1. Sign in (OAuth) — for clients that support MCP authorization

A client that implements the MCP authorization spec (Claude does) connects by signing in; there is no
token to copy. Point it at the MCP URL above and it discovers the rest:

- An unauthenticated request gets `401` with
  `WWW-Authenticate: Bearer resource_metadata="https://therankingfactory.com/.well-known/oauth-protected-resource/api/mcp"`
  (`error="invalid_token"` when a credential was sent but is unknown, expired or revoked).
- Protected-resource metadata (RFC 9728):
  `https://therankingfactory.com/.well-known/oauth-protected-resource/api/mcp`
  (also at `/.well-known/oauth-protected-resource`). Its `resource` is `https://therankingfactory.com/api/mcp`.
- Authorization-server metadata (RFC 8414):
  `https://therankingfactory.com/.well-known/oauth-authorization-server`.
- **Client identity:** a Client ID Metadata Document (an `https` URL as `client_id`, fetched and checked
  before anything is shown), or registration (RFC 7591) at `POST /api/oauth/register` — public clients
  only (`token_endpoint_auth_method: none`), redirects `https` or loopback `http`.
- **The flow:** authorization code with PKCE (`S256` only), and the `resource` parameter (RFC 8707) set
  to the MCP URL — a token is never issued for any other resource, and is refused anywhere else. The
  token endpoint is `POST /api/oauth/token`, form-encoded.
- **Consent:** the customer sees who is asking, where they will be sent back to, and what it may read,
  and chooses **every project** in the account or **one project**. A connection given one project reads
  that project and no other: every tool answers for it, and asking for another is refused. The same page
  carries one box, "Let it make changes too", which starts unticked whatever the client requested: asking
  for `write` is not being given it, and the token response's `scope` says what was granted.
- **Tokens:** access tokens (`rfa_…`) last one hour. Refresh tokens (`rfr_…`) last 30 days and rotate on
  every use; presenting one that was already replaced ends the connection, because only a copy would do
  that. A refresh is refused once the account's plan no longer includes signing in.
- Connections are listed, with what each reads, at **Settings → Agent access**, and ending one stops its
  tokens at once.

### 2. An agent key — any client, including ones that cannot sign in

1. A signed-in customer opens **Settings → Agent access** in the app.
2. They create a key: give it a name, choose what it may read, leave or tick "Let it make changes"
   (unticked by default), and copy it — it is shown **once** and only its hash is stored.
3. They hand it to the agent they run. The key looks like `rft_…` and is sent as
   `Authorization: Bearer rft_…` on every request to the MCP endpoint.

Keys are revocable at the same screen, and revocation is immediate. A key sees every project in the
account; pass `project` to any tool to narrow it.

### Scopes

Named per capability (`visibility:read`, `gaps:read`, `evidence:read`, `entity:read`, and `write` — the
live list with descriptions is in the server card, which is generated from the catalog the server
actually runs on rather than kept here as a second copy). A credential only sees, and can only call, the
tools its scopes unlock. A client that requests no scope is given the four read scopes; the 401 challenge
names those four only.

Either credential works **only** at the MCP endpoint. Neither is a session, neither opens anything else
on this API, and our login sessions do not work at the MCP endpoint — they are different types and none
substitutes for another.

## What is available without authentication

- `GET /api/version` — the running build. Anonymous, `no-store`.
- `/llms.txt` — what this product is, for AI systems summarising it.
- `/sitemap.xml` — the public page index.
- `/.well-known/mcp/server-card.json` — the MCP server description above.
- `/.well-known/oauth-protected-resource/api/mcp` and `/.well-known/oauth-authorization-server` — the
  sign-in discovery documents above.

Everything else under `/api/` requires a signed-in customer session and is intended for our own
application. Please do not drive the customer-facing app with a browser-automation agent as a
substitute; the MCP surface is the supported interface, and if it lacks something you need, ask us for
it (below).

## Writes

Off unless the customer granted them. One scope, `write`, held only by a connection whose consent had
"Let it make changes too" ticked, or a key minted with the same box ticked. Credentials made before
21 September 2026 do not hold it and nothing grants it to them.

- **Five write tools**, each needing `write` **and** the read scope of what it changes: `update_entity`
  and `confirm_product` (`entity:read`), `close_gap` (`gaps:read`), `approve_publish` (`evidence:read`),
  `run_visibility_now` (`visibility:read`). `tools/list` shows a write tool only to a caller that holds
  both, and every tool carries the MCP annotations `readOnlyHint`, `destructiveHint` and `idempotentHint`.
- **Without the scope**, a write tool answers `403` with
  `WWW-Authenticate: Bearer error="insufficient_scope", scope="…", resource_metadata="…"` — the scopes the
  credential already holds plus the ones the tool needs — and a JSON-RPC error body. A client that can
  re-authorise may then send the customer back to the consent page; the box is still unticked there.
- **One project per write, matched exactly.** A connection granted one project can change that project and
  no other, refused before anything runs. A key, which sees every project, must pass `project` as the
  project's domain on every write; a partial or similar domain is refused with the projects that exist.
- **The same rules as the app.** Each tool calls the code the app's own button calls, so its refusals are
  the app's: gaps only a person can do, a page with nowhere to publish, an account whose subscription has
  lapsed (read-only here too). `close_gap` never sends a page that is held because the customer reviews
  before publishing; that is `approve_publish`, which an agent should call only when the person it is
  working for has told it to approve that specific item.
- **A ceiling and a record.** Write attempts are limited per credential per hour (reads are not), and every
  attempt — done, refused or failed — is recorded and shown to the customer under **Settings → Agent
  access → What agents changed**. A refusal says what was refused, why, and what would be accepted;
  retrying it unchanged will be refused the same way.

## Limits and conduct

Be reasonable: this surface serves accounts, not crawls. Cache what you read; a poll loop against
`tools/call` measures nothing that changed in the last minute. Report suspected credential misuse or
abusive automation to **support@therankingfactory.com** — a human reads it.

## Asking for more

Email **support@therankingfactory.com** describing what the agent would do on the customer's behalf.
If there is a real use case we would rather build the interface than have people work around its
absence.
