Spaces:
Runtime error
Runtime error
File size: 44,066 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 | ---
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.
|