# Homepage Agent Chat API

`/api/agent/*` powers the in-page chat agent on the AI & Trends dashboard at `/homepage/`. It answers questions about the aggregated feed, and about one item at a time when the chat is opened from a card.

## Purpose

Use this API when you want either:

- a conversational way to search, summarize, translate, or find similar items across the dashboard's archive
- a machine-readable description of what that chat can do and what it currently costs you in quota

The agent holds a read-only contract over the dashboard's own stores. It cannot write to any of them, it does not browse the open web, and it has no accounts and no server-side transcripts.

## Endpoints

### `GET /api/agent/config`

Public capability and quota advertisement. No authentication.

Key fields:

- `enabled`
- `requireTurnstile`
- `turnstileSiteKey`
- `itemChat`
- `briefEnabled`
- `model`
- `quotas`

Call this before opening a chat: `enabled` is false when the kill switch is set, and `requireTurnstile` tells you whether session minting needs a human verification token.

### `POST /api/agent/session`

Mints a signed, short-lived session token.

Body fields:

- `turnstileToken` (required only when `/api/agent/config` reports `requireTurnstile: true`)

Key response fields:

- `sessionToken`
- `expiresAt`
- `tier`
- `quotas`

The token is opaque and HMAC-signed, carries its own expiry, and is never set as a cookie. Send it back in the `X-Agent-Session` header. Nothing about the caller is stored server-side; rotating the signing secret invalidates every outstanding session.

### `POST /api/agent/chat`

Runs one conversational turn and streams the answer as Server-Sent Events. Requires the `X-Agent-Session` header.

Body fields:

- `messages` — the conversation so far. Only the last 12 entries are read, each truncated to 2000 characters, and the last entry must be a user message. Roles are normalized to `user` or `assistant`.
- `scope` — optional `{ "type": "item", "key": "<source-prefix>-<source-id>" }` to scope the turn to one card. An unresolvable key degrades to the site scope rather than inventing context.
- `sort` — `trending` (default), `day`, `week`, or `month`; selects which feed ranking grounds a site-scoped turn.
- `settings` — optional `preferredLanguage`, `tone`, `prePrompt`, and `interests`. These carry user-level authority only: they may shape style, tone, topics, and language, never tool, quota, or safety policy.

The whole request body is capped at 32 KB.

Conversation state is replayed by the client on every request. The server keeps no transcript and calls the model with storage disabled.

### `GET /api/agent/status`

Coarse health for the site's API catalog.

Key fields:

- `ok`
- `service`
- `budget` — `ok`, `soft`, `exhausted`, or `unknown`
- `itemChat`

`soft` means the daily spend breaker is in its degrade band and answers are produced without tool rounds; `exhausted` means turns are rejected for the rest of the day.

## Stream format

The chat response is `text/event-stream`. The event vocabulary is the site's own, not the upstream provider's, so the wire format survives a provider change. Comment lines (`: ping`) are sent periodically to keep intermediaries from closing an idle connection.

| Event | Payload | Meaning |
|---|---|---|
| `meta` | `{remaining: {session, day}, tier}` | First event; quota headroom left **after** this turn — it is already counted |
| `tool` | `{name, status}` | A read-only tool started or finished; `status` is `start` or `done` |
| `delta` | `{text}` | One chunk of answer text; concatenate in arrival order |
| `done` | `{usage, degraded?, cached?}` | Terminal success event |
| `error` | `{error, code}` | Terminal failure event; `error` is `timeout` or `upstream_error` |

Only the tool's name is disclosed, never its arguments or results. `done` reports `cached: true` when the answer came from the 24h answer cache, and `degraded: true` when the spend breaker suppressed tool rounds.

## Throttling

Turns are rejected before any model call when a limit is hit. Those rejections are plain HTTP responses, not stream events:

```json
{ "error": "rate_limited", "scope": "session", "retryAfterSeconds": 30 }
```

Returned with HTTP 429 and a `Retry-After` header. `scope` is `ip` (per-colo burst or the per-day cap), `session` (message cap or minimum gap between turns), or `global-budget` (the daily spend breaker).

Other failures return an HTTP 4xx or 5xx with a JSON `error` field: `401 invalid_session`, `403 forbidden_origin`, `413 payload_too_large`, `503 disabled`.

## Related endpoints

- OpenAPI: `/docs/api/agent-chat/openapi.json`
- Capabilities: `/api/agent/config`
- Status: `/api/agent/status`
- Public page: `/homepage/`
- Privacy: `/homepage/privacy.html`
