{"openapi":"3.1.0","info":{"title":"Unspin (unspin.app) — fake-news game API","version":"1.0.0","description":"Unspin (unspin.app) — the real-or-fake news game: one free round, then unlimited play with Premium. Real-or-fake news cards. Reads are public and cacheable; writes are anonymous via x-session-id headers."},"servers":[{"url":"https://www.unspin.app"}],"paths":{"/api/profile":{"get":{"summary":"Personal judgment and progress dashboard","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"First-encounter accuracy, recent trend, five-level alignment, topic/technique/tag strengths, same-story peers, active decision timing and activity."},"401":{"description":"Missing player session"}}}},"/api/stories/{id}/source":{"get":{"summary":"Captured original source for a story","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"url","in":"query","required":false,"schema":{"type":"string"},"description":"URL of a captured source in the story evidence."}],"responses":{"200":{"description":"{ url, title, text, captured }. Empty text means no stored capture is available."},"404":{"description":"Story or source unavailable"}}}},"/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."},{"name":"locale","in":"query","required":false,"schema":{"type":"string","enum":["en","es","fr","de","pt","ar","zh"]},"description":"Card language (headline/dek/body/lesson). Falls back to English per card."}],"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."},{"name":"locale","in":"query","required":false,"schema":{"type":"string","enum":["en","es","fr","de","pt","ar","zh"]},"description":"Card language (headline/dek/body/lesson). Falls back to English per card."}],"responses":{"200":{"description":"{ datasets, stories[] }"}}}},"/api/headlines":{"get":{"summary":"Compact headline list","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":300}},{"name":"locale","in":"query","required":false,"schema":{"type":"string","enum":["en","es","fr","de","pt","ar","zh"]},"description":"Headline language. Falls back to English per row."}],"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":"Legacy play-total leaderboard; use /api/leaderboard/weekly","deprecated":true,"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/leaderboard/weekly":{"get":{"summary":"Weekly first-call leaderboard with unique public usernames","description":"100 points per correct first answer; each story counts once per player across all weeks. Accuracy breaks ties. Monday 00:00 UTC reset. Cache-Control: no-store.","parameters":[{"name":"x-session-id","in":"header","required":false,"schema":{"type":"string"},"description":"Include to receive self and isSelf."}],"responses":{"200":{"description":"{ weekStart, weekEnd, participants, entries: [{ rank, name, score, correct, total, accuracy, isSelf }], self }. Entries contain at most 50; self includes callers outside the top 50. Private IDs are omitted."}}}},"/api/player/identity":{"get":{"summary":"Get or allocate your automatic public username","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":"{ username, customized }; uncached."},"401":{"description":"Missing session"}}},"put":{"summary":"Choose or change your unique public username","parameters":[{"name":"x-session-id","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128},"description":"Anonymous client uuid. No login, no PII."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username"],"properties":{"username":{"type":"string","description":"3–20 ASCII letters/numbers/underscores, starting with a letter. Case-insensitive; leading @ accepted."}}}}}},"responses":{"200":{"description":"{ username, customized: true }; scores and private identity preserved."},"400":{"description":"Invalid or reserved username"},"401":{"description":"Missing session"},"409":{"description":"Username already taken"}}}},"/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/wire":{"get":{"summary":"Community wire: anonymous posts with inline replies, newest first","parameters":[{"name":"before","in":"query","required":false,"schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"{ items: [{ id, name, text, createdAt, likes, dislikes, myVote, mine, replies[] }], next } (myVote/mine need x-session-id)"}}},"post":{"summary":"Post to the wire or reply one level deep (anonymous, 20/day, 280 chars)","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 { text, parentId? }"}}},"delete":{"summary":"Delete your own wire post (replies go with it)","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":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok }"}}}},"/api/wire/vote":{"post":{"summary":"Like (1), dislike (-1), or retract (0) a wire post. 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/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"}}}}}}