MEMBERS WIRE — agent submissions
Public API for floor members' agents to submit highlight cards to the moderation queue.
You landed on this page — here's what to do
- Fetch
/llms.txtand/.well-known/ai-agent.json. Together they tell you every URL that matters here. - Check the human's
FLOOR10_AGENT_TOKENenv var. If it's set, you can call the API. Skip to step 4. - If missing, surface this to the human: "To grant me submission access, please sign in to /floor10/agent-console with your member email and click Mint token. Copy the plaintext (shown once) and set
FLOOR10_AGENT_TOKENin your shell." Mint requires a human Clerk session by design — you cannot self-mint. - Confirm auth with a no-op:
GET /api/ingest/highlights/pendingwithAuthorization: Bearer $FLOOR10_AGENT_TOKEN. On{ ok, count }you're good. - Compose a
HighlightStoryper the schema below; POST to/api/ingest/highlights/pending. On202, 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 just type /floor10-submit when you have a moment to share. It walks you through metadata extraction, dek drafting, preview + submit.
# 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 guide (Windows + token setup): /skills/floor10-submit/INSTALL.md · Skill source: /skills/floor10-submit/SKILL.md
Eligibility
Floor membership is admin-assigned. Once Ray flags you on the life/-side and syncs to KV, sign in to /floor10/agent-console with an email on your member record. 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.
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:
{ "story": <HighlightStory> }<HighlightStory>
HighlightStory schema (strict)
| Field | Type | Req | 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 as you'd want it on the card. |
member_id | string | — | Your slug from members.yaml. |
action | string | ✓ | Verb-clause completing "<member> <action> <event_title>". Lowercase. E.g. spoke at, hosted, demoed at, moderated. |
event_title | string | ✓ | ≤200 chars. |
event_url | string | — | Source URL. |
date | string | ✓ | Display (MAY 08) or ISO. |
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}> | — | ≤6 entries. Convention: RSVPS / ORGANIZATION / ROLE. |
images | string[] | ✓ | 1-8 URLs. Candids first, posters last. |
image_focals | Record<path, {focalX,focalY}> | — | Subject focal points in [0,1]. Renderer falls back to a rule-of-thirds upper bias if missing. |
Behaviour
- Validation: every required field is checked server-side. Missing
dek, missingimages, malformedid, or oversized payload returns 400. - Rate limit: 3 submissions per token per UTC day. Over → 429 with
Retry-After. Counter rolls at UTC midnight. - Idempotency: re-POSTing the same id overwrites the pending record (TTL refreshes).
- TTL: pending records auto-expire after 7 days. If no admin acts, the story is dropped — ping Ray.
- Approval: a human admin reviews at
/floor10/admin/highlightsand approves (flow into live 4-card list) or rejects (drop + audit).
Response shapes
202 Accepted (queued)
{
"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"
}4xx
{ "ok": false, "error": "<reason>" }401 — missing / malformed / unknown / revoked token. Re-mint at /floor10/agent-console if you lost it.
403— token scope doesn't permit this surface. The submission flow requires events:submit_recap (legacy alias highlights:submit). See /.well-known/ai-agent.json for the full scope catalog (31 advertised) mapped to the 5-tier ladder.
429 — wait the Retry-After window.
Python helper
Pure standard library, no packages to install. Reads the token from FLOOR10_AGENT_TOKEN and POSTs a bare HighlightStory (the alternate body root the endpoint accepts).
import json, os, urllib.request, urllib.error
ENDPOINT = "https://www.immersivecommons.com/api/ingest/highlights/pending"
story = {
"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": [
{"label": "RSVPS", "value": "47"},
{"label": "ORGANIZATION", "value": "Immersive Commons"},
{"label": "ROLE", "value": "host"},
],
"images": [
"https://.../candid-01.jpg",
"https://.../candid-02.jpg",
"https://.../poster.jpg",
],
}
req = urllib.request.Request(
ENDPOINT,
data=json.dumps(story).encode("utf-8"),
headers={
"Authorization": "Bearer " + os.environ["FLOOR10_AGENT_TOKEN"],
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req) as resp:
body = json.load(resp)
rate = body.get("rate", {})
print(f"queued: {body['id']} "
f"({rate.get('current')}/{rate.get('limit')} this UTC day)")
except urllib.error.HTTPError as e:
detail = e.read().decode("utf-8", "replace")
raise SystemExit(f"submission failed (HTTP {e.code}): {detail}")Signed requests (optional security upgrade)
A bearer agent token is enough to call the surface. If the token leaks, anyone holding the plaintext can act as your agent until you revoke it. To close that gap, IC supports an RFC 9421 strict-Ed25519 upgrade: bind a public key to the token, set requires_signature: true, and the server rejects every call that doesn't carry a fresh Ed25519 signature over a canonical signature base. A leaked bearer is then useless without the private key your agent generated locally.
Setup is human-gated by design — only Clerk-authenticated requests can bind, revoke, or toggle a key, so a leaked bearer can't rebind itself.
Lifecycle
- Agent generates an Ed25519 keypair locally (Web Crypto, Python cryptography, OpenSSL — anything that produces an RFC 8037 JWK). Never ship the private half.
- Human signs in at
/settings, pastes the public-key JWK into the inline form on the token card, and clicks BIND KEY. Check the Enforce box to fliprequires_signature: truein the same step. The token card's badge flips fromBEARER ONLYtoSIGNED REQUIRED. - Agent signs every subsequent request. The covered fields are exactly
@method,@authority,@target-uri, and (on POST/PUT)content-digest. Thecreatedparam must be within ±60 seconds of server time. - Rotation: human clicks REVOKE KEY, regenerates keypair, pastes new public JWK, clicks BIND KEY again. The token sha256 stays stable; the bound key changes.
- Emergency downgrade: if the signing client breaks (clock drift, dependency bug), the human clicks RELAX to disable enforcement temporarily. Key stays bound. Re-enable with ENFORCE once fixed.
Endpoints (all Clerk-session-only)
POST /api/agent/keys/register— binds a JWK to one of your tokens. Body:{ sha256, pubkey_jwk, enforce? }. Refuses overwrite — revoke first to rotate.POST /api/agent/keys/revoke— clears the binding. Body:{ sha256 }. Token reverts to bearer-only.POST /api/agent/keys/enforce-toggle— flipsrequires_signaturewithout revoking the key. Body:{ sha256, enforce }. Idempotent (returnschanged: falseif already at the requested value).
Wire shape (the agent's side)
POST /api/ingest/highlights/pending HTTP/1.1
Host: www.immersivecommons.com
Authorization: Bearer agt_<base64url-32bytes>
Content-Type: application/json
Content-Digest: sha-256=:<base64-sha256-of-body>:
Signature-Input: sig1=("@method" "@authority" "@target-uri" "content-digest");created=1730000000;keyid="kid_abc";alg="ed25519"
Signature: sig1=:<base64-ed25519-sig-bytes>:
{"id": "...", "member_name": "...", ...}Strict subset: Ed25519 only, ±60s freshness, covered fields are exactly the canonical set in canonical order. No HMAC, no ECDSA, no RSA, no JWKS-URI fetch, no expires override. Skill walkthrough has full TypeScript and Python signing samples.
Hard rules
- No invented dates. Ask the human if you don't have one.
- No editorial verbs in the dek. News-wire third-person only.
- No paraphrased quotes. Link to the recording instead.
- Candids first, poster last. The lead card cycles.
The schema can't enforce these — the moderator will. Your submission gets rejected with a reason that cites the rule.