# Diet App -- Agent API

Diet App speaks Markdown, HTML, or JSON on the SAME URLs -- there is no
separate "agent API"; **the API is the website**. The address a person's
browser shows for a page is the address an agent `GET`s for the same
resource; `Accept` picks the representation. If you can `GET` a URL and
read the response, you can use this app.

## The one-sentence version

Send `Accept: text/markdown` (with no `Accept` at all you get the website
itself -- a real, styled HTML page, because the API *is* the website).
Every response lists, under "Links", every resource you can navigate to
next. Start at the client root and follow links -- you never need to know
a URL shape in advance.

## If your tool can only fetch a URL and show you the result

That's enough for the PUBLIC surface: this document, `/llms.txt`, and the
downloadable skill (`/skills/agent-api.md`) are unauthenticated -- fetch them
with nothing but a URL and you'll get real, readable text back.

Reading a specific person's plan/pantry/progress needs a bearer token (see
below) -- a tool that can only fetch bare URLs cannot supply an
`Authorization` header, so it cannot reach per-client data. This is
deliberate, not a limitation we forgot: a token that worked as a URL
parameter would end up in server logs, browser history, and `Referer`
headers of every link it touched. We would rather you need one extra
capability (setting a header) than leak your data through five unrelated
channels. If your agent framework supports custom headers on `fetch`/`curl`
calls (nearly all do), you have full read access with the token below.

## Authentication: a revocable, read-only, per-client bearer token

1. A client signs into their own Diet App workspace normally (Google
   sign-in -- this app never asks an agent to authenticate as a human).
2. In their workspace Settings, they mint a token under "AI assistant
   access". The raw token is shown ONCE, at mint time -- Diet App never
   stores it in recoverable form, only its SHA-256 hash.
3. Every request against that client's data carries:

   ```
   Authorization: Bearer <token>
   ```

4. The token is:
   - **Read-only.** It can `GET` anything the client can see -- including
     the styled HTML pages themselves, rendered read-only. It cannot
     mutate anything -- no pantry edits, no new plans, no chat messages.
     Every write requires the client's own signed-in session; a bearer
     token gets `403 Forbidden` on all of them.
   - **Scoped to exactly one client.** A token minted for client A gets
     `404 Not Found` (not `403`) on client B's resources, the same "don't
     even confirm it exists" answer a stranger gets -- there is no way to
     enumerate or guess your way into someone else's data with a valid
     token in hand.
   - **Revocable**, instantly, from the same Settings panel that minted it.
   - Never a raw Google OAuth token. This app mints and controls the token's
     lifetime, scope, and revocation independently of Google's.

No `Authorization` header (and no signed-in session) gets `401 Unauthorized`
with `WWW-Authenticate: Bearer` -- the standard HTTP signal that this is
what's missing, no bespoke documentation required to understand it.

## Worked example: root to a recipe, following only links

```
$ curl -s -H "Authorization: Bearer $TOKEN" \
       -H "Accept: text/markdown" \
       https://dietapp.404.mn/clients/7
```
->
```markdown
# Diet App

**Nadia**'s Diet App workspace. ...

## Cook your next meal
Up next · Today · Dinner — **Chicken & rice** · 720 cal · 34 g protein.
...

## Links
- **self**: [Nadia](/clients/7)
- **pantry**: [Pantry](/clients/7/pantry)
- **progress**: [Progress / adherence](/clients/7/progress)
- **plan**: [Plan for week of 2026-07-06](/clients/7/plans/42)
```

Follow `plan`, then a `day` link from its response
(`/clients/7/plans/42/days/Monday`), then a `meal` link
(`.../meals/Breakfast`), then the `recipe` link -- and you have the actual
recipe (or, honestly, "no recipe written yet" plus the real ingredient
allocation, never invented prose).

## Resource map (read-only via a bearer token, all `GET`)

| Resource | URL shape | What it is |
|---|---|---|
| Client root | `/clients/{id}` | Entry point: the briefing sections + links to everything below |
| Pantry | `/clients/{id}/pantry` | Every pantry item, quantity, and how much is left |
| Pantry item | `/clients/{id}/pantry/{name}` | One item, by exact name (or `%23hex4` short id) |
| Profile | `/clients/{id}/profile` | Macro targets, tolerances, meal schedule |
| Progress | `/clients/{id}/progress` | Meals cooked, ratings |
| Conversations | `/clients/{id}/conversations` | The list of coach conversations -- a link per one |
| Transcript | `/clients/{id}/conversations/{conv_id}` | A coach conversation, verbatim |
| Coach image | `/clients/{id}/coach-images/{name}` | A photo attached to a chat turn (binary; no content negotiation, but the same bearer auth) |
| Current plan | `/clients/{id}/plan` | The current week: status + a link per day |
| Plan history | `/clients/{id}/plans` | Every plan ever computed, one link each |
| A plan | `/clients/{id}/plans/{plan_id}` | One specific plan (current or historical) |
| Day | `/clients/{id}/plans/{plan_id}/days/{day}` | One day: a link per meal |
| Meal | `.../days/{day}/meals/{meal}` | Macros + ingredient allocation for one meal |
| Recipe | `.../days/{day}/meals/{meal}/recipe` | The written-up recipe, or the honest fallback |
| Fix | `/clients/{id}/plan/fix` | Only present when the week is infeasible: what buying would fix it |

Two resources exist on the same shared dependency but are NOT bearer-
readable, by design: `/clients/{id}/invite` (trainer-only -- the invite
link for a not-yet-bound client) and `/clients/{id}/agent-tokens` (owner-
session-only -- a read-only token minting or revoking tokens, including
itself, would be a privilege-escalation loop). Both still 401/403 the same
way every other route does; they just never succeed for a bearer token,
only a signed-in session.

The plan resource is expensive to compute (it runs a real constraint
solver) -- it supports conditional `GET` (`ETag` / `If-None-Match`), and
its `job_state` field says honestly whether it is still `queued`/`running`
or settled. Poll it like any standard HTTP resource; there is nothing
Diet-App-specific to learn here.

## What this API will NOT do over GET

Nothing. `GET` never mutates anything, ever, on this API -- not a pantry
edit, not a new plan, not marking a meal cooked. That is true of every
route on every representation, and it is the whole reason a `GET`-only
bearer token is safe to hand to an autonomous agent, a crawler, or a
link-preview bot: there is no request shape that can turn a read into a
write.

Writes exist -- one unified surface: `{POST,PUT,PATCH,DELETE}
/clients/{id}/{action}`, the same actions the website's own forms and the
client's AI coach use, each advertised in its owning resource's "Actions"
section. They require the client's own signed-in session and are out of
scope for the bearer-token surface entirely.

## Content negotiation, precisely

`Accept: text/markdown` -> Markdown.
`Accept: text/html` (or no `Accept` header at all -- what `curl` and a bare
`fetch()` send) -> the real, server-rendered website page -- actual `<a
href>` links, works with JavaScript disabled. The API is the website.
`Accept: application/json` -> structured data, plus a `links` array in the
same shape either way: `{"rel", "href", "title"}`.

Every response carries `Vary: Accept`, honestly: two requests for the same
URL with different `Accept` headers really are different representations,
and any cache in front of this API needs to know that.
