# FPLai Agent Guide

FPLai exposes a read-only agent surface for discovering Fantasy Premier League content, pSEO route families, season hubs, and public product pages.

## Best starting points

- `https://fplai.app/` for the main product overview
- `https://fplai.app/fpl/` for the all-seasons FPL hub
- `https://fplai.app/llms.txt` for the long-form content index
- `https://fplai.app/api/agent/content-index` for a machine-friendly route index
- `https://fplai.app/mcp` for the read-only MCP endpoint

## Public content types

- Season hubs, such as `/fpl/2026-27/` (this document always names the current season; older seasons stay browsable from `/fpl/`)
- Weekly analysis pages like captain picks and wildcard drafts
- Player, team, comparison, ownership, and fixture pages under `/fpl/{season}/...`
- Product landing pages such as `/fpl-team-analyzer/` and `/best-fpl-app/`

## Public agent endpoints

- `GET /api/v1/agent/health`
- `GET /api/v1/agent/content-index?season=&family=&q=&limit=&cursor=` — cursor pagination: `limit` is the page size (1–100, default 25); omit `cursor` for the first page, then pass the response's `nextCursor` back as `cursor` (`null` on the last page). A cursor is only valid for the filters it was issued with; a forged or mismatched cursor returns a 400 problem with code `invalid_query`. The MCP tools `list_content_sections` and `search_content_paths` accept the same `cursor` argument and return `nextCursor` in `structuredContent`.
- `GET /?mode=agent` — the homepage as a machine-readable site manifest (JSON, or Markdown with `Accept: text/markdown`): endpoints, authentication, capabilities, rate limits, versioning, content signals, contact.
- `POST /mcp` (tools include `get_page_markdown`, which converts any public page path to Markdown)
- `POST /a2a` (A2A JSON-RPC, see below)

The OpenAPI 3.1 description of these endpoints is published at `https://fplai.app/openapi.json` (also `/api/openapi.json` and `/.well-known/openapi/agent-api.json`) and linked from `/.well-known/api-catalog`. Human-readable developer docs for the same surface: `https://fplai.app/developers`.

All agent endpoints are read-only (`GET`, `HEAD`, `OPTIONS`; JSON-RPC over `POST` for `/mcp` and `/a2a`), CORS is open (`Access-Control-Allow-Origin: *`), and every REST response links its own description with `Link: </openapi.json>; rel="service-desc"`.

## CLI

An official zero-dependency Node CLI wraps this whole surface: `npx fplai-cli health`, `npx fplai-cli content-index --season 2026-27 --family captain-picks`, `npx fplai-cli page /fpl/2026-27/captain-picks/gw01/`, `npx fplai-cli overview`, `npx fplai-cli ask "find captain picks for 2026-27"`, `npx fplai-cli openapi`. Install with `npm i -g fplai-cli` (Node 18+, no runtime dependencies). Add `--json` for the raw payload, `--base-url` to point elsewhere, `--verbose` to see rate-limit headers. Source and docs: `https://www.npmjs.com/package/fplai-cli` and `https://fplai.app/developers`.

## Versioning

The canonical agent API is versioned in the path: `/api/v1/agent/...`. The older unversioned paths `/api/agent/health` and `/api/agent/content-index` still return identical payloads, but every response carries an RFC 9745 `Deprecation` header (`@1788912000`, i.e. 2026-09-09) and `Link: </api/v1/agent/...>; rel="successor-version"`. No `Sunset` header is sent because no removal date is decided; when one is, it will be published at least 12 months before removal. Breaking changes ship as `/api/v2/...`; additive fields may appear inside v1 without a version bump.

## Errors

Every non-2xx response from `/api/v1/*` and `/api/agent/*` is an RFC 9457 problem detail (`Content-Type: application/problem+json`, `Cache-Control: no-store`) with a stable `code` field: `not_found`, `method_not_allowed`, `invalid_query`, `rate_limited`, `internal_error`. Branch on `code`, not on the `type` URI text. `/mcp` and `/a2a` use JSON-RPC 2.0 error envelopes instead, except for 429, which is a problem detail on every transport.

## Rate limits

60 requests per 60 seconds per client IP across `/api/v1/*`, `/api/agent/*`, `/mcp`, and `/a2a`. Responses carry both `RateLimit-Policy: "agent-api";q=60;w=60` / `RateLimit: "agent-api";r=<remaining>;t=<seconds>` and the legacy `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset`. Exhaustion returns a 429 problem with `Retry-After`. Enforcement is best-effort and per edge isolate, not a hard global quota.

## A2A (Agent2Agent)

- **Agent Card:** `https://fplai.app/.well-known/agent-card.json` (A2A v1.0)
- **Endpoint:** `POST https://fplai.app/a2a`, A2A v1.0 JSON-RPC 2.0 binding. `GET /a2a` returns a small descriptor that links the Agent Card. No auth; public, read-only; responses are `no-store`.
- **Methods:** `SendMessage` (v1.0) and the v0.3 spelling `message/send` answer synchronously with a **Message**, not a Task (spec §3.1.1); v1.0 callers get `result.message`, v0.3 callers get the Message as `result`. `GetTask`/`tasks/get`/`CancelTask`/`tasks/cancel` return `-32001` TaskNotFoundError (this agent never creates Tasks). Streaming and subscribe methods return `-32004` UnsupportedOperationError; push-notification config methods return `-32003`; unknown methods `-32601`; bad JSON `-32700`; bad params `-32602`.
- **Skills:** `site-overview` (entry points and discovery documents), `find-fpl-content` (season and/or family parsed from the text, remainder used as a keyword query; same payload as `/api/v1/agent/content-index`), `page-markdown` (a same-origin path or `https://fplai.app/...` URL in the message returns that page as Markdown). Every reply carries a text part and a `data` part with the structured JSON result.

```bash
curl -sS https://fplai.app/a2a -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"messageId":"1","role":"user","parts":[{"kind":"text","text":"find captain picks for 2026-27"}]}}}'
```

## Reading pages as Markdown

The homepage, product pages (`/pricing`, `/about`, `/support`, `/demo`, `/review-my-squad`, `/news/…`), and the weekly-analysis families routed through the Worker (captain picks, gameweek reviews, wildcard drafts, compare, ownership, transfers, price changes, predicted lineups, and similar) negotiate on the `Accept` header (acceptmarkdown.com convention). Send `Accept: text/markdown` and the same URL returns `Content-Type: text/markdown; charset=utf-8` with the page's title, description, canonical `Source:` line, and body text. Both the HTML and Markdown variants carry `Vary: Accept`. RFC 9110 q-values are honoured, so `text/html, text/markdown;q=0.5` still returns HTML. Statically served pSEO pages (player, team, and season-table pages) currently return HTML only; use `POST /mcp` with `get_page_markdown` to read any public page as Markdown.

## Errors and recovery

- Unknown paths return a real `404 Not Found` with a short body linking to `/sitemap.xml`, `/llms.txt`, `/openapi.json`, and this guide. (Unknown sub-paths under client-rendered prefixes such as `/rivals/…` or `/demo/…` return the app shell with `X-Robots-Tag: noindex`.)
- Retired analysis URLs return `410 Gone` with the same recovery links; ask for `text/markdown` to get them as a list.
- `/about`, `/support`, `/privacy-policy`, and `/terms` are the trust pages for verifying who runs FPLai.

## Auth notes

FPLai account authentication is backed by Supabase. Public content discovery is unauthenticated. OAuth discovery metadata is published to help agents understand how authenticated FPLai sessions are issued for account-linked features.

See `https://fplai.app/auth.md` for the full agent-auth contract: the public agent surface (`/api/v1/agent/*`, `/api/agent/*`, `/mcp`, `/a2a`, `/openapi.json`, `/llms.txt`, all public pages) needs no authentication. Agents cannot self-register: accounts are created only by humans at `https://fplai.app/register` (email and password, or Google OAuth). Account-linked features (team analysis, saved plans) are only available to a signed-in human in the web or iOS app. Content-usage preferences are declared in `robots.txt` as `Content-Signal: ai-train=no, search=yes, ai-input=yes`.
