- Add ticker column to games table with migration - Bootstrap tickers from tickers.json config on startup - POST /api/votes/live accepts optional ticker field for direct game lookup (bypasses timestamp-interval matching) - Ticker votes work for any game, not just session games - Update API docs and add e2e tests for ticker voting - Version bump to 0.6.5 Made-with: Cursor
6.1 KiB
6.1 KiB
Votes Endpoints
Real-time popularity voting. Bots or integrations send votes during live gaming sessions. Two voting mechanisms are supported:
thisgame++/thisgame--— votes for the game currently being played, matched via timestamp intervals.- Ticker voting — votes for a specific game by its ticker symbol (e.g.
QPL3for Quiplash 3), regardless of what is currently being played.
Endpoint Summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/votes |
None | Paginated vote history with filtering |
| POST | /api/votes/live |
Bearer | Submit a live vote (up/down) |
GET /api/votes
Paginated vote history with filtering. Use query parameters to filter by session, game, username, or vote type.
Authentication
None.
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| session_id | int | No | — | Filter by session ID |
| game_id | int | No | — | Filter by game ID |
| username | string | No | — | Filter by voter username |
| vote_type | string | No | — | "up" or "down" |
| page | int | No | 1 | Page number |
| limit | int | No | 50 | Items per page (max 100) |
Response
200 OK
Results are ordered by timestamp DESC. The vote_type field is returned as "up" or "down" (not raw integers).
{
"votes": [
{
"id": 891,
"session_id": 5,
"game_id": 42,
"game_title": "Quiplash 3",
"pack_name": "Party Pack 7",
"username": "viewer123",
"vote_type": "up",
"timestamp": "2026-03-15T20:29:55.000Z",
"created_at": "2026-03-15T20:29:56.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 237,
"total_pages": 5
}
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "..." } |
Invalid session_id, game_id, or vote_type |
| 200 | { "votes": [], "pagination": { "page": 1, "limit": 50, "total": 0, "total_pages": 0 } } |
No results match the filters |
POST /api/votes/live
Submit a real-time up/down vote. Supports two independent voting mechanisms:
- Ticker voting — include a
tickerfield to vote for a specific game by symbol. The game is resolved globally and does not need to be in the active session. thisgame++/thisgame--voting — omittickerto vote for the game currently being played, matched via timestamp intervals againstsession_games.
Authentication
Bearer token required. Include in header: Authorization: Bearer <token>.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Identifier for the voter (used for deduplication) |
| vote | string | Yes | "up" or "down" |
| timestamp | string | Yes | ISO 8601 timestamp when the vote occurred |
| ticker | string | No | Ticker symbol identifying the game (e.g. QPL3, TMP2). When provided, the game is resolved by ticker and timestamp matching is skipped. |
Ticker vote:
{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z",
"ticker": "QPL3"
}
thisgame++/thisgame-- vote (no ticker):
{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z"
}
Behavior
- Finds the active session (single session with
is_active = 1). - With
ticker: Looks up the game globally by ticker symbol. The game does not need to be part of the active session. - Without
ticker: Matches the vote timestamp to the game being played at that time (uses interval between consecutiveplayed_attimestamps). - Updates game
upvotes,downvotes, andpopularity_scoreatomically in a transaction. - Deduplication: Rejects votes from the same username within a 1-second window (409 Conflict).
- Broadcasts a
vote.receivedWebSocket event to all clients subscribed to the active session. See WebSocket Protocol for event payload.
Response
200 OK
The ticker field is included in the response when the vote was submitted with a ticker.
{
"success": true,
"message": "Vote recorded successfully",
"session": { "id": 3, "games_played": 5 },
"game": {
"id": 42,
"title": "Quiplash 3",
"upvotes": 11,
"downvotes": 2,
"popularity_score": 9
},
"vote": {
"username": "viewer123",
"type": "up",
"timestamp": "2026-03-15T20:30:00Z",
"ticker": "QPL3"
}
}
Error Responses
| Status | Body | When |
|---|---|---|
| 400 | { "error": "Missing required fields: username, vote, timestamp" } |
Missing required fields |
| 400 | { "error": "vote must be either \"up\" or \"down\"" } |
Invalid vote value |
| 400 | { "error": "Invalid timestamp format. Use ISO 8601 format (e.g., 2025-11-01T20:30:00Z)" } |
Invalid timestamp |
| 404 | { "error": "No active session found" } |
No session with is_active = 1 |
| 404 | { "error": "Unknown ticker 'XYZ'" } |
Ticker does not match any game |
| 404 | { "error": "No games have been played in the active session yet" } |
Active session has no games (timestamp voting only) |
| 404 | { "error": "Vote timestamp does not match any game in the active session", "debug": { ... } } |
Timestamp outside any game interval (timestamp voting only) |
| 409 | { "error": "Duplicate vote detected (within 1 second of previous vote)", "message": "Please wait at least 1 second between votes", "timeSinceLastVote": 0.5 } |
Same username voted within 1 second |
| 500 | { "error": "..." } |
Server error |
Examples
Ticker vote:
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z",
"ticker": "QPL3"
}'
thisgame++ vote (no ticker):
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "viewer123",
"vote": "up",
"timestamp": "2026-03-15T20:30:00Z"
}'