{"openapi":"3.1.0","info":{"title":"Unspin public API","version":"1.0.0","description":"Real-or-fake news cards. Reads are public and cacheable; writes are anonymous via x-session-id headers."},"servers":[{"url":"https://unspin.hidden-pond-5e6a.workers.dev"}],"paths":{"/api/today":{"get":{"summary":"Newest active stories","parameters":[{"name":"date","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"description":"Replay a dated set instead of the live pool."}],"responses":{"200":{"description":"{ dataset, stories[] }"}}}},"/api/feed":{"get":{"summary":"Continuous story stream","parameters":[{"name":"mode","in":"query","required":false,"schema":{"type":"string","enum":["latest","recent","archive"]},"description":"latest=60, recent=150, archive=300 stories."},{"name":"date","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD replays one dated set."}],"responses":{"200":{"description":"{ datasets, stories[] }"}}}},"/api/headlines":{"get":{"summary":"Compact headline list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":300}}],"responses":{"200":{"description":"{ headlines: [{ id, headline, date, image }] }"}}}},"/api/dates":{"get":{"summary":"Available dated sets","responses":{"200":{"description":"{ dates: [{ id, date }] } newest first"}}}},"/api/leaderboard":{"get":{"summary":"Top desks","parameters":[{"name":"datasetId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"{ entries: [{ name, score, correct, total }] }"}}}},"/api/rogue/board":{"get":{"summary":"Best rogue streaks","responses":{"200":{"description":"{ entries: [{ name, score }] } top 10"}}}},"/api/stats":{"get":{"summary":"Aggregate answer stats","responses":{"200":{"description":"{ sessions, byVerdict[{ verdict, shown, caught }], perStory[] }"}}}},"/api/health":{"get":{"summary":"Liveness probe","responses":{"200":{"description":"{ ok, stories, lastRun, feedback }"}}}},"/api/comments":{"get":{"summary":"Story comments with votes, newest first","parameters":[{"name":"storyId","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ items: [{ id, name, text, createdAt, parentId, likes, dislikes, myVote, mine }] } (myVote/mine need x-session-id)"}}},"post":{"summary":"Post a comment or one-level reply (anonymous, 10/day)","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."},{"name":"x-player-name","in":"header","required":false,"schema":{"type":"string","maxLength":40},"description":"Auto player tag shown on boards."}],"responses":{"200":{"description":"{ ok, id } — body { storyId, text, parentId? }"}}}},"/api/votes":{"post":{"summary":"Like (1), dislike (-1), or retract (0) a comment. Idempotent per session.","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."}],"responses":{"200":{"description":"{ ok, likes, dislikes }"}}}},"/api/plays":{"post":{"summary":"Submit a score (server-graded: score = correct * 100)","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."},{"name":"x-player-name","in":"header","required":false,"schema":{"type":"string","maxLength":40},"description":"Auto player tag shown on boards."}],"responses":{"200":{"description":"{ ok, id }"}}}},"/api/answers":{"post":{"summary":"Log per-answer choices (graded against stored verdicts)","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."}],"responses":{"200":{"description":"{ ok, count }"}}}},"/api/rogue/finish":{"post":{"summary":"Save a rogue best streak (keeps the max)","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."},{"name":"x-player-name","in":"header","required":false,"schema":{"type":"string","maxLength":40},"description":"Auto player tag shown on boards."}],"responses":{"200":{"description":"{ ok, best, newBest }"}}}},"/api/feedback":{"post":{"summary":"Anonymous feedback inbox (20/day)","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."}],"responses":{"200":{"description":"{ ok, id }"}}}},"/feed.xml":{"get":{"summary":"RSS 2.0 of the latest 40 cards","responses":{"200":{"description":"RSS XML with verdict categories"}}}},"/feed.json":{"get":{"summary":"JSON Feed 1.1 of the latest 40 cards","responses":{"200":{"description":"JSON Feed with _unspin extension block"}}}}},"components":{"schemas":{"Story":{"type":"object","properties":{"id":{"type":"string"},"headline":{"type":"string"},"dek":{"type":"string"},"body":{"type":"string"},"verdict":{"type":"string","enum":["real","fake"]},"technique":{"type":"string"},"technique_label":{"type":"string"},"outlet":{"type":"string"},"domain":{"type":"string"},"image_src":{"type":["string","null"]},"image_caption":{"type":["string","null"]},"video_src":{"type":["string","null"]},"youtube_id":{"type":["string","null"]},"fact_check_url":{"type":["string","null"]},"lesson":{"type":"string"},"set_date":{"type":"string"}}}}}}