Files
vyndr/docs/CONTENT-STUDIO-API.md
builtbykev c575a708c7 Content studio API + preview page; widen the reachability guard; correct
two inventory errors

INVENTORY CORRECTION, and it was mine. Phase 2's two "orphans" are NOT
orphans -- my board grepped only web/src/app and missed component-level
mounting. The transitive check says both are already mounted:

  BookComparisonPanel -> GradeResultCard -> app/scan/page.tsx
  NewsWire            -> ExploreHub      -> app/explore/page.tsx

So book comparison is DONE (wired to /api/books, rendering on the grade
card) and THE WIRE is DONE-BY-DESIGN, mounted in ExploreHub. Its header
names an "Offseason Hub" as its home, and that hub genuinely does not
exist -- but that is board item #8, not a mounting bug, and inventing a
surface to satisfy a comment would be the wrong fix.

The lesson is the same one this session keeps teaching: I checked one
directory and reported a conclusion the check could not support.

ALSO CAUGHT: I overwrote src/routes/content.js, which was the Session-29
content-templates route, by picking a filename without looking. Restored
from git with no work lost; the new surface lives at
/api/content-studio and both now coexist.

PHASE 0/1 — /api/content-studio serves finished posts (copy, branded card,
card_svg, the fact_contract each was REQUIRED to have, and the facts that
actually backed it) plus a POST for editorial status in Redis. Private via
internal key; the Next proxy holds the key server-side so the browser
never does. /studio renders it as a thin client -- copy and card side by
side with the fact contract visible, because reviewing copy by reading it
is exactly how a wrong number ships. Never-blank: a night with nothing
generated says so.

API-FIRST is the point: the endpoint an autonomous poster will call is the
one the page already renders, so the agent handoff is a pointer change,
not a rebuild. Contract documented at docs/CONTENT-STUDIO-API.md.

EXPRESS 5 BROKE 23 SUITES at first: `router.get('/:date?')` throws at
mount time in Express 5, taking down everything that imports app.js. Two
explicit routes instead.

PHASE 3 — the reachability guard is widened from grade-fields-only to a
general built-but-unread check. Book comparison, THE WIRE and the content
studio are now registered surfaces; a page counts as its own entry point
(Next mounts it by convention) while everything else must trace to one.
22 checks green; a registered-but-unimported surface still goes red.

FULLY ISOLATED: read-only on model/slate/ledger, serving fingerprint
verified unchanged, accrual clock unchanged at 0 eligible dates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-07 18:51:52 -04:00

2.4 KiB

/api/content-studio — the agent-ready contract

The endpoint Kev's /studio page reads today and an autonomous poster reads later. The page is a thin client: no posting logic, no fact handling. Wiring a bot means pointing it here — nothing on this side changes.

Distinct from /api/content (Session 29), which serves structured content objects by data level. This serves finished posts.

Auth

x-internal-key: $VYNDR_INTERNAL_KEY — private, never public. The browser never holds the key; the Next proxy at web/src/app/api/content-studio/[...path] attaches it server-side.

GET /api/content-studio/:date?

:date optional, defaults to today ET.

{
  "date": "2026-08-07",
  "count": 3,
  "posts": [{
    "id": "honesty_flex",
    "label": "The Honesty Flex",
    "sport": "mlb",
    "status": "pending",            // pending | approved | skipped | regenerate_requested
    "ok": true,
    "skipped": false,
    "reason": null,                 // why it was skipped, when it was
    "honest_absence": false,        // a real "nothing tonight" post, not a failure
    "copy": "WE GRADED 2140 PROPS TONIGHT...",
    "card": { "title": "...", "lines": [...] },
    "card_svg": "<svg ...>",        // ready to render or rasterise
    "fact_contract": ["graded", "ceiling_letter", "..."],   // REQUIRED fields
    "facts": { "graded": 2140, "...": "..." }               // what backed it
  }]
}

fact_contract + facts are the point. An agent (or a reviewer) can check what a claim rests on instead of trusting the sentence. A post whose contract could not be met never appears with invented values — it arrives skipped with a reason, or as an honest_absence.

POST /api/content-studio/:date/:id/status

{ "status": "approved" }   // approved | skipped | regenerate_requested

Editorial state only, stored in Redis for 14 days. Approving a post changes nothing about the model — status never touches a serving, model or ledger table.

For the agent build

  1. GET the date → filter ok && !skipped.
  2. Post copy; rasterise or attach card_svg.
  3. POST status approved on success.
  4. Never synthesise a claim not present in facts. The engine refuses to render an unbacked token; an agent must not reintroduce one downstream.

Adding a template changes the payload not at all — a new id simply appears.