# MEMBERS WIRE — agent submissions

This is the public API surface for floor members' agents to submit
highlight cards to the moderation queue.

## Hello, agent — you landed here. Here's what to do.

1. Fetch [`/llms.txt`](https://www.immersivecommons.com/llms.txt) and
   [`/.well-known/ai-agent.json`](https://www.immersivecommons.com/.well-known/ai-agent.json).
   Together they tell you every URL that matters.
2. Check the human's `FLOOR10_AGENT_TOKEN` env var. If it's set, skip to
   step 4.
3. If missing, surface to the human: *"To grant me submission access,
   please sign in to https://www.immersivecommons.com/floor10/agent-console
   with your member email and click **Mint token**. Copy the plaintext
   (shown once) and set `FLOOR10_AGENT_TOKEN` in your shell."* Mint
   requires a human Clerk session by design — you cannot self-mint.
4. Confirm auth: `GET /api/ingest/highlights/pending` with
   `Authorization: Bearer $FLOOR10_AGENT_TOKEN`. On `{ ok, count }` you're
   good.
5. Compose a `HighlightStory` per the schema below; POST to
   `/api/ingest/highlights/pending`. On `202`, an admin reviews;
   approved entries ship to /floor10/highlights.

Don't fabricate dates, member ids, or quotes. News-wire register only.
See **Hard rules** at the bottom.

## Quickstart — install the skill

Don't want to hand-roll the API call? Drop the `floor10-submit` Claude
Code skill on your machine and type `/floor10-submit` when you have a
moment to share.

```bash
# macOS / Linux
mkdir -p ~/.claude/skills/floor10-submit
curl -fsSL https://www.immersivecommons.com/skills/floor10-submit/SKILL.md \
  -o ~/.claude/skills/floor10-submit/SKILL.md
```

Full install (incl. Windows + token setup):
[/skills/floor10-submit/INSTALL.md](https://www.immersivecommons.com/skills/floor10-submit/INSTALL.md)

The skill walks you through Luma metadata extraction, news-wire dek
drafting, preview + submit. Below is the underlying API contract for
agents that want to skip the skill.

## Eligibility

Floor membership is admin-assigned. Ray flags you with
`python main.py ic members floor add <member_id>` on his side, then
syncs to KV. Once synced, sign in to https://immersivecommons.com with
an email on your member record and visit
**[/floor10/agent-console](https://immersivecommons.com/floor10/agent-console)**.
The page mints `agt_*` tokens scoped to your `member_id`.

The plaintext token is shown to you exactly once. Copy it to your
agent's secret store; the server only keeps a SHA-256 of it.

## Endpoint

```
POST https://immersivecommons.com/api/ingest/highlights/pending
Authorization: Bearer agt_<your-token>
Content-Type: application/json
```

### Request body

Either of these is accepted:

```json
{ "story": <HighlightStory> }
```

```json
<HighlightStory>
```

### `HighlightStory` schema (strict mode)

| field | type | required | notes |
|---|---|---|---|
| `id` | string slug | ✓ | Lowercase, alphanumeric + hyphens, ≤120 chars. Stable per submission — re-POSTing the same `id` overwrites the pending record. Convention: `YYYY-MM-DD-<member-slug>-<event-slug>`. |
| `member_name` | string | ✓ | Display name. Capitalised as you'd want it on the card. |
| `member_id` | string | optional | Your slug from `members.yaml`; sets the kicker member chip. |
| `action` | string | ✓ | Verb-clause that completes "<member> <action> <event_title>". E.g. `"spoke at"`, `"hosted"`, `"demoed at"`, `"moderated"`. Lower-case so the kiosk can transform-uppercase. |
| `event_title` | string | ✓ | ≤200 chars. |
| `event_url` | string | optional | Source URL (Luma event, LinkedIn post, etc.). |
| `date` | string | ✓ | Display string (`"MAY 08"`) or ISO date — the renderer normalises either. |
| `dek` | string | ✓ | News-wire third-person, 1-2 sentences. ≤800 chars. **No editorial verbs** (`Ray's pitch`, `Ray argued`); **no first-person**. |
| `stats` | `Array<{label,value}>` | optional | ≤6 entries. Convention: `RSVPS / ORGANIZATION / ROLE`. The kiosk auto-appends `DATE`. |
| `images` | `string[]` | ✓ | 1-8 URLs. **Candids first, posters last.** Lead card cycles through; briefs render `images[0]`. |
| `image_focals` | `Record<path, {focalX,focalY}>` | optional | Subject focal points in `[0,1]`. Optional — the renderer falls back to a rule-of-thirds upper bias when missing. Useful if you've already detected faces and want the cycler to lock the subject. |

## Behaviour

- **Validation**: every required field is checked server-side. A missing
  `dek`, missing `images`, malformed `id`, or oversized payload returns
  `400` with the specific reason. Strict-mode is intentional — admins
  shouldn't have to write copy for you.

- **Rate limit**: **3 submissions per token per UTC day**. Hit it and
  you get `429` with a `Retry-After` header. Counter rolls at UTC
  midnight.

- **Idempotency**: re-POSTing the same `id` overwrites the pending
  record (same TTL refresh). Useful if you fix a typo and resubmit.

- **TTL**: pending records auto-expire after 7 days. If you submit and
  no admin acts, your story is dropped silently. Ping Ray on TG.

- **Approval**: once submitted, the record sits in
  `floor10:highlights:pending:*`. An admin reviews at
  `/floor10/admin/highlights` and approves (flows into the live
  4-card list, with overflow rolling into the archive) or rejects.

## Response shapes

### 202 Accepted (queued)

```json
{
  "ok": true,
  "id": "2026-05-08-rayyan-zahid-ai-extension-launch",
  "status": "pending",
  "rate": { "current": 1, "remaining": 2, "limit": 3 },
  "expires_at": "2026-05-15T18:42:11.812Z"
}
```

### 400 / 401 / 403 / 413 / 429

```json
{ "ok": false, "error": "<human-readable reason>" }
```

`401` with `error: "missing bearer token"` / `"malformed token"` /
`"unknown token"` / `"token revoked"` — re-mint or check your secret
store.

`403` — your token's scope doesn't permit this surface. Today only
`highlights:submit` exists.

`429` — wait for the `Retry-After` window (24h).

## Python helper

```python
from floor10_highlights.submit_pending import submit_pending
from floor10_highlights.models import HighlightStory, HighlightStat
import os

story = HighlightStory(
    id="2026-05-08-rayyan-zahid-ai-extension-launch",
    member_name="Rayyan Zahid",
    member_id="rayyan-zahid",
    action="demoed at",
    event_title="AI Extension Launch",
    date="2026-05-08",
    dek="The Immersive Commons facilitator launched the floor's "
        "AI Extension at a packed FT10 panel.",
    stats=[
        HighlightStat(label="RSVPS", value="47"),
        HighlightStat(label="ORGANIZATION", value="Immersive Commons"),
        HighlightStat(label="ROLE", value="host"),
    ],
    images=[
        "https://.../candid-01.jpg",
        "https://.../candid-02.jpg",
        "https://.../poster.jpg",
    ],
)

res = submit_pending(token=os.environ["FLOOR10_AGENT_TOKEN"], story=story)
if not res.ok:
    raise SystemExit(f"submission failed (HTTP {res.http_status}): {res.error}")
print(f"queued: id={res.id} ({res.rate_current}/{res.rate_limit} this UTC day)")
```

## Hard rules (mirror what the floor10-highlights agent enforces)

1. **No invented dates.** If you don't have a date, ask the human; don't
   default to today.
2. **No editorial verbs in the dek.** News-wire third-person only.
3. **No paraphrased quotes.** If the member said something, link to the
   recording — don't write a fake quote.
4. **Candids first, poster last.** The lead card cycles. Posters look
   like ads.

These aren't enforced by the schema (the server can't tell), but rejection at
moderation will cite them.
