Spaces:
Runtime error
Runtime error
| title: "Gamification & Leaderboard System" | |
| version: 3.8.2 | |
| lastUpdated: 2026-05-19 | |
| # Gamification & Leaderboard System | |
| > **Source of truth:** `src/lib/gamification/`, `src/lib/db/gamification.ts`, `src/app/api/gamification/` | |
| > **Last updated:** 2026-05-19 — v3.8.0 | |
| OmniRoute includes a local-first gamification layer that rewards users for | |
| engaging with the platform — making requests, switching providers, creating | |
| combos, sharing tokens, and contributing to the community. All state lives in | |
| SQLite; federation with community servers is opt-in and push-based. | |
| The system is designed to be **zero-latency on the hot path** — gamification | |
| events are dispatched fire-and-forget from the request pipeline and never block | |
| an LLM response. | |
| --- | |
| ## Overview | |
| ### Purpose | |
| Increase user engagement and retention by providing visible progress (XP, | |
| levels, badges), social proof (leaderboards), and economic incentives (token | |
| sharing, invite rewards). | |
| ### Scope | |
| | Feature | Description | | |
| | ----------------- | --------------------------------------------------------------- | | |
| | XP & Levels | Earn XP per action; level up along a polynomial curve | | |
| | Badges | 20+ achievements across 5 categories with 4 rarity tiers | | |
| | Streaks | Daily active usage tracking with current/longest streak | | |
| | Leaderboards | Global, weekly, monthly, token-sharing, and contribution scopes | | |
| | Token Sharing | Transfer credits between users via double-entry ledger | | |
| | Invite & Redeem | Referral codes with SHA-256 hashed storage | | |
| | Community Servers | Federate with external OmniRoute instances | | |
| | Anti-Cheat | Server-side scoring, rate limiting, z-score anomaly detection | | |
| ### Design Principles | |
| 1. **Local-first** — all state in SQLite, no external services required. | |
| 2. **Non-blocking** — events are fire-and-forget; the LLM response path is | |
| never delayed by gamification logic. | |
| 3. **Server-authoritative** — XP is computed server-side only; clients cannot | |
| inflate scores. | |
| 4. **Privacy-respecting** — leaderboard participation is opt-in; users can | |
| hide their profile. | |
| 5. **Federation-ready** — community servers can push scores via signed API; | |
| sync is overwrite, not additive. | |
| --- | |
| ## Architecture | |
| ### High-Level Flow | |
| ``` | |
| Client Request | |
| → /v1/chat/completions | |
| → handleChatCore() [open-sse/handlers/chatCore.ts] | |
| → ... (existing pipeline) ... | |
| → upstream response sent to client | |
| → setImmediate (fire-and-forget): | |
| → emitGamificationEvent() [src/lib/gamification/events.ts] | |
| → awardXp() [src/lib/gamification/xp.ts] | |
| → updateStreak() [src/lib/gamification/streaks.ts] | |
| → evaluateBadges() [src/lib/gamification/badges.ts] | |
| → updateLeaderboard() [src/lib/gamification/leaderboard.ts] | |
| → checkAnomalies() [src/lib/gamification/antiCheat.ts] | |
| ``` | |
| The event emitter is the single integration point. `chatCore.ts` calls | |
| `emitGamificationEvent()` after the response is sent; the event module fans | |
| out to XP, streak, badge, leaderboard, and anti-cheat subsystems. | |
| ### Module Dependency Graph | |
| ``` | |
| src/lib/gamification/ | |
| events.ts ← entry point (called from chatCore.ts) | |
| ├── xp.ts ← XP calculation & level resolution | |
| ├── streaks.ts ← daily active streak tracking | |
| ├── badges.ts ← badge criteria evaluation | |
| ├── leaderboard.ts ← rank computation & SSE broadcasting | |
| ├── antiCheat.ts ← rate limiting & anomaly detection | |
| ├── sharing.ts ← token transfer ledger | |
| ├── invites.ts ← invite/redeem code management | |
| ├── servers.ts ← community server federation | |
| └── notifications.ts ← SSE notification stream | |
| src/lib/db/ | |
| gamification.ts ← all CRUD operations (8 tables) | |
| src/app/api/gamification/ | |
| leaderboard/ ← GET rankings, POST manual refresh | |
| leaderboard/stream ← SSE real-time updates | |
| transfer/ ← GET history, POST send tokens | |
| invite/ ← GET/POST codes, DELETE revoke | |
| invite/redeem/ ← POST redeem a code | |
| servers/ ← GET/POST/DELETE community servers | |
| federation/score/ ← POST push score to server | |
| federation/leaderboard/ ← GET pull leaderboard from server | |
| notifications/ ← SSE badge/level-up notifications | |
| anomalies/ ← GET anomaly reports (admin) | |
| rotate/ ← POST rotate invite token secrets | |
| ``` | |
| --- | |
| ## Data Layer | |
| ### Database Tables | |
| All tables live in the main OmniRoute SQLite database, created by migration | |
| `060_create_gamification.sql`. WAL journaling is inherited from the singleton | |
| `getDbInstance()` in `src/lib/db/core.ts`. | |
| ``` | |
| ┌─────────────────────────┐ ┌──────────────────────────┐ | |
| │ leaderboard │ │ user_levels │ | |
| ├─────────────────────────┤ ├──────────────────────────┤ | |
| │ id TEXT PK │ │ api_key_id TEXT PK │ | |
| │ api_key_id TEXT │ │ xp INTEGER │ | |
| │ scope TEXT │ │ level INTEGER │ | |
| │ score INTEGER │ │ title TEXT │ | |
| │ period TEXT │ │ updated_at TEXT │ | |
| │ updated_at TEXT │ └──────────────────────────┘ | |
| └─────────────────────────┘ | |
| │ | |
| │ 1:N | |
| ▼ | |
| ┌─────────────────────────┐ ┌──────────────────────────┐ | |
| │ user_badges │ │ badge_definitions │ | |
| ├─────────────────────────┤ ├──────────────────────────┤ | |
| │ id TEXT PK │ │ id TEXT PK │ | |
| │ api_key_id TEXT │ │ name TEXT │ | |
| │ badge_id TEXT FK │ │ category TEXT │ | |
| │ earned_at TEXT │ │ rarity TEXT │ | |
| │ notified INTEGER │ │ criteria_type TEXT │ | |
| └─────────────────────────┘ │ criteria TEXT(JSON) │ | |
| │ description TEXT │ | |
| │ icon TEXT │ | |
| │ hidden INTEGER │ | |
| └──────────────────────────┘ | |
| ┌─────────────────────────┐ ┌──────────────────────────┐ | |
| │ xp_audit_log │ │ token_ledger │ | |
| ├─────────────────────────┤ ├──────────────────────────┤ | |
| │ id TEXT PK │ │ id TEXT PK │ | |
| │ api_key_id TEXT │ │ from_key_id TEXT │ | |
| │ action TEXT │ │ to_key_id TEXT │ | |
| │ xp_awarded INTEGER │ │ amount INTEGER │ | |
| │ metadata TEXT(JSON)│ │ idempotency_key TEXT UQ │ | |
| │ created_at TEXT │ │ created_at TEXT │ | |
| └─────────────────────────┘ └──────────────────────────┘ | |
| ┌─────────────────────────┐ ┌──────────────────────────┐ | |
| │ invite_tokens │ │ community_servers │ | |
| ├─────────────────────────┤ ├──────────────────────────┤ | |
| │ id TEXT PK │ │ id TEXT PK │ | |
| │ api_key_id TEXT │ │ name TEXT │ | |
| │ code TEXT UQ │ │ url TEXT │ | |
| │ token_hash TEXT │ │ token_hash TEXT │ | |
| │ uses INTEGER │ │ status TEXT │ | |
| │ max_uses INTEGER │ │ last_sync TEXT │ | |
| │ created_at TEXT │ │ created_at TEXT │ | |
| │ expires_at TEXT │ └──────────────────────────┘ | |
| └─────────────────────────┘ | |
| ``` | |
| ### Domain Module: `src/lib/db/gamification.ts` | |
| Follows the standard OmniRoute pattern — imports `getDbInstance()` from | |
| `core.ts`, exports typed CRUD functions. No raw SQL in route handlers. | |
| Key functions: | |
| | Function | Description | | |
| | -------------------------- | ------------------------------------------------------ | | |
| | `upsertLeaderboardEntry()` | Insert or update score for (api_key_id, scope, period) | | |
| | `getLeaderboard()` | Paginated rankings for a given scope/period | | |
| | `getUserLevel()` | Get or create user level record | | |
| | `updateUserLevel()` | Set XP, level, and title atomically | | |
| | `getBadgeDefinitions()` | All badge definitions (optionally filtered) | | |
| | `getUserBadges()` | Badges earned by a user | | |
| | `awardBadge()` | Insert badge earn (idempotent on badge_id) | | |
| | `logXpAction()` | Append to xp_audit_log | | |
| | `getXpAuditLog()` | Paginated audit history for a user | | |
| | `insertLedgerEntry()` | Double-entry transfer (in transaction) | | |
| | `getBalance()` | Sum of received minus sent for a user | | |
| | `getTransferHistory()` | Paginated transfer log | | |
| | `createInviteToken()` | Insert invite code + hashed token | | |
| | `redeemInviteToken()` | Look up by code, validate, increment uses | | |
| | `upsertCommunityServer()` | Register or update a federation server | | |
| | `getCommunityServers()` | List servers for a user | | |
| | `deleteCommunityServer()` | Remove a server registration | | |
| --- | |
| ## XP / Level System | |
| **File:** `src/lib/gamification/xp.ts` | |
| ### Level Curve | |
| The XP required to reach level `n` follows a polynomial curve: | |
| ``` | |
| xp_for_level(n) = floor(100 * n^1.5) | |
| ``` | |
| | Level | XP to Next | Cumulative XP | Title | | |
| | ----- | ---------- | ------------- | -------- | | |
| | 1 | 100 | 100 | Beginner | | |
| | 5 | 1,118 | 2,415 | Beginner | | |
| | 10 | 3,162 | 10,523 | Explorer | | |
| | 25 | 12,500 | 86,024 | Explorer | | |
| | 50 | 35,355 | 345,529 | Expert | | |
| | 75 | 64,952 | 948,683 | Master | | |
| | 100 | 100,000 | 2,050,000 | Legend | | |
| ### Titles | |
| | Level Range | Title | | |
| | ----------- | -------- | | |
| | 1 – 9 | Beginner | | |
| | 10 – 24 | Explorer | | |
| | 25 – 49 | Expert | | |
| | 50 – 74 | Master | | |
| | 75 – 100 | Legend | | |
| ### XP Rewards | |
| | Action | XP | Description | | |
| | ------------------ | --- | --------------------------------------------------------- | | |
| | `request` | 1 | Per successful LLM request | | |
| | `provider_switch` | 5 | Switching to a different provider | | |
| | `combo_create` | 10 | Creating a new combo configuration | | |
| | `combo_use` | 2 | Using a combo (per target hit) | | |
| | `badge_earned` | 25 | Earning any badge | | |
| | `streak_milestone` | 15 | Reaching a streak milestone (7, 14, 30, 60, 90, 180, 365) | | |
| | `referral` | 50 | Successfully referring a new user | | |
| | `token_share` | 5 | Sharing tokens with another user | | |
| | `daily_login` | 3 | First request of the day | | |
| | `model_diversity` | 3 | Using a model not used in the past 7 days | | |
| | `compression_use` | 2 | Using prompt compression | | |
| | `skill_use` | 2 | Executing a skill via MCP | | |
| ### Award Flow | |
| ```typescript | |
| export async function awardXp( | |
| apiKeyId: string, | |
| action: XpAction, | |
| metadata?: Record<string, unknown> | |
| ): Promise<{ xp: number; level: number; title: string; levelUp: boolean }>; | |
| ``` | |
| 1. Look up `XP_REWARDS[action]` to get the XP amount. | |
| 2. Pass through `checkRateLimit()` (anti-cheat: max 1000 XP/min per key). | |
| 3. Open a transaction: | |
| - Read current `user_levels` row. | |
| - Add XP; recompute level via `levelFromXp(totalXp)`. | |
| - If level changed, set `levelUp = true`. | |
| - Update `user_levels` row. | |
| - Insert into `xp_audit_log`. | |
| 4. Return the result. Caller handles notifications. | |
| ### Helper: `levelFromXp(totalXp)` | |
| Iterates level 1..100, summing `xp_for_level(n)` until the cumulative XP | |
| exceeds `totalXp`. Returns the highest level whose threshold is met. | |
| This is O(100) — acceptable since levels cap at 100. | |
| --- | |
| ## Badge System | |
| **File:** `src/lib/gamification/badges.ts` | |
| ### Categories | |
| | Category | Description | Example Badges | | |
| | -------------- | ---------------------------------- | --------------------------------- | | |
| | `usage` | Volume-based milestones | First Request, 1K Requests, 100K | | |
| | `sharing` | Token sharing and referrals | First Share, Generous (10 shares) | | |
| | `contribution` | Community engagement | Combo Creator, Provider Explorer | | |
| | `streak` | Consistency over time | Week Warrior, Monthly Devoted | | |
| | `rare` | Hard-to-get or hidden achievements | Early Adopter, Bug Reporter | | |
| ### Rarities | |
| | Rarity | Color | Probability Hint | | |
| | ----------- | ----- | ---------------- | | |
| | `common` | Gray | Most users | | |
| | `uncommon` | Green | Active users | | |
| | `rare` | Blue | Dedicated users | | |
| | `legendary` | Gold | Top 1% | | |
| ### Criteria Types | |
| | Type | Field | Description | | |
| | -------------- | ------------ | ----------------------------------------------- | | |
| | `action_count` | `count` | Perform action N times (e.g., 1000 requests) | | |
| | `streak` | `days` | Maintain streak for N consecutive days | | |
| | `unique_count` | `field`, `n` | Use N unique values (e.g., 10 different models) | | |
| | `rank` | `scope`, `n` | Reach rank N on a leaderboard scope | | |
| | `first` | — | Be the first to perform an action | | |
| | `hidden` | (varies) | Criteria not shown until earned | | |
| Badge definitions are stored in `badge_definitions` as JSON `criteria`: | |
| ```json | |
| { | |
| "type": "action_count", | |
| "action": "request", | |
| "count": 1000 | |
| } | |
| ``` | |
| ### Evaluation Flow | |
| ``` | |
| emitGamificationEvent(event) | |
| → evaluateBadges(apiKeyId, event) | |
| → getBadgeDefinitions() # all definitions | |
| → getUserBadges(apiKeyId) # already earned (skip) | |
| → for each unearned badge: | |
| → matchesCriteria(badge, event, userState) | |
| → if match: awardBadge(apiKeyId, badgeId) | |
| → return notification payload | |
| ``` | |
| Evaluation is **event-driven** — it runs after every gamification event, but | |
| only checks badges whose `criteria.type` aligns with the event action. This | |
| keeps evaluation fast (< 5ms for most events). | |
| ### `matchesCriteria(badge, event, userState)` | |
| | Criteria Type | Check | | |
| | -------------- | -------------------------------------------------- | | |
| | `action_count` | `getActionCount(apiKeyId, action) >= count` | | |
| | `streak` | `getCurrentStreak(apiKeyId) >= days` | | |
| | `unique_count` | `getUniqueCount(apiKeyId, field) >= n` | | |
| | `rank` | `getRank(apiKeyId, scope) <= n` | | |
| | `first` | No prior `xp_audit_log` entry for this action type | | |
| | `hidden` | Delegates to the appropriate sub-check | | |
| ### Built-in Badges (20+) | |
| <details> | |
| <summary>Full badge list</summary> | |
| | Badge | Category | Rarity | Criteria | | |
| | ------------------- | ------------ | --------- | ---------------------------- | | |
| | First Steps | usage | common | 1 request | | |
| | Getting Warmed Up | usage | common | 100 requests | | |
| | Power User | usage | uncommon | 1,000 requests | | |
| | Centurion | usage | rare | 10,000 requests | | |
| | OmniPower | usage | legendary | 100,000 requests | | |
| | Provider Hopper | contribution | common | Use 5 different providers | | |
| | Provider Master | contribution | uncommon | Use 20 different providers | | |
| | Combo Architect | contribution | uncommon | Create 5 combos | | |
| | Combo Grandmaster | contribution | rare | Create 25 combos | | |
| | First Share | sharing | common | 1 token transfer | | |
| | Generous | sharing | uncommon | 10 token transfers | | |
| | Philanthropist | sharing | rare | Transfer 10,000 tokens total | | |
| | Referrer | sharing | common | 1 successful referral | | |
| | Network Builder | sharing | uncommon | 10 successful referrals | | |
| | Week Warrior | streak | uncommon | 7-day streak | | |
| | Monthly Devoted | streak | rare | 30-day streak | | |
| | Unstoppable | streak | legendary | 365-day streak | | |
| | Early Adopter | rare | legendary | Join during beta period | | |
| | Compression Pioneer | rare | uncommon | Use compression 100 times | | |
| | Skill Collector | rare | rare | Use 10 different skills | | |
| | Model Explorer | contribution | uncommon | Use 15 different models | | |
| </details> | |
| --- | |
| ## Streak Tracker | |
| **File:** `src/lib/gamification/streaks.ts` | |
| ### Data Model | |
| Streaks are stored in the `key_value` table (shared utility table) under | |
| namespaced keys: | |
| | Key | Value | Description | | |
| | ----------------------------- | -------------------------------- | ------------------ | | |
| | `gamification:streak:{keyId}` | `{current},{longest},{lastDate}` | Active streak data | | |
| ### Logic | |
| ```typescript | |
| export async function updateStreak( | |
| apiKeyId: string | |
| ): Promise<{ current: number; longest: number; milestone: boolean }>; | |
| ``` | |
| 1. Read streak record from `key_value`. | |
| 2. Parse `{current}`, `{longest}`, `{lastDate}` (ISO date string). | |
| 3. If `lastDate === today` — no change (already counted today). | |
| 4. If `lastDate === yesterday` — increment `current`; update `longest` if needed. | |
| 5. If `lastDate < yesterday` — reset `current = 1` (streak broken). | |
| 6. Write updated record. | |
| 7. Check milestones: 7, 14, 30, 60, 90, 180, 365 days. If crossed, set | |
| `milestone = true` (caller awards XP and checks badges). | |
| ### Edge Cases | |
| - **Timezone**: streaks use UTC dates (`new Date().toISOString().slice(0, 10)`). | |
| This is intentional — a single canonical timezone prevents gaming via | |
| timezone hopping. | |
| - **New users**: no streak record exists; first request creates it with | |
| `current=1, longest=1, lastDate=today`. | |
| - **Multiple requests per day**: only the first request of the UTC day | |
| increments the streak. | |
| --- | |
| ## Leaderboard | |
| **File:** `src/lib/gamification/leaderboard.ts` | |
| ### Scopes | |
| | Scope | Period | Description | | |
| | --------------- | ------- | --------------------------------------------- | | |
| | `global` | `all` | All-time cumulative XP | | |
| | `weekly` | `week` | XP earned in current UTC week (Mon-Sun) | | |
| | `monthly` | `month` | XP earned in current UTC month | | |
| | `tokens_shared` | `all` | Total tokens transferred to others | | |
| | `contributions` | `all` | Combos created + providers used + skills used | | |
| ### Rank Computation | |
| Ranks are **computed at read time**, not stored. This avoids stale rank data | |
| and eliminates the need for periodic rank recalculation jobs. | |
| ```typescript | |
| export async function getLeaderboard( | |
| scope: LeaderboardScope, | |
| period: string, | |
| limit: number, | |
| offset: number | |
| ): Promise<{ entries: LeaderboardEntry[]; total: number }>; | |
| ``` | |
| Query pattern: | |
| ```sql | |
| SELECT api_key_id, score, | |
| RANK() OVER (ORDER BY score DESC) as rank | |
| FROM leaderboard | |
| WHERE scope = ? AND period = ? | |
| ORDER BY score DESC | |
| LIMIT ? OFFSET ? | |
| ``` | |
| ### Period Rotation | |
| Weekly and monthly leaderboards rotate automatically: | |
| 1. **Archive**: at period boundary, copy current entries to | |
| `leaderboard_archive` with the period label. | |
| 2. **Reset**: delete entries for the expired period. | |
| 3. **Trigger**: checked on every `updateLeaderboard()` call; the first request | |
| of a new period triggers the rotation. | |
| This ensures weekly boards reset every Monday 00:00 UTC and monthly boards | |
| reset on the 1st of each month. | |
| ### SSE Real-Time Updates | |
| **Endpoint:** `GET /api/gamification/stream` | |
| ``` | |
| Client → GET /api/gamification/stream | |
| → SSE connection established | |
| → Server sends top-10 leaderboard snapshot immediately | |
| → Every 5 seconds: push updated top-10 if changed | |
| → Every 15 seconds: heartbeat comment (": heartbeat\n\n") | |
| → Client disconnects → cleanup (remove listener) | |
| ``` | |
| Event format: | |
| ``` | |
| event: leaderboard | |
| data: {"scope":"global","entries":[...]} | |
| event: leaderboard | |
| data: {"scope":"weekly","entries":[...]} | |
| : heartbeat | |
| ``` | |
| The SSE manager tracks connected clients per scope and only sends updates | |
| when the leaderboard data has actually changed since the last push. | |
| --- | |
| ## Token Sharing | |
| **File:** `src/lib/gamification/sharing.ts` | |
| ### Double-Entry Ledger | |
| Every transfer creates two rows in `token_ledger`: | |
| | Row | `from_key_id` | `to_key_id` | `amount` | | |
| | ------ | ------------- | ----------- | -------- | | |
| | Debit | sender | receiver | +amount | | |
| | Credit | receiver | sender | -amount | | |
| Wait — the convention is: | |
| | Row | `from_key_id` | `to_key_id` | `amount` | Meaning | | |
| | ------- | ------------- | ----------- | -------- | ------------------- | | |
| | Send | sender | receiver | +amount | Outflow from sender | | |
| | Receive | receiver | sender | +amount | Inflow to receiver | | |
| Balance is computed as: | |
| ```sql | |
| SELECT | |
| COALESCE(SUM(CASE WHEN to_key_id = ? THEN amount ELSE 0 END), 0) | |
| - COALESCE(SUM(CASE WHEN from_key_id = ? THEN amount ELSE 0 END), 0) | |
| AS balance | |
| FROM token_ledger | |
| WHERE from_key_id = ? OR to_key_id = ? | |
| ``` | |
| ### Transfer Flow | |
| ```typescript | |
| export async function transferTokens( | |
| fromKeyId: string, | |
| toKeyId: string, | |
| amount: number, | |
| idempotencyKey: string | |
| ): Promise<{ success: boolean; balance: number }>; | |
| ``` | |
| 1. **Validate**: `amount > 0`, `fromKeyId !== toKeyId`. | |
| 2. **Idempotency**: check if `idempotency_key` already exists in ledger. | |
| If yes, return cached result. | |
| 3. **Transaction** (single SQLite transaction): | |
| a. Compute sender balance. | |
| b. If `balance < amount`, abort (insufficient funds). | |
| c. Insert send row (`from=sender, to=receiver, amount`). | |
| d. Insert receive row (`from=receiver, to=sender, amount`). | |
| 4. **Rate limit**: check transfer rate for sender (max 10 transfers/min). | |
| 5. **Event**: emit `token_share` gamification event for XP + badge evaluation. | |
| 6. Return `{ success: true, balance: newBalance }`. | |
| ### Rate Limiting | |
| - Max 10 transfers per minute per API key. | |
| - Max 10,000 tokens per single transfer. | |
| - Max 100,000 tokens transferred per day per API key. | |
| --- | |
| ## Invite & Redeem Tokens | |
| **File:** `src/lib/gamification/invites.ts` | |
| ### Code Format | |
| - **Code**: 8-character alphanumeric (e.g., `A3K9-X7M2`), human-readable, | |
| displayed to the user. | |
| - **Token**: 32-byte random token, stored as SHA-256 hash. Used for | |
| programmatic redemption (e.g., URL links). | |
| ### Storage | |
| | Column | Value | | |
| | ------------ | ---------------------------- | | |
| | `code` | `A3K9X7M2` (unique, indexed) | | |
| | `token_hash` | SHA-256(raw_token) | | |
| The raw token is returned to the user exactly once at creation time. OmniRoute | |
| never stores or displays it again — only the hash persists. | |
| ### Self-Referral Prevention | |
| When a user redeems a code, the system checks: | |
| 1. The code belongs to a different `api_key_id`. | |
| 2. The redeeming user has not previously redeemed any code from the same | |
| referrer (joins on `invite_tokens` + redemption log). | |
| If either check fails, the redemption is rejected with a clear error message. | |
| ### Expiry & Limits | |
| - Default `max_uses`: 10 (configurable at creation). | |
| - Default `expires_at`: 30 days from creation. | |
| - Expired or exhausted codes return HTTP 410 Gone. | |
| --- | |
| ## Community Server Federation | |
| **File:** `src/lib/gamification/servers.ts` | |
| ### Connect | |
| A community server is registered via an invite token issued by the remote | |
| server. The local instance: | |
| 1. Receives the invite token (e.g., pasted into dashboard). | |
| 2. Calls `POST /api/gamification/federation/leaderboard` on the remote server | |
| to validate the token and fetch the current leaderboard. | |
| 3. Stores the server record with `status: connected`. | |
| ### Sync Model | |
| Federation uses **overwrite sync**, not additive: | |
| ``` | |
| Local Instance Community Server | |
| │ │ | |
| ├── push score ───────────────►│ POST /federation/score | |
| │ { api_key_id, score } │ (server validates token hash) | |
| │ │ | |
| ├── pull leaderboard ─────────►│ GET /federation/leaderboard | |
| │◄── top-N entries ────────────┤ (overwrites local cache) | |
| │ │ | |
| └── health check ─────────────►│ GET /federation/health | |
| (every 60s, timeout 5s) │ | |
| ``` | |
| ### Auth | |
| Federation requests include: | |
| ``` | |
| Authorization: Bearer <raw_token> | |
| X-Federation-Version: 1 | |
| ``` | |
| The remote server hashes the token and looks up the matching | |
| `community_servers` row. This avoids transmitting the stored hash. | |
| ### Health Monitoring | |
| Each server record tracks: | |
| | Field | Description | | |
| | ----------- | -------------------------------------- | | |
| | `status` | `connected`, `degraded`, `unreachable` | | |
| | `last_sync` | ISO timestamp of last successful sync | | |
| | `failures` | Consecutive health check failures | | |
| After 5 consecutive failures, status changes to `unreachable` and sync is | |
| paused until a manual health check succeeds. | |
| --- | |
| ## Anti-Cheat | |
| **File:** `src/lib/gamification/antiCheat.ts` | |
| ### Server-Side Scoring | |
| All XP calculations happen in `src/lib/gamification/xp.ts`. Clients never | |
| submit a score — they submit actions, and the server computes XP. The | |
| `leaderboard.score` column is only writable by server-side code. | |
| ### Rate Limiting | |
| | Limit | Value | Scope | | |
| | --------------------- | ------- | ------------ | | |
| | Max XP per minute | 1,000 | Per API key | | |
| | Max transfers per min | 10 | Per API key | | |
| | Max transfer amount | 10,000 | Per transfer | | |
| | Max daily transfers | 100,000 | Per API key | | |
| Rate limits use an in-memory sliding window (same pattern as | |
| `RateLimitManager` in `open-sse/services/`). Falls back to SQLite-backed | |
| counters if the process restarts. | |
| ### Z-Score Anomaly Detection | |
| For each API key, the system maintains a rolling 7-day window of XP earned per | |
| hour. On each XP award: | |
| 1. Compute the user's current hourly XP rate. | |
| 2. Compute the population mean and standard deviation. | |
| 3. Calculate `z = (user_rate - mean) / stddev`. | |
| 4. If `z > 3.0` (3 standard deviations), flag as anomaly. | |
| Anomalies are logged to `xp_audit_log` with `action = 'anomaly_detected'` | |
| and surfaced on the admin dashboard. | |
| ### Audit Trail | |
| Every XP award, transfer, badge earn, and anomaly detection is logged to | |
| `xp_audit_log` with: | |
| | Field | Description | | |
| | ------------ | ---------------------------------------------- | | |
| | `api_key_id` | Who | | |
| | `action` | What happened (xp_award, transfer, anomaly, …) | | |
| | `xp_awarded` | Amount (0 for non-XP events) | | |
| | `metadata` | JSON with context (action type, target, …) | | |
| | `created_at` | When (ISO 8601) | | |
| Admins can query the full audit trail via `GET /api/gamification/anomalies`. | |
| --- | |
| ## API Routes | |
| All routes follow the standard OmniRoute pattern: | |
| ``` | |
| Route → CORS preflight → Body validation (Zod) → Auth (extractApiKey) | |
| → Handler | |
| ``` | |
| ### Endpoints | |
| | Method | Path | Description | Auth | | |
| | ------ | ------------------------------------------ | ------------------------------------------- | ---------- | | |
| | GET | `/api/gamification/leaderboard` | Get leaderboard (scope, period, pagination) | Optional | | |
| | POST | `/api/gamification/leaderboard` | Force refresh leaderboard cache | Required | | |
| | GET | `/api/gamification/stream` | SSE real-time leaderboard updates | Optional | | |
| | GET | `/api/gamification/transfer` | Get transfer history (pagination) | Required | | |
| | POST | `/api/gamification/transfer` | Send tokens to another user | Required | | |
| | GET | `/api/gamification/invite` | List my invite codes | Required | | |
| | POST | `/api/gamification/invite` | Generate a new invite code | Required | | |
| | DELETE | `/api/gamification/invite` | Revoke an invite code | Required | | |
| | POST | `/api/gamification/invite/redeem` | Redeem an invite code | Required | | |
| | GET | `/api/gamification/servers` | List community servers | Required | | |
| | POST | `/api/gamification/servers` | Connect to a community server | Required | | |
| | DELETE | `/api/gamification/servers` | Disconnect from a community server | Required | | |
| | POST | `/api/gamification/federation/score` | Push score to remote server | Federation | | |
| | GET | `/api/gamification/federation/leaderboard` | Pull leaderboard from remote | Federation | | |
| | GET | `/api/gamification/notifications` | SSE badge/level-up notifications | Required | | |
| | GET | `/api/gamification/anomalies` | View anomaly reports (admin) | Admin | | |
| | POST | `/api/gamification/rotate` | Rotate invite token secrets | Required | | |
| ### Request/Response Examples | |
| **POST /api/gamification/transfer** | |
| ```json | |
| // Request | |
| { | |
| "to": "recipient-api-key-id", | |
| "amount": 500, | |
| "idempotencyKey": "uuid-v4" | |
| } | |
| // Response 200 | |
| { | |
| "success": true, | |
| "transfer": { | |
| "id": "txn-uuid", | |
| "from": "sender-api-key-id", | |
| "to": "recipient-api-key-id", | |
| "amount": 500, | |
| "createdAt": "2026-05-19T12:00:00.000Z" | |
| }, | |
| "balance": 2500 | |
| } | |
| // Response 400 (insufficient funds) | |
| { | |
| "error": "Insufficient balance", | |
| "balance": 200, | |
| "requested": 500 | |
| } | |
| ``` | |
| **GET /api/gamification/leaderboard?scope=weekly&limit=10** | |
| ```json | |
| { | |
| "scope": "weekly", | |
| "period": "2026-W20", | |
| "entries": [ | |
| { | |
| "rank": 1, | |
| "apiKeyId": "key-uuid", | |
| "displayName": "User***1234", | |
| "score": 15230, | |
| "level": 42, | |
| "title": "Expert" | |
| } | |
| ], | |
| "total": 847, | |
| "updatedAt": "2026-05-19T12:00:00.000Z" | |
| } | |
| ``` | |
| --- | |
| ## MCP Tools (8) | |
| Registered in `open-sse/mcp-server/` alongside existing tools. Scoped under | |
| the `gamification` permission scope. | |
| | Tool | Description | Input Schema | | |
| | -------------------------- | ------------------------------------- | ---------------------------- | --------- | | |
| | `gamification_leaderboard` | Get leaderboard for a scope/period | `{ scope, period?, limit? }` | | |
| | `gamification_rank` | Get caller's rank and neighbors | `{ scope }` | | |
| | `gamification_profile` | Get XP, level, title, streak summary | `{}` | | |
| | `gamification_badges` | List earned badges or all definitions | `{ earned?: boolean }` | | |
| | `gamification_transfer` | Send tokens to another user | `{ to, amount }` | | |
| | `gamification_invite` | Generate or list invite codes | `{ action: "create" | "list" }` | | |
| | `gamification_servers` | List or connect community servers | `{ action, token? }` | | |
| | `gamification_anomalies` | View anomaly reports (admin scope) | `{ limit?, since? }` | | |
| --- | |
| ## Dashboard Pages | |
| ### `/dashboard/leaderboard` | |
| - Podium display (top 3 with avatars and XP). | |
| - Scope selector: Global / Weekly / Monthly / Tokens Shared / Contributions. | |
| - Paginated table (25 per page) with rank, name, score, level, title. | |
| - SSE real-time updates — rank changes animate in. | |
| - Current user highlighted in the table with a "Your Rank" sticky row. | |
| ### `/dashboard/profile` | |
| - XP progress bar with current level and next-level threshold. | |
| - Title badge displayed prominently. | |
| - Badge gallery — earned badges with earn date, unearned badges grayed out | |
| (hidden badges show "???" until earned). | |
| - Streak counter with flame icon; streak calendar (last 30 days). | |
| - XP history chart (daily XP over last 30 days). | |
| ### `/dashboard/tokens` | |
| - Token balance (prominent, top of page). | |
| - Transfer form: recipient, amount, confirm dialog. | |
| - Transfer history table with filters (sent/received/all). | |
| - Invite section: active codes, generate new, share link. | |
| - Community servers: list with health status, connect/disconnect. | |
| ### `/dashboard/gamification/admin` | |
| - Anomaly list with severity, user, timestamp, z-score. | |
| - Audit log viewer with filters (action type, user, date range). | |
| - System stats: total XP awarded, active users, badge earn rates. | |
| - Federation server health overview. | |
| --- | |
| ## Pipeline Integration | |
| ### Integration Point | |
| Gamification hooks into the request pipeline at a single point in | |
| `open-sse/handlers/chatCore.ts`: | |
| ```typescript | |
| // After response is sent to client: | |
| setImmediate(() => { | |
| emitGamificationEvent({ | |
| type: "request.completed", | |
| apiKeyId, | |
| metadata: { | |
| provider: selectedProvider, | |
| model: selectedModel, | |
| comboId: resolvedCombo?.id, | |
| compressionUsed: compressionStats?.applied, | |
| skillUsed: skillExecution?.name, | |
| }, | |
| }).catch(() => { | |
| // Fire-and-forget: log but never propagate to client | |
| }); | |
| }); | |
| ``` | |
| ### Event Types | |
| | Event Type | When Emitted | | |
| | ------------------- | ---------------------------------------- | | |
| | `request.completed` | Successful LLM response sent | | |
| | `provider.switch` | Provider changed (combo fallback counts) | | |
| | `combo.created` | New combo configuration saved | | |
| | `combo.used` | Combo target successfully hit | | |
| | `badge.earned` | Badge evaluation found a match | | |
| | `streak.milestone` | Streak threshold crossed | | |
| | `transfer.sent` | Token transfer completed | | |
| | `referral.redeemed` | Invite code successfully redeemed | | |
| | `compression.used` | Prompt compression applied | | |
| | `skill.executed` | Skill execution completed | | |
| | `model.first_use` | Model not used in past 7 days | | |
| ### Non-Blocking Guarantee | |
| The `setImmediate` + `.catch(() => {})` pattern ensures: | |
| 1. The response is fully sent before gamification runs. | |
| 2. Gamification errors never surface to the client. | |
| 3. The event processing runs in the next microtask, not inline. | |
| --- | |
| ## Security | |
| ### Threat Model | |
| | Threat | Mitigation | | |
| | ------------------------ | ------------------------------------------------------------------- | | |
| | Score inflation | Server-side XP computation only; clients submit actions, not scores | | |
| | Replay attacks | Idempotency keys on transfers; audit log dedup | | |
| | Transfer fraud | Double-entry ledger; atomic transactions; rate limits | | |
| | Self-referral | Cross-check `api_key_id` on redemption | | |
| | Leaderboard manipulation | Z-score anomaly detection; admin anomaly dashboard | | |
| | Federation token theft | SHA-256 hashed storage; raw token shown once only | | |
| | Brute force invite codes | Rate limiting on redemption endpoint; 8-char entropy | | |
| | XSS in display names | Display names sanitized; leaderboard entries escaped | | |
| | Timing attacks on hashes | `crypto.timingSafeEqual` for token hash comparison | | |
| ### Auth Requirements | |
| - **Public** (no auth): `GET /leaderboard`, `GET /stream` (read-only | |
| leaderboards). | |
| - **API key required**: all write operations, profile, transfers, invites. | |
| - **Admin only**: anomaly dashboard, audit log viewer. | |
| - **Federation**: separate auth path using raw token in `Authorization` | |
| header, validated against stored SHA-256 hash. | |
| --- | |
| ## Testing | |
| ### Test Files | |
| All tests use the Node.js native test runner (`node --import tsx/esm --test`). | |
| | Test File | Covers | Tests | | |
| | --------------------------------------------- | --------------------------------------- | ----- | | |
| | `tests/unit/gamification/xp.test.ts` | XP calculation, level curve, titles | 8 | | |
| | `tests/unit/gamification/badges.test.ts` | Badge criteria matching, awarding | 10 | | |
| | `tests/unit/gamification/streaks.test.ts` | Streak logic, milestones, edge cases | 7 | | |
| | `tests/unit/gamification/leaderboard.test.ts` | Rank computation, pagination, rotation | 8 | | |
| | `tests/unit/gamification/sharing.test.ts` | Transfers, balance, idempotency | 9 | | |
| | `tests/unit/gamification/invites.test.ts` | Create, redeem, expiry, self-referral | 7 | | |
| | `tests/unit/gamification/antiCheat.test.ts` | Rate limits, z-score, audit logging | 6 | | |
| | `tests/unit/gamification/events.test.ts` | Event emission, fan-out, error handling | 5 | | |
| ### Running Tests | |
| ```bash | |
| # All gamification tests | |
| node --import tsx/esm --test tests/unit/gamification/*.test.ts | |
| # Single test file | |
| node --import tsx/esm --test tests/unit/gamification/xp.test.ts | |
| ``` | |
| ### Coverage Requirements | |
| Per `CONTRIBUTING.md` — all new modules must have: | |
| - Branch coverage >= 80%. | |
| - Every public function tested at least once. | |
| - Error paths tested (insufficient balance, expired codes, rate limits). | |
| --- | |
| ## File Structure | |
| ``` | |
| src/ | |
| lib/ | |
| db/ | |
| migrations/ | |
| 060_create_gamification.sql # All 8 tables + indexes | |
| gamification.ts # Domain CRUD module | |
| gamification/ | |
| xp.ts # XP calculation, level curve, titles | |
| badges.ts # Badge definitions, criteria, evaluation | |
| streaks.ts # Daily streak tracking | |
| leaderboard.ts # Rank computation, SSE, rotation | |
| antiCheat.ts # Rate limiting, z-score, audit | |
| sharing.ts # Token transfer ledger | |
| invites.ts # Invite/redeem codes | |
| servers.ts # Community server federation | |
| events.ts # Event emitter (integration point) | |
| notifications.ts # SSE notification stream | |
| app/ | |
| api/ | |
| gamification/ | |
| leaderboard/route.ts # GET/POST leaderboard | |
| leaderboard/stream/route.ts # SSE real-time updates | |
| transfer/route.ts # GET/POST transfers | |
| invite/route.ts # GET/POST/DELETE invite codes | |
| invite/redeem/route.ts # POST redeem code | |
| servers/route.ts # GET/POST/DELETE servers | |
| federation/score/route.ts # POST push score | |
| federation/leaderboard/route.ts # GET pull leaderboard | |
| notifications/route.ts # SSE notifications | |
| anomalies/route.ts # GET anomaly reports | |
| rotate/route.ts # POST rotate secrets | |
| (dashboard)/ | |
| dashboard/ | |
| leaderboard/page.tsx # Rankings page | |
| profile/page.tsx # XP/badges/streaks page | |
| tokens/page.tsx # Balance/transfers/invites page | |
| gamification/admin/page.tsx # Admin anomaly monitoring | |
| shared/ | |
| constants/ | |
| gamification.ts # XP_REWARDS, TITLES, BADGE_DEFS, LIMITS | |
| tests/ | |
| unit/ | |
| gamification/ | |
| xp.test.ts | |
| badges.test.ts | |
| streaks.test.ts | |
| leaderboard.test.ts | |
| sharing.test.ts | |
| invites.test.ts | |
| antiCheat.test.ts | |
| events.test.ts | |
| docs/ | |
| frameworks/ | |
| GAMIFICATION.md # This document | |
| ``` | |
| --- | |
| ## Migration Strategy | |
| ### Phase 1: Backend Core (PR 1) | |
| - Migration `060_create_gamification.sql` (8 tables). | |
| - `src/lib/db/gamification.ts` (domain module). | |
| - `src/lib/gamification/xp.ts`, `streaks.ts`, `events.ts`. | |
| - Integration point in `chatCore.ts`. | |
| - Unit tests for XP, streaks, events. | |
| ### Phase 2: Badges & Leaderboard (PR 2) | |
| - `src/lib/gamification/badges.ts`, `leaderboard.ts`. | |
| - Badge definitions in constants. | |
| - Leaderboard API routes + SSE stream. | |
| - Unit tests for badges, leaderboard. | |
| ### Phase 3: Sharing & Invites (PR 3) | |
| - `src/lib/gamification/sharing.ts`, `invites.ts`, `antiCheat.ts`. | |
| - Transfer + invite API routes. | |
| - Unit tests for sharing, invites, anti-cheat. | |
| ### Phase 4: Federation & Dashboard (PR 4) | |
| - `src/lib/gamification/servers.ts`, `notifications.ts`. | |
| - Federation API routes. | |
| - Dashboard pages (leaderboard, profile, tokens, admin). | |
| - MCP tools registration. | |
| --- | |
| ## Future Considerations | |
| - **Seasonal events**: time-limited badge sets and leaderboard seasons. | |
| - **Team leaderboards**: group users by organization or combo. | |
| - **XP multipliers**: boost XP during promotional periods. | |
| - **Achievement sharing**: generate shareable badge cards (OpenGraph images). | |
| - **Mobile push**: webhook-based notifications for badge/level events. | |
| - **Leaderboard API**: public API for third-party integrations. | |