Gamification & Leaderboard System
Source of truth:
src/lib/gamification/,src/lib/db/gamification.ts,src/app/api/gamification/Last updated: 2026-06-28 — v3.8.40
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
Section titled “Overview”Purpose
Section titled “Purpose”Increase user engagement and retention by providing visible progress (XP, levels, badges), social proof (leaderboards), and economic incentives (token sharing, invite rewards).
| 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
Section titled “Design Principles”- Local-first — all state in SQLite, no external services required.
- Non-blocking — events are fire-and-forget; the LLM response path is never delayed by gamification logic.
- Server-authoritative — XP is computed server-side only; clients cannot inflate scores.
- Privacy-respecting — leaderboard participation is opt-in; users can hide their profile.
- Federation-ready — community servers can push scores via signed API; sync is overwrite, not additive.
Architecture
Section titled “Architecture”High-Level Flow
Section titled “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
Section titled “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 secretsData Layer
Section titled “Data Layer”Database Tables
Section titled “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
Section titled “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
Section titled “XP / Level System”File: src/lib/gamification/xp.ts
Level Curve
Section titled “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
Section titled “Titles”| Level Range | Title |
|---|---|
| 1 – 9 | Beginner |
| 10 – 24 | Explorer |
| 25 – 49 | Expert |
| 50 – 74 | Master |
| 75 – 100 | Legend |
XP Rewards
Section titled “XP Rewards”| Action | XP | Description |
|---|---|---|
request |
1 | Per API request routed through OmniRoute |
provider_switch |
5 | Switching to a different provider |
model_switch |
3 | Switching to a different model |
combo_create |
10 | Creating a new combo |
combo_use |
2 | Using a combo for a request |
token_share |
1 | Per 1 000 tokens shared with another user |
invite_redeem |
50 | Redeeming an invite code |
daily_login |
5 | Daily active usage (once per day) |
streak_bonus |
2 | Per consecutive streak day (multiplied by streak length) |
badge_unlock |
10 | Unlocking a badge |
Award Flow
Section titled “Award Flow”export async function awardXp( apiKeyId: string, action: XpAction, metadata?: Record<string, unknown>): Promise<{ xp: number; level: number; title: string; levelUp: boolean }>;- Look up
XP_REWARDS[action]to get the XP amount. - Pass through
checkRateLimit()(anti-cheat: max 1000 XP/min per key). - Open a transaction:
- Read current
user_levelsrow. - Add XP; recompute level via
levelFromXp(totalXp). - If level changed, set
levelUp = true. - Update
user_levelsrow. - Insert into
xp_audit_log.
- Read current
- Return the result. Caller handles notifications.
Helper: levelFromXp(totalXp)
Section titled “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
Section titled “Badge System”File: src/lib/gamification/badges.ts
Categories
Section titled “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
Section titled “Rarities”| Rarity | Color | Probability Hint |
|---|---|---|
common |
Gray | Most users |
uncommon |
Green | Active users |
rare |
Blue | Dedicated users |
legendary |
Gold | Top 1% |
Criteria Types
Section titled “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:
{ "type": "action_count", "action": "request", "count": 1000}Evaluation Flow
Section titled “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 payloadEvaluation 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)
Section titled “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+)
Section titled “Built-in Badges (20+)”Full badge list
| 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 |
Streak Tracker
Section titled “Streak Tracker”File: src/lib/gamification/streaks.ts
Data Model
Section titled “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 |
export async function updateStreak( apiKeyId: string): Promise<{ current: number; longest: number; milestone: boolean }>;- Read streak record from
key_value. - Parse
{current},{longest},{lastDate}(ISO date string). - If
lastDate === today— no change (already counted today). - If
lastDate === yesterday— incrementcurrent; updatelongestif needed. - If
lastDate < yesterday— resetcurrent = 1(streak broken). - Write updated record.
- Check milestones: 7, 14, 30, 60, 90, 180, 365 days. If crossed, set
milestone = true(caller awards XP and checks badges).
Edge Cases
Section titled “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
Section titled “Leaderboard”File: src/lib/gamification/leaderboard.ts
Scopes
Section titled “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
Section titled “Rank Computation”Ranks are computed at read time, not stored. This avoids stale rank data and eliminates the need for periodic rank recalculation jobs.
export async function getLeaderboard( scope: LeaderboardScope, period: string, limit: number, offset: number): Promise<{ entries: LeaderboardEntry[]; total: number }>;Query pattern:
SELECT api_key_id, score, RANK() OVER (ORDER BY score DESC) as rankFROM leaderboardWHERE scope = ? AND period = ?ORDER BY score DESCLIMIT ? OFFSET ?Period Rotation
Section titled “Period Rotation”Weekly and monthly leaderboards rotate automatically:
- Archive: at period boundary, copy current entries to
leaderboard_archivewith the period label. - Reset: delete entries for the expired period.
- 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
Section titled “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: leaderboarddata: {"scope":"global","entries":[...]}
event: leaderboarddata: {"scope":"weekly","entries":[...]}
: heartbeatThe SSE manager tracks connected clients per scope and only sends updates when the leaderboard data has actually changed since the last push.
Token Sharing
Section titled “Token Sharing”File: src/lib/gamification/sharing.ts
Double-Entry Ledger
Section titled “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:
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 balanceFROM token_ledgerWHERE from_key_id = ? OR to_key_id = ?Transfer Flow
Section titled “Transfer Flow”export async function transferTokens( fromKeyId: string, toKeyId: string, amount: number, idempotencyKey: string): Promise<{ success: boolean; balance: number }>;- Validate:
amount > 0,fromKeyId !== toKeyId. - Idempotency: check if
idempotency_keyalready exists in ledger. If yes, return cached result. - 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). - Rate limit: check transfer rate for sender (max 10 transfers/min).
- Event: emit
token_sharegamification event for XP + badge evaluation. - Return
{ success: true, balance: newBalance }.
Rate Limiting
Section titled “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
Section titled “Invite & Redeem Tokens”File: src/lib/gamification/invites.ts
Code Format
Section titled “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
Section titled “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
Section titled “Self-Referral Prevention”When a user redeems a code, the system checks:
- The code belongs to a different
api_key_id. - 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
Section titled “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
Section titled “Community Server Federation”File: src/lib/gamification/servers.ts
Connect
Section titled “Connect”A community server is registered via an invite token issued by the remote server. The local instance:
- Receives the invite token (e.g., pasted into dashboard).
- Calls
POST /api/gamification/federation/leaderboardon the remote server to validate the token and fetch the current leaderboard. - Stores the server record with
status: connected.
Sync Model
Section titled “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) │Federation requests include:
Authorization: Bearer <raw_token>X-Federation-Version: 1The remote server hashes the token and looks up the matching
community_servers row. This avoids transmitting the stored hash.
Health Monitoring
Section titled “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
Section titled “Anti-Cheat”File: src/lib/gamification/antiCheat.ts
Server-Side Scoring
Section titled “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
Section titled “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
Section titled “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:
- Compute the user’s current hourly XP rate.
- Compute the population mean and standard deviation.
- Calculate
z = (user_rate - mean) / stddev. - 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
Section titled “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
Section titled “API Routes”All routes follow the standard OmniRoute pattern:
Route → CORS preflight → Body validation (Zod) → Auth (extractApiKey) → HandlerEndpoints
Section titled “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
Section titled “Request/Response Examples”POST /api/gamification/transfer
// 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
{ "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)
Section titled “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
Section titled “Dashboard Pages”/dashboard/leaderboard
Section titled “/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
Section titled “/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
Section titled “/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
Section titled “/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
Section titled “Pipeline Integration”Integration Point
Section titled “Integration Point”Gamification hooks into the request pipeline at a single point in
open-sse/handlers/chatCore.ts:
// 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
Section titled “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
Section titled “Non-Blocking Guarantee”The setImmediate + .catch(() => {}) pattern ensures:
- The response is fully sent before gamification runs.
- Gamification errors never surface to the client.
- The event processing runs in the next microtask, not inline.
Security
Section titled “Security”Threat Model
Section titled “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
Section titled “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
Authorizationheader, validated against stored SHA-256 hash.
Testing
Section titled “Testing”Test Files
Section titled “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
Section titled “Running Tests”# All gamification testsnode --import tsx/esm --test tests/unit/gamification/*.test.ts
# Single test filenode --import tsx/esm --test tests/unit/gamification/xp.test.tsCoverage Requirements
Section titled “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
Section titled “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 documentMigration Strategy
Section titled “Migration Strategy”Phase 1: Backend Core (PR 1)
Section titled “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)
Section titled “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)
Section titled “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)
Section titled “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
Section titled “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.
HagiCode
HagiCode is an agentic coding workspace: structured workflows, multi-agent execution, and Hero Dungeon views turn ideas into shipped software.
Turn ideas into polished, usable software with a smarter, faster, and more enjoyable agentic coding workflow.

- SmartStructured workflows turn intent into an executable path from idea to shipped change.
- EfficientMulti-agent workflows keep research, implementation, and review moving in parallel.
- FunHero Dungeon interfaces make long coding sessions visual, collaborative, and rewarding.