* feat(docs): mirror every docs/ page in all 65 locales Extends the documentation mirrors from the 22-page core set (#13940) to every Markdown page under docs/: 152 sources x 65 locales = 9,880 mirrors (6,208 new), language bars rewritten for the full locale list, state adopted so the blocking drift gate now covers all 152 pages. run-translation.mjs: an oversized block made only of table rows or list items (PROVIDER_REFERENCE.md 244-row table, FREE_TIERS.md 71-item list) is cut at item boundaries and rejoined without a blank line — the single 16-40 KB request outlived the backend socket for verbose scripts. 48 older mirrors whose tables had lost rows were retranslated with --force. * docs(i18n): refresh mirrors for the sources the base changed since the branch cut Section-level retranslation of the 29 docs (and README.md) whose source or mirrors moved on release/v3.8.51 during the run, then state adoption; the drift gate is green again on the merged tree.
54 KiB
Gamification & Leaderboard System (Español)
🌐 Languages: 🇺🇸 English · 🇪🇹 am · 🇸🇦 ar · 🇦🇿 az · 🇧🇬 bg · 🇧🇩 bn · 🇨🇿 cs · 🇩🇰 da · 🇩🇪 de · 🇬🇷 el · 🇪🇪 et · 🇮🇷 fa · 🇫🇮 fi · 🇫🇷 fr · 🇮🇪 ga · 🇮🇳 gu · 🇳🇬 ha · 🇮🇱 he · 🇮🇳 hi · 🇭🇷 hr · 🇭🇺 hu · 🇦🇲 hy · 🇮🇩 id · 🇳🇬 ig · 🇮🇹 it · 🇯🇵 ja · 🇬🇪 ka · 🇰🇭 km · 🇮🇳 kn · 🇰🇷 ko · 🇱🇹 lt · 🇱🇻 lv · 🇮🇳 ml · 🇮🇳 mr · 🇲🇾 ms · 🇲🇹 mt · 🇲🇲 my · 🇳🇵 ne · 🇳🇱 nl · 🇳🇴 no · 🇮🇳 or · 🇮🇳 pa · 🇵🇭 phi · 🇵🇱 pl · 🇵🇹 pt · 🇧🇷 pt-BR · 🇷🇴 ro · 🇷🇺 ru · 🇱🇰 si · 🇸🇰 sk · 🇸🇮 sl · 🇷🇸 sr · 🇸🇪 sv · 🇰🇪 sw · 🇮🇳 ta · 🇮🇳 te · 🇹🇭 th · 🇹🇷 tr · 🇺🇦 uk-UA · 🇵🇰 ur · 🇺🇿 uz · 🇻🇳 vi · 🇳🇬 yo · 🇨🇳 zh-CN · 🇹🇼 zh-TW
Fuente de verdad:
src/lib/gamification/,src/lib/db/gamification.ts,src/app/api/gamification/Última actualización: 2026-06-28 — v3.8.40
OmniRoute incluye una capa de gamificación con enfoque local que recompensa a los usuarios por interactuar con la plataforma: realizar solicitudes, cambiar de proveedor, crear combinaciones, compartir tokens y contribuir a la comunidad. Todo el estado reside en SQLite; la federación con servidores de la comunidad es opcional y se basa en envíos.
El sistema está diseñado para tener latencia cero en la ruta crítica: los eventos de gamificación se despachan sin esperar respuesta desde el flujo de solicitudes y nunca bloquean una respuesta del LLM.
Descripción general
Propósito
Aumentar la interacción y retención de los usuarios proporcionando progreso visible (XP, niveles, insignias), prueba social (tablas de clasificación) e incentivos económicos (compartición de tokens, recompensas por invitación).
Alcance
| Funcionalidad | Descripción |
|---|---|
| XP y niveles | Obtén XP por cada acción; sube de nivel según una curva polinómica |
| Insignias | Más de 20 logros en 5 categorías con 4 niveles de rareza |
| Rachas | Seguimiento del uso activo diario con la racha actual y la más larga |
| Tablas de clasificación | Ámbitos global, semanal, mensual, de compartición de tokens y de contribuciones |
| Compartición de tokens | Transfiere créditos entre usuarios mediante un libro mayor de partida doble |
| Invitación y canje | Códigos de referido almacenados con hashes SHA-256 |
| Servidores de la comunidad | Federación con instancias externas de OmniRoute |
| Prevención de trampas | Puntuación del lado del servidor, limitación de frecuencia y detección de anomalías mediante puntuación z |
Principios de diseño
- Enfoque local — todo el estado se almacena en SQLite; no se requieren servicios externos.
- No bloqueante — los eventos se despachan sin esperar respuesta; la ruta de respuesta del LLM nunca se retrasa debido a la lógica de gamificación.
- Servidor autoritativo — el XP se calcula únicamente en el lado del servidor; los clientes no pueden inflar las puntuaciones.
- Respeto por la privacidad — la participación en la tabla de clasificación es opcional; los usuarios pueden ocultar su perfil.
- Preparado para federación — los servidores de la comunidad pueden enviar puntuaciones mediante una API firmada; la sincronización sobrescribe los datos, no los suma.
Arquitectura
Flujo de alto nivel
Solicitud del cliente
→ /v1/chat/completions
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ ... (flujo existente) ...
→ respuesta ascendente enviada al cliente
→ setImmediate (sin esperar respuesta):
→ 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]
El emisor de eventos es el único punto de integración. chatCore.ts llama a
emitGamificationEvent() después de enviar la respuesta; el módulo de eventos distribuye
el evento a los subsistemas de XP, rachas, insignias, tablas de clasificación y prevención de trampas.
Grafo de dependencias de módulos
src/lib/gamification/
events.ts ← punto de entrada (llamado desde chatCore.ts)
├── xp.ts ← cálculo de XP y resolución de niveles
├── streaks.ts ← seguimiento de rachas de actividad diaria
├── badges.ts ← evaluación de criterios de insignias
├── leaderboard.ts ← cálculo de posiciones y difusión por SSE
├── antiCheat.ts ← limitación de frecuencia y detección de anomalías
├── sharing.ts ← libro mayor de transferencias de tokens
├── invites.ts ← gestión de códigos de invitación/canje
├── servers.ts ← federación de servidores de la comunidad
└── notifications.ts ← flujo de notificaciones SSE
src/lib/db/
gamification.ts ← todas las operaciones CRUD (8 tablas)
src/app/api/gamification/
leaderboard/ ← GET clasificaciones, POST actualización manual
leaderboard/stream ← actualizaciones en tiempo real mediante SSE
transfer/ ← GET historial, POST envío de tokens
invite/ ← GET/POST códigos, DELETE revocación
invite/redeem/ ← POST canje de un código
servers/ ← GET/POST/DELETE servidores de la comunidad
federation/score/ ← POST envío de puntuación al servidor
federation/leaderboard/ ← GET obtención de la tabla de clasificación del servidor
notifications/ ← notificaciones SSE de insignias/subidas de nivel
anomalies/ ← GET informes de anomalías (administración)
rotate/ ← POST rotación de secretos de tokens de invitación
Capa de datos
Tablas de la base de datos
Todas las tablas se encuentran en la base de datos SQLite principal de OmniRoute, creada mediante la migración
060_create_gamification.sql. El registro en diario WAL se hereda de la instancia singleton
getDbInstance() de 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 │ └──────────────────────────┘
└─────────────────────────┘
Módulo de dominio: src/lib/db/gamification.ts
Sigue el patrón estándar de OmniRoute: importa getDbInstance() desde
core.ts y exporta funciones CRUD tipadas. No se utiliza SQL sin procesar en los controladores de rutas.
Funciones principales:
| Función | Descripción |
|---|---|
upsertLeaderboardEntry() |
Inserta o actualiza la puntuación de (api_key_id, scope, period) |
getLeaderboard() |
Clasificación paginada para un scope/period determinado |
getUserLevel() |
Obtiene o crea el registro de nivel del usuario |
updateUserLevel() |
Establece el XP, el nivel y el título de forma atómica |
getBadgeDefinitions() |
Todas las definiciones de insignias (opcionalmente filtradas) |
getUserBadges() |
Insignias obtenidas por un usuario |
awardBadge() |
Inserta la obtención de una insignia (idempotente respecto a badge_id) |
logXpAction() |
Añade una entrada a xp_audit_log |
getXpAuditLog() |
Historial de auditoría paginado de un usuario |
insertLedgerEntry() |
Transferencia por partida doble (dentro de una transacción) |
getBalance() |
Suma de lo recibido menos lo enviado por un usuario |
getTransferHistory() |
Registro paginado de transferencias |
createInviteToken() |
Inserta un código de invitación y un token con hash |
redeemInviteToken() |
Busca por código, lo valida e incrementa uses |
upsertCommunityServer() |
Registra o actualiza un servidor de federación |
getCommunityServers() |
Enumera los servidores de un usuario |
deleteCommunityServer() |
Elimina el registro de un servidor |
Sistema de XP / niveles
Archivo: src/lib/gamification/xp.ts
Curva de niveles
La XP necesaria para alcanzar el nivel n sigue una curva polinómica:
xp_for_level(n) = floor(100 * n^1.5)
| Nivel | XP para el siguiente | XP acumulada | Título |
|---|---|---|---|
| 1 | 100 | 100 | Principiante |
| 5 | 1,118 | 2,415 | Principiante |
| 10 | 3,162 | 10,523 | Explorador |
| 25 | 12,500 | 86,024 | Explorador |
| 50 | 35,355 | 345,529 | Experto |
| 75 | 64,952 | 948,683 | Maestro |
| 100 | 100,000 | 2,050,000 | Leyenda |
Títulos
| Rango de niveles | Título |
|---|---|
| 1 – 9 | Principiante |
| 10 – 24 | Explorador |
| 25 – 49 | Experto |
| 50 – 74 | Maestro |
| 75 – 100 | Leyenda |
Recompensas de XP
| Acción | XP | Descripción |
|---|---|---|
request |
1 | Por cada solicitud de API enrutada a través de OmniRoute |
provider_switch |
5 | Cambiar a un proveedor diferente |
model_switch |
3 | Cambiar a un modelo diferente |
combo_create |
10 | Crear un nuevo combo |
combo_use |
2 | Usar un combo para una solicitud |
token_share |
1 | Por cada 1 000 tokens compartidos con otro usuario |
invite_redeem |
50 | Canjear un código de invitación |
daily_login |
5 | Uso activo diario (una vez al día) |
streak_bonus |
2 | Por cada día consecutivo de racha (multiplicado por su duración) |
badge_unlock |
10 | Desbloquear una insignia |
Flujo de asignación
export async function awardXp(
apiKeyId: string,
action: XpAction,
metadata?: Record<string, unknown>
): Promise<{ xp: number; level: number; title: string; levelUp: boolean }>;
- Consultar
XP_REWARDS[action]para obtener la cantidad de XP. - Pasar por
checkRateLimit()(sistema antitrampas: máximo de 1000 XP/min por clave). - Abrir una transacción:
- Leer la fila actual de
user_levels. - Añadir XP; volver a calcular el nivel mediante
levelFromXp(totalXp). - Si el nivel ha cambiado, establecer
levelUp = true. - Actualizar la fila de
user_levels. - Insertar en
xp_audit_log.
- Leer la fila actual de
- Devolver el resultado. El invocador gestiona las notificaciones.
Función auxiliar: levelFromXp(totalXp)
Itera por los niveles 1..100, sumando xp_for_level(n) hasta que la XP acumulada
supere totalXp. Devuelve el nivel más alto cuyo umbral se haya alcanzado.
Esto es O(100), algo aceptable dado que los niveles tienen un límite de 100.
Sistema de insignias
Archivo: src/lib/gamification/badges.ts
Categorías
| Categoría | Descripción | Ejemplos de insignias |
|---|---|---|
usage |
Hitos basados en el volumen | Primera solicitud, 1K solicitudes, 100K |
sharing |
Uso compartido de tokens y referidos | Primera vez compartiendo, Generoso (10 veces) |
contribution |
Participación en la comunidad | Creador de combos, Explorador de proveedores |
streak |
Constancia a lo largo del tiempo | Guerrero semanal, Devoto mensual |
rare |
Logros difíciles de obtener u ocultos | Usuario pionero, Informador de errores |
Rarezas
| Rareza | Color | Indicador de probabilidad |
|---|---|---|
common |
Gris | La mayoría de los usuarios |
uncommon |
Verde | Usuarios activos |
rare |
Azul | Usuarios dedicados |
legendary |
Oro | 1 % superior |
Tipos de criterios
| Tipo | Campo | Descripción |
|---|---|---|
action_count |
count |
Realizar una acción N veces (p. ej., 1000 solicitudes) |
streak |
days |
Mantener una racha durante N días consecutivos |
unique_count |
field, n |
Usar N valores únicos (p. ej., 10 modelos diferentes) |
rank |
scope, n |
Alcanzar la posición N en una clasificación determinada |
first |
— | Ser el primero en realizar una acción |
hidden |
(varía) | Criterios que no se muestran hasta obtener la insignia |
Las definiciones de las insignias se almacenan en badge_definitions como criteria JSON:
{
"type": "action_count",
"action": "request",
"count": 1000
}
Flujo de evaluación
emitGamificationEvent(event)
→ evaluateBadges(apiKeyId, event)
→ getBadgeDefinitions() # todas las definiciones
→ getUserBadges(apiKeyId) # ya obtenidas (omitir)
→ para cada insignia no obtenida:
→ matchesCriteria(badge, event, userState)
→ si coincide: awardBadge(apiKeyId, badgeId)
→ devolver la carga útil de la notificación
La evaluación está basada en eventos: se ejecuta después de cada evento de gamificación, pero
solo comprueba las insignias cuyo criteria.type se corresponde con la acción del evento. Esto
mantiene una evaluación rápida (< 5 ms para la mayoría de los eventos).
matchesCriteria(badge, event, userState)
| Tipo de criterio | Comprobación |
|---|---|
action_count |
getActionCount(apiKeyId, action) >= count |
streak |
getCurrentStreak(apiKeyId) >= days |
unique_count |
getUniqueCount(apiKeyId, field) >= n |
rank |
getRank(apiKeyId, scope) <= n |
first |
No hay ninguna entrada previa en xp_audit_log para este tipo de acción |
hidden |
Delega en la comprobación secundaria correspondiente |
Insignias integradas (más de 20)
Lista completa de insignias
| Insignia | Categoría | Rareza | Criterios |
|---|---|---|---|
| Primeros pasos | uso | común | 1 solicitud |
| Entrando en calor | uso | común | 100 solicitudes |
| Usuario avanzado | uso | poco común | 1,000 solicitudes |
| Centurión | uso | rara | 10,000 solicitudes |
| OmniPower | uso | legendaria | 100,000 solicitudes |
| Explorador de proveedores | contribución | común | Usar 5 proveedores diferentes |
| Maestro de proveedores | contribución | poco común | Usar 20 proveedores diferentes |
| Arquitecto de combos | contribución | poco común | Crear 5 combos |
| Gran maestro de combos | contribución | rara | Crear 25 combos |
| Primera transferencia | compartir | común | 1 transferencia de tokens |
| Generoso | compartir | poco común | 10 transferencias de tokens |
| Filántropo | compartir | rara | Transferir 10,000 tokens en total |
| Referidor | compartir | común | 1 referido exitoso |
| Creador de redes | compartir | poco común | 10 referidos exitosos |
| Guerrero semanal | racha | poco común | Racha de 7 días |
| Devoto mensual | racha | rara | Racha de 30 días |
| Imparable | racha | legendaria | Racha de 365 días |
| Adoptante temprano | rara | legendaria | Unirse durante el período beta |
| Pionero de la compresión | rara | poco común | Usar la compresión 100 veces |
| Coleccionista de habilidades | rara | rara | Usar 10 habilidades diferentes |
| Explorador de modelos | contribución | poco común | Usar 15 modelos diferentes |
Seguimiento de rachas
Archivo: src/lib/gamification/streaks.ts
Modelo de datos
Las rachas se almacenan en la tabla key_value (tabla de utilidades compartida) bajo
claves con espacio de nombres:
| Clave | Valor | Descripción |
|---|---|---|
gamification:streak:{keyId} |
{current},{longest},{lastDate} |
Datos de la racha activa |
Lógica
export async function updateStreak(
apiKeyId: string
): Promise<{ current: number; longest: number; milestone: boolean }>;
- Leer el registro de la racha desde
key_value. - Analizar
{current},{longest}y{lastDate}(cadena de fecha ISO). - Si
lastDate === today, no realizar ningún cambio (ya se contabilizó hoy). - Si
lastDate === yesterday, incrementarcurrenty actualizarlongestsi es necesario. - Si
lastDate < yesterday, restablecercurrent = 1(racha interrumpida). - Escribir el registro actualizado.
- Comprobar los hitos: 7, 14, 30, 60, 90, 180 y 365 días. Si se alcanza uno,
establecer
milestone = true(el llamador otorga XP y comprueba las insignias).
Casos límite
- Zona horaria: las rachas utilizan fechas UTC (
new Date().toISOString().slice(0, 10)). Esto es intencionado: una única zona horaria canónica evita manipulaciones mediante cambios de zona horaria. - Usuarios nuevos: no existe ningún registro de racha; la primera solicitud lo crea con
current=1, longest=1, lastDate=today. - Varias solicitudes al día: solo la primera solicitud del día UTC incrementa la racha.
Clasificación
Archivo: src/lib/gamification/leaderboard.ts
Ámbitos
| Ámbito | Período | Descripción |
|---|---|---|
global |
all |
XP acumulado de todos los tiempos |
weekly |
week |
XP obtenido en la semana UTC actual (lunes-domingo) |
monthly |
month |
XP obtenido en el mes UTC actual |
tokens_shared |
all |
Total de tokens transferidos a otros |
contributions |
all |
Combos creados + proveedores usados + skills usadas |
Cálculo de posiciones
Las posiciones se calculan en el momento de la lectura, no se almacenan. Esto evita datos de posiciones obsoletos y elimina la necesidad de tareas periódicas para recalcularlas.
export async function getLeaderboard(
scope: LeaderboardScope,
period: string,
limit: number,
offset: number
): Promise<{ entries: LeaderboardEntry[]; total: number }>;
Patrón de consulta:
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 ?
Rotación de períodos
Las clasificaciones semanales y mensuales rotan automáticamente:
- Archivar: al finalizar el período, copiar las entradas actuales a
leaderboard_archivecon la etiqueta del período. - Restablecer: eliminar las entradas del período vencido.
- Activación: se comprueba en cada llamada a
updateLeaderboard(); la primera solicitud de un período nuevo activa la rotación.
Esto garantiza que las clasificaciones semanales se restablezcan cada lunes a las 00:00 UTC y que las mensuales se restablezcan el primer día de cada mes.
Actualizaciones en tiempo real mediante SSE
Endpoint: GET /api/gamification/stream
Cliente → GET /api/gamification/stream
→ Se establece la conexión SSE
→ El servidor envía inmediatamente una instantánea de los 10 primeros de la clasificación
→ Cada 5 segundos: envía los 10 primeros actualizados si han cambiado
→ Cada 15 segundos: comentario de latido (": heartbeat\n\n")
→ El cliente se desconecta → limpieza (eliminar el listener)
Formato del evento:
event: leaderboard
data: {"scope":"global","entries":[...]}
event: leaderboard
data: {"scope":"weekly","entries":[...]}
: heartbeat
El gestor de SSE realiza un seguimiento de los clientes conectados por ámbito y solo envía actualizaciones cuando los datos de la clasificación han cambiado realmente desde el último envío.
Compartición de tokens
Archivo: src/lib/gamification/sharing.ts
Libro mayor de partida doble
Cada transferencia crea dos filas en token_ledger:
| Fila | from_key_id |
to_key_id |
amount |
|---|---|---|---|
| Débito | remitente | destinatario | +cantidad |
| Crédito | destinatario | remitente | -cantidad |
Un momento: la convención es:
| Fila | from_key_id |
to_key_id |
amount |
Significado |
|---|---|---|---|---|
| Envío | remitente | destinatario | +cantidad | Salida de fondos del remitente |
| Recepción | destinatario | remitente | +cantidad | Entrada de fondos al destinatario |
El saldo se calcula de la siguiente manera:
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 = ?
Flujo de transferencia
export async function transferTokens(
fromKeyId: string,
toKeyId: string,
amount: number,
idempotencyKey: string
): Promise<{ success: boolean; balance: number }>;
- Validación:
amount > 0,fromKeyId !== toKeyId. - Idempotencia: comprobar si
idempotency_keyya existe en el libro mayor. Si existe, devolver el resultado almacenado en caché. - Transacción (una única transacción de SQLite):
a. Calcular el saldo del remitente.
b. Si
balance < amount, cancelar la operación (fondos insuficientes). c. Insertar la fila de envío (from=sender,.
Limitación de frecuencia
- Máximo de 10 transferencias por minuto y por clave de API.
- Máximo de 10 000 tokens por transferencia.
- Máximo de 100 000 tokens transferidos al día por clave de API.
Tokens de invitación y canje
Archivo: src/lib/gamification/invites.ts
Formato del código
- Código: alfanumérico de 8 caracteres (p. ej.,
A3K9-X7M2), legible para las personas y mostrado al usuario. - Token: token aleatorio de 32 bytes, almacenado como hash SHA-256. Se utiliza para el canje programático (p. ej., enlaces URL).
Almacenamiento
| Columna | Valor |
|---|---|
code |
A3K9X7M2 (único, indexado) |
token_hash |
SHA-256(raw_token) |
El token sin procesar se devuelve al usuario una sola vez durante su creación. OmniRoute nunca vuelve a almacenarlo ni a mostrarlo; únicamente se conserva el hash.
Prevención de autorreferencias
Cuando un usuario canjea un código, el sistema comprueba lo siguiente:
- El código pertenece a un
api_key_iddiferente. - El usuario que realiza el canje no ha canjeado previamente ningún código del mismo
referente (mediante una unión de
invite_tokensy el registro de canjes).
Si alguna de las comprobaciones falla, el canje se rechaza con un mensaje de error claro.
Caducidad y límites
- Valor predeterminado de
max_uses: 10 (configurable durante la creación). - Valor predeterminado de
expires_at: 30 días desde la creación. - Los códigos caducados o que hayan agotado sus usos devuelven HTTP 410 Gone.
Federación de servidores comunitarios
Archivo: src/lib/gamification/servers.ts
Conexión
Un servidor comunitario se registra mediante un token de invitación emitido por el servidor remoto. La instancia local:
- Recibe el token de invitación (p. ej., pegado en el panel de control).
- Llama a
POST /api/gamification/federation/leaderboarden el servidor remoto para validar el token y obtener la clasificación actual. - Almacena el registro del servidor con
status: connected.
Modelo de sincronización
La federación utiliza sincronización por sobrescritura, no acumulativa:
Instancia local Servidor comunitario
│ │
├── enviar puntuación ────────►│ POST /federation/score
│ { api_key_id, score } │ (el servidor valida el hash del token)
│ │
├── obtener clasificación ────►│ GET /federation/leaderboard
│◄── primeras N entradas ──────┤ (sobrescribe la caché local)
│ │
└── comprobación de estado ───►│ GET /federation/health
(cada 60 s, tiempo de espera de 5 s) │
Autenticación
Las solicitudes de federación incluyen:
Authorization: Bearer <raw_token>
X-Federation-Version: 1
El servidor remoto calcula el hash del token y busca la fila correspondiente en community_servers. Esto evita transmitir el hash almacenado.
Supervisión del estado
Cada registro de servidor realiza un seguimiento de:
| Campo | Descripción |
|---|---|
status |
connected, degraded, unreachable |
last_sync |
Marca de tiempo ISO de la última sincronización correcta |
failures |
Fallos consecutivos de comprobación de estado |
Tras 5 fallos consecutivos, el estado cambia a unreachable y la sincronización se pausa hasta que una comprobación de estado manual se complete correctamente.
Prevención de trampas
Archivo: src/lib/gamification/antiCheat.ts
Cálculo de puntuación en el servidor
Todos los cálculos de XP se realizan en src/lib/gamification/xp.ts. Los clientes nunca envían una puntuación: envían acciones y el servidor calcula los XP. La columna leaderboard.score solo puede ser modificada por código del servidor.
Limitación de frecuencia
| Límite | Valor | Ámbito |
|---|---|---|
| Máximo de XP por minuto | 1,000 | Por clave de API |
| Máx. transferencias/min. | 10 | Por clave de API |
| Importe máx. de transferencia | 10,000 | Por transferencia |
| Máx. de transferencias diarias | 100,000 | Por clave de API |
Los límites de frecuencia utilizan una ventana deslizante en memoria (el mismo patrón que RateLimitManager en open-sse/services/). Si el proceso se reinicia, se recurre a contadores respaldados por SQLite.
Detección de anomalías mediante puntuación Z
Para cada clave de API, el sistema mantiene una ventana móvil de 7 días de los XP obtenidos por hora. Con cada asignación de XP:
- Calcula la tasa actual de XP por hora del usuario.
- Calcula la media y la desviación estándar de la población.
- Calcula
z = (user_rate - mean) / stddev. - Si
z > 3.0(3 desviaciones estándar), lo marca como anomalía.
Las anomalías se registran en xp_audit_log con action = 'anomaly_detected' y se muestran en el panel de administración.
Registro de auditoría
Cada asignación de XP, transferencia, obtención de insignia y detección de anomalías se registra en xp_audit_log con:
| Campo | Descripción |
|---|---|
api_key_id |
Quién |
action |
Qué ocurrió (xp_award, transfer, anomaly, …) |
xp_awarded |
Cantidad (0 para eventos que no sean de XP) |
metadata |
JSON con el contexto (tipo de acción, objetivo, …) |
created_at |
Cuándo (ISO 8601) |
Los administradores pueden consultar el registro de auditoría completo mediante GET /api/gamification/anomalies.
Rutas de la API
Todas las rutas siguen el patrón estándar de OmniRoute:
Ruta → Preflight de CORS → Validación del cuerpo (Zod) → Autenticación (extractApiKey)
→ Controlador
Endpoints
| Método | Ruta | Descripción | Autenticación |
|---|---|---|---|
| GET | /api/gamification/leaderboard |
Obtener la clasificación (ámbito, período, paginación) | Opcional |
| POST | /api/gamification/leaderboard |
Forzar la actualización de la caché de clasificación | Obligatoria |
| GET | /api/gamification/stream |
Actualizaciones de la clasificación en tiempo real mediante SSE | Opcional |
| GET | /api/gamification/transfer |
Obtener el historial de transferencias (paginación) | Obligatoria |
| POST | /api/gamification/transfer |
Enviar tokens a otro usuario | Obligatoria |
| GET | /api/gamification/invite |
Enumerar mis códigos de invitación | Obligatoria |
| POST | /api/gamification/invite |
Generar un nuevo código de invitación | Obligatoria |
| DELETE | /api/gamification/invite |
Revocar un código de invitación | Obligatoria |
| POST | /api/gamification/invite/redeem |
Canjear un código de invitación | Obligatoria |
| GET | /api/gamification/servers |
Enumerar los servidores de la comunidad | Obligatoria |
| POST | /api/gamification/servers |
Conectarse a un servidor de la comunidad | Obligatoria |
| DELETE | /api/gamification/servers |
Desconectarse de un servidor de la comunidad | Obligatoria |
| POST | /api/gamification/federation/score |
Enviar la puntuación a un servidor remoto | Federación |
| GET | /api/gamification/federation/leaderboard |
Obtener la clasificación de un servidor remoto | Federación |
| GET | /api/gamification/notifications |
Notificaciones SSE de insignias y subidas de nivel | Obligatoria |
| GET | /api/gamification/anomalies |
Ver informes de anomalías (administrador) | Administrador |
| POST | /api/gamification/rotate |
Rotar los secretos de los tokens de invitación | Obligatoria |
Ejemplos de solicitudes/respuestas
POST /api/gamification/transfer
// Solicitud
{
"to": "recipient-api-key-id",
"amount": 500,
"idempotencyKey": "uuid-v4"
}
// Respuesta 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
}
// Respuesta 400 (fondos insuficientes)
{
"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"
}
Herramientas MCP (8)
Registradas en open-sse/mcp-server/ junto con las herramientas existentes. Limitadas al ámbito de permisos
gamification.
| Herramienta | Descripción | Esquema de entrada | |
|---|---|---|---|
gamification_leaderboard |
Obtener la clasificación de un ámbito/período | { scope, period?, limit? } |
|
gamification_rank |
Obtener la posición del solicitante y sus vecinos | { scope } |
|
gamification_profile |
Obtener resumen de XP, nivel, título y racha | {} |
|
gamification_badges |
Listar insignias obtenidas o todas las definiciones | { earned?: boolean } |
|
gamification_transfer |
Enviar tokens a otro usuario | { to, amount } |
|
gamification_invite |
Generar o listar códigos de invitación | `{ action: "create" | "list" }` |
gamification_servers |
Listar o conectar servidores de la comunidad | { action, token? } |
|
gamification_anomalies |
Ver informes de anomalías (ámbito de administrador) | { limit?, since? } |
Páginas del panel
/dashboard/leaderboard
- Visualización del podio (los 3 primeros con avatares y XP).
- Selector de ámbito: Global / Semanal / Mensual / Tokens compartidos / Contribuciones.
- Tabla paginada (25 por página) con posición, nombre, puntuación, nivel y título.
- Actualizaciones en tiempo real mediante SSE: los cambios de posición se animan.
- El usuario actual aparece resaltado en la tabla con una fila fija que muestra "Tu posición".
/dashboard/profile
- Barra de progreso de XP con el nivel actual y el umbral del siguiente nivel.
- La insignia del título se muestra de forma destacada.
- Galería de insignias: las insignias obtenidas incluyen la fecha de obtención y las no obtenidas aparecen atenuadas (las insignias ocultas muestran "???" hasta que se obtienen).
- Contador de racha con icono de llama; calendario de racha (últimos 30 días).
- Gráfico del historial de XP (XP diario durante los últimos 30 días).
/dashboard/tokens
- Saldo de tokens (destacado en la parte superior de la página).
- Formulario de transferencia: destinatario, cantidad y diálogo de confirmación.
- Tabla del historial de transferencias con filtros (enviadas/recibidas/todas).
- Sección de invitaciones: códigos activos, generar uno nuevo y compartir enlace.
- Servidores de la comunidad: lista con estado de salud y opciones para conectar/desconectar.
/dashboard/gamification/admin
- Lista de anomalías con gravedad, usuario, marca de tiempo y puntuación z.
- Visor del registro de auditoría con filtros (tipo de acción, usuario e intervalo de fechas).
- Estadísticas del sistema: XP total otorgado, usuarios activos y tasas de obtención de insignias.
- Vista general del estado de los servidores de federación.
Integración con el pipeline
Punto de integración
La gamificación se conecta al pipeline de solicitudes en un único punto de
open-sse/handlers/chatCore.ts:
// Después de enviar la respuesta al cliente:
setImmediate(() => {
emitGamificationEvent({
type: "request.completed",
apiKeyId,
metadata: {
provider: selectedProvider,
model: selectedModel,
comboId: resolvedCombo?.id,
compressionUsed: compressionStats?.applied,
skillUsed: skillExecution?.name,
},
}).catch(() => {
// Ejecutar y olvidar: registrar, pero nunca propagar al cliente
});
});
Tipos de eventos
| Tipo de evento | Cuándo se emite |
|---|---|
request.completed |
Se envió correctamente la respuesta del LLM |
provider.switch |
Cambió el proveedor (los fallbacks de combos cuentan) |
combo.created |
Se guardó una nueva configuración de combo |
combo.used |
Se alcanzó correctamente el destino del combo |
badge.earned |
La evaluación de insignias encontró una coincidencia |
streak.milestone |
Se superó el umbral de la racha |
transfer.sent |
Se completó la transferencia de tokens |
referral.redeemed |
Se canjeó correctamente el código de invitación |
compression.used |
Se aplicó la compresión del prompt |
skill.executed |
Se completó la ejecución de la habilidad |
model.first_use |
El modelo no se había utilizado durante los últimos 7 días |
Garantía de no bloqueo
El patrón setImmediate + .catch(() => {}) garantiza lo siguiente:
- La respuesta se envía por completo antes de que se ejecute la gamificación.
- Los errores de gamificación nunca llegan al cliente.
- El procesamiento del evento se ejecuta en la siguiente microtarea, no en línea.
Seguridad
Modelo de amenazas
| Amenaza | Mitigación |
|---|---|
| Inflación de puntuación | Cálculo de XP solo en el servidor; los clientes envían acciones, no puntuaciones |
| Ataques de repetición | Claves de idempotencia en transferencias; deduplicación del registro de auditoría |
| Fraude en transferencias | Libro mayor de partida doble; transacciones atómicas; límites de frecuencia |
| Autorreferencia | Verificación cruzada de api_key_id durante el canje |
| Manipulación de la clasificación | Detección de anomalías mediante puntuación Z; panel de anomalías para administradores |
| Robo de tokens de federación | Almacenamiento con hash SHA-256; el token sin procesar se muestra una sola vez |
| Fuerza bruta de códigos de invitación | Límite de frecuencia en el endpoint de canje; entropía de 8 caracteres |
| XSS en nombres para mostrar | Nombres para mostrar saneados; entradas de la clasificación escapadas |
| Ataques de temporización en hashes | crypto.timingSafeEqual para comparar hashes de tokens |
Requisitos de autenticación
- Público (sin autenticación):
GET /leaderboard,GET /stream(clasificaciones de solo lectura). - Clave de API obligatoria: todas las operaciones de escritura, perfil, transferencias e invitaciones.
- Solo administradores: panel de anomalías y visor del registro de auditoría.
- Federación: ruta de autenticación independiente que utiliza el token sin procesar en el encabezado
Authorization, validado mediante el hash SHA-256 almacenado.
Pruebas
Archivos de pruebas
Todas las pruebas utilizan el ejecutor de pruebas nativo de Node.js (node --import tsx/esm --test).
| Archivo de prueba | Cobertura | Pruebas |
|---|---|---|
tests/unit/gamification/xp.test.ts |
Cálculo de XP, curva de niveles, títulos | 8 |
tests/unit/gamification/badges.test.ts |
Coincidencia de criterios e insignias otorgadas | 10 |
tests/unit/gamification/streaks.test.ts |
Lógica de rachas, hitos, casos límite | 7 |
tests/unit/gamification/leaderboard.test.ts |
Cálculo de posiciones, paginación, rotación | 8 |
tests/unit/gamification/sharing.test.ts |
Transferencias, saldo, idempotencia | 9 |
tests/unit/gamification/invites.test.ts |
Creación, canje, caducidad, autorreferencia | 7 |
tests/unit/gamification/antiCheat.test.ts |
Límites de frecuencia, puntuación Z, registro de auditoría | 6 |
tests/unit/gamification/events.test.ts |
Emisión de eventos, distribución, gestión de errores | 5 |
Ejecución de las pruebas
# Todas las pruebas de gamificación
node --import tsx/esm --test tests/unit/gamification/*.test.ts
# Un solo archivo de prueba
node --import tsx/esm --test tests/unit/gamification/xp.test.ts
Requisitos de cobertura
Según CONTRIBUTING.md, todos los módulos nuevos deben tener:
- Cobertura de ramas >= 80 %.
- Cada función pública probada al menos una vez.
- Rutas de error probadas (saldo insuficiente, códigos caducados, límites de frecuencia).
Estructura de archivos
src/
lib/
db/
migrations/
060_create_gamification.sql # Las 8 tablas y sus índices
gamification.ts # Módulo CRUD del dominio
gamification/
xp.ts # Cálculo de XP, curva de niveles y títulos
badges.ts # Definiciones, criterios y evaluación de insignias
streaks.ts # Seguimiento de rachas diarias
leaderboard.ts # Cálculo de clasificación, SSE y rotación
antiCheat.ts # Limitación de frecuencia, puntuación z y auditoría
sharing.ts # Registro contable de transferencias de tokens
invites.ts # Códigos de invitación/canje
servers.ts # Federación de servidores comunitarios
events.ts # Emisor de eventos (punto de integración)
notifications.ts # Flujo de notificaciones SSE
app/
api/
gamification/
leaderboard/route.ts # GET/POST de la clasificación
leaderboard/stream/route.ts # Actualizaciones en tiempo real mediante SSE
transfer/route.ts # GET/POST de transferencias
invite/route.ts # GET/POST/DELETE de códigos de invitación
invite/redeem/route.ts # POST para canjear un código
servers/route.ts # GET/POST/DELETE de servidores
federation/score/route.ts # POST para enviar una puntuación
federation/leaderboard/route.ts # GET para obtener la clasificación
notifications/route.ts # Notificaciones mediante SSE
anomalies/route.ts # GET de informes de anomalías
rotate/route.ts # POST para rotar secretos
(dashboard)/
dashboard/
leaderboard/page.tsx # Página de clasificación
profile/page.tsx # Página de XP, insignias y rachas
tokens/page.tsx # Página de saldo, transferencias e invitaciones
gamification/admin/page.tsx # Supervisión administrativa de anomalías
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 # Este documento
Estrategia de migración
Fase 1: Núcleo del backend (PR 1)
- Migración
060_create_gamification.sql(8 tablas). src/lib/db/gamification.ts(módulo del dominio).src/lib/gamification/xp.ts,streaks.ts,events.ts.- Punto de integración en
chatCore.ts. - Pruebas unitarias para XP, rachas y eventos.
Fase 2: Insignias y clasificación (PR 2)
src/lib/gamification/badges.ts,leaderboard.ts.- Definiciones de insignias en las constantes.
- Rutas de la API de clasificación y flujo SSE.
- Pruebas unitarias para insignias y clasificación.
Fase 3: Uso compartido e invitaciones (PR 3)
src/lib/gamification/sharing.ts,invites.ts,antiCheat.ts.- Rutas de la API de transferencias e invitaciones.
- Pruebas unitarias para uso compartido, invitaciones y prevención de trampas.
Fase 4: Federación y panel de control (PR 4)
src/lib/gamification/servers.ts,notifications.ts.- Rutas de la API de federación.
- Páginas del panel de control (clasificación, perfil, tokens y administración).
- Registro de herramientas MCP.
Consideraciones futuras
- Eventos de temporada: conjuntos de insignias por tiempo limitado y temporadas de clasificación.
- Clasificaciones por equipos: agrupar a los usuarios por organización o combo.
- Multiplicadores de XP: aumentar los XP durante los períodos promocionales.
- Compartir logros: generar tarjetas de insignias para compartir (imágenes OpenGraph).
- Notificaciones push móviles: notificaciones basadas en webhooks para eventos de insignias/niveles.
- API de clasificación: API pública para integraciones de terceros.