Skip to main content

@robojs/xp

MEE6-style chat XP system that exposes a powerful event-driven API that makes building custom features incredibly easy. No need to fork the plugin or write complex integrations—just listen to events and call imperative functions.

GitHub licensenpminstall sizeDiscord

📚 Documentation: Getting started

🚀 Community: Join our Discord server

Features

  • 💬 MEE6-parity XP mechanics - Awards 15-25 XP per message with 60-second cooldown
  • 🎯 Role rewards - Automatic role assignment with stack or replace modes
  • 🚀 Multipliers - Server, role, and user XP boosts (MEE6 Pro parity)
  • 📊 Cached leaderboards - Optimized for 10k+ users with under 200ms refresh
  • 🛡️ No-XP roles/channels - Granular control over where XP is earned
  • 🔧 Admin commands - Complete control via /xp command suite
  • 📈 Event system - Real-time hooks for level changes and XP updates
  • 🌐 REST API - Optional HTTP endpoints (requires @robojs/server)
  • 💾 Flashcore persistence - Guild-scoped data storage with automatic caching

Build Custom Features in Minutes

Here's a complete level-up announcement system in ~10 lines:

import { events } from '@robojs/xp'
import { client } from 'robo.js'

events.onLevelUp(async ({ guildId, userId, newLevel }) => {
const guild = await client.guilds.fetch(guildId)
const channel = guild.channels.cache.find((c) => c.name === 'level-ups')
if (!channel?.isTextBased()) return

await channel.send(`🎉 <@${userId}> just reached Level ${newLevel}!`)
})

Use the same pattern to award contest bonuses, apply moderation penalties, enable premium XP boosts, track analytics, or build any custom XP-based feature you can imagine.

Check out seed/events/_start/level-announcements.ts for a production-ready example with rich embeds and customization options, or explore the Integration Recipes section below for more patterns.

Getting Started

Terminal
npx robo add @robojs/xp

New to Robo.js? Start your project with this plugin pre-installed:

Terminal
npx create-robo <project-name> -p @robojs/xp

Optional: REST API

To enable the HTTP API endpoints, install @robojs/server:

Terminal
npx robo add @robojs/server

Slash Commands Reference

Admin Commands (Require Manage Guild Permission)

XP Manipulation

CommandDescriptionOptions
/xp give <user> <amount> [reason]Award XP to a useruser (required), amount (1-1,000,000), reason (optional)
/xp remove <user> <amount> [reason]Remove XP from a useruser (required), amount (1-1,000,000), reason (optional)
/xp set <user> <amount> [reason]Set exact XP valueuser (required), amount (0-1,000,000), reason (optional)
/xp recalc <user>Recalculate level and reconcile rolesuser (required)

Role Rewards

CommandDescriptionOptions
/xp rewards add <level> <role>Add role reward at levellevel (1-1000), role (required)
/xp rewards remove <level>Remove role rewardlevel (required)
/xp rewards listList all role rewardsNone
/xp rewards mode <mode>Set stack or replace modemode (stack or replace)
/xp rewards remove-on-loss <enabled>Toggle reward removal on XP lossenabled (boolean)

Stack Mode: Users keep all role rewards from previous levels Replace Mode: Users only get the highest level role reward

Configuration

CommandDescriptionOptions
/xp config getView current configurationNone
/xp config set-cooldown <seconds>Set XP award cooldownseconds (0-3600)
/xp config set-xp-rate <rate>Set XP rate multiplierrate (0.1-10.0)
/xp config add-no-xp-role <role>Add No-XP rolerole (required)
/xp config remove-no-xp-role <role>Remove No-XP rolerole (required)
/xp config add-no-xp-channel <channel>Add No-XP channelchannel (required)
/xp config remove-no-xp-channel <channel>Remove No-XP channelchannel (required)
/xp config set-leaderboard-public <enabled>Toggle public leaderboardenabled (boolean)

Multipliers

CommandDescriptionOptions
/xp multiplier server <multiplier>Set server-wide XP multipliermultiplier (0.1-10.0)
/xp multiplier role <role> <multiplier>Set role XP multiplierrole, multiplier (0.1-10.0)
/xp multiplier user <user> <multiplier>Set user XP multiplieruser, multiplier (0.1-10.0)
/xp multiplier remove-role <role>Remove role multiplierrole (required)
/xp multiplier remove-user <user>Remove user multiplieruser (required)

User Commands (No Permission Required)

CommandDescriptionOptions
/rank [user]View rank card with progressuser (optional, defaults to self)
/leaderboard [page]View server leaderboardpage (optional, default: 1)

Note: /leaderboard requires admin permission when leaderboard.public is false

Configuration Guide

Guild Configuration Structure

interface GuildConfig {
// Basic settings
cooldownSeconds: number // Per-user message cooldown (default: 60)
xpRate: number // XP rate multiplier (default: 1.0)

// Exclusions
noXpRoleIds: string[] // Roles that don't earn XP
noXpChannelIds: string[] // Channels that don't award XP

// Role rewards
roleRewards: RoleReward[] // Level → Role mappings
rewardsMode: 'stack' | 'replace' // Stack (keep all) or replace (highest only)
removeRewardOnXpLoss: boolean // Remove roles when XP drops below level

// Multipliers (MEE6 Pro parity)
multipliers: {
server?: number // Server-wide multiplier
role?: Record<string, number> // Per-role multipliers
user?: Record<string, number> // Per-user multipliers
}

// Leaderboard
leaderboard: {
public: boolean // Allow non-admins to view
}

// Theme (future use)
theme: {
embedColor?: string // Hex color for embeds
backgroundUrl?: string // Custom rank card background
}
}

Global Configuration Defaults

Global defaults apply to all guilds unless overridden. They're stored at ['xp', 'global', 'config'].

import { config } from '@robojs/xp'

// Set global defaults
await config.setGlobal({
cooldownSeconds: 45,
xpRate: 1.5,
leaderboard: { public: true }
})

// Guild-specific overrides
await config.set(guildId, {
cooldownSeconds: 30 // This guild gets 30s cooldown
})

Config Precedence: Guild config > Global config > System defaults

MEE6 Parity Notes

This plugin matches MEE6's core mechanics:

  • XP per message: 15-25 XP (random, configurable via xpRate)
  • Cooldown: 60 seconds (configurable)
  • Level curve: XP = 5 * level² + 50 * level + 100
  • Role rewards: Stack or replace modes
  • Multipliers: Server, role, and user multipliers (MEE6 Pro feature)

TypeScript API Reference

XP Manipulation

import { XP } from '@robojs/xp'

// Add XP to a user
const result = await XP.addXP(guildId, userId, 100, { reason: 'contest_winner' })
console.log(result.leveledUp) // true if user leveled up

// Remove XP from a user
const result = await XP.removeXP(guildId, userId, 50, { reason: 'moderation' })
console.log(result.leveledDown) // true if user leveled down

// Set exact XP value
const result = await XP.setXP(guildId, userId, 5000, { reason: 'admin_adjustment' })

// Recalculate level and reconcile roles
const result = await XP.recalc(guildId, userId)
console.log(result.reconciled) // true if level was corrected

// Query user data
const user = await XP.getUser(guildId, userId)
const xp = await XP.getXP(guildId, userId)
const level = await XP.getLevel(guildId, userId)

// User object includes both message counters
console.log(user.messages) // Total messages sent: 423
console.log(user.xpMessages) // Messages that awarded XP: 156

Result Types:

interface XPChangeResult {
oldXp: number
newXp: number
oldLevel: number
newLevel: number
leveledUp: boolean
}

interface XPRemoveResult {
oldXp: number
newXp: number
oldLevel: number
newLevel: number
leveledDown: boolean
}

interface XPSetResult {
oldXp: number
newXp: number
oldLevel: number
newLevel: number
}

interface RecalcResult {
oldLevel: number
newLevel: number
totalXp: number
reconciled: boolean
}

Configuration Management

import { config } from '@robojs/xp'

// Get guild configuration
const guildConfig = await config.get(guildId)

// Update guild configuration (partial updates supported)
await config.set(guildId, {
cooldownSeconds: 45,
noXpChannelIds: ['123456789012345678']
})

// Validate configuration
const isValid = config.validate(configObject)

// Global configuration
const globalConfig = config.getGlobal()
config.setGlobal({ xpRate: 1.5 })

Leaderboard Queries

import { leaderboard } from '@robojs/xp'

// Get top 10 users (offset 0, limit 10)
const result = await leaderboard.get(guildId, 0, 10)
// Returns: { entries: Array<{ userId: string; xp: number; level: number; rank: number }>, total: number }

// Get next 10 users (offset 10, limit 10)
const page2 = await leaderboard.get(guildId, 10, 10)

// Get user's rank
const rankInfo = await leaderboard.getRank(guildId, userId)
// Returns: { rank: number; total: number } or null if not found

// Manually invalidate cache
await leaderboard.invalidateCache(guildId)

Role Rewards

import { reconcileRewards } from '@robojs/xp'

// Manually reconcile role rewards for a user
await reconcileRewards(guildId, userId)
// Automatically called on level changes
// Note: this is also available as rewards.reconcile(guildId, userId)

Math Utilities

import { math } from '@robojs/xp'

// XP needed to reach a specific level
const xpNeeded = math.xpNeededForLevel(10) // 1100

// Total XP accumulated up to a level
const totalXp = math.totalXpForLevel(10) // 5675

// Compute level from total XP
const progress = math.computeLevelFromTotalXp(5500)
// { level: 9, inLevel: 925, toNext: 175 }

// Progress within current level
const { percentage } = math.progressInLevel(5500) // ~84.1%

// Validate level or XP
const isValidLevel = math.isValidLevel(50) // true
const isValidXp = math.isValidXp(10000) // true

Event System

import { events } from '@robojs/xp'

// Listen for level-up events
events.onLevelUp(async ({ guildId, userId, oldLevel, newLevel, totalXp }) => {
console.log(`User ${userId} leveled up from ${oldLevel} to ${newLevel}!`)
})

// Listen for level-down events
events.onLevelDown(async ({ guildId, userId, oldLevel, newLevel, totalXp }) => {
console.log(`User ${userId} lost a level: ${oldLevel}${newLevel}`)
})

// Listen for XP changes
events.onXPChange(async ({ guildId, userId, oldXp, newXp, delta, reason }) => {
console.log(`User ${userId} XP changed by ${delta} (reason: ${reason})`)
})

// One-time listeners (generic API)
events.once('levelUp', handler)
events.once('levelDown', handler)
events.once('xpChange', handler)

// Remove listeners (generic API)
events.off('levelUp', handler)
events.off('levelDown', handler)
events.off('xpChange', handler)

Event Payloads:

interface LevelUpEvent {
guildId: string
userId: string
oldLevel: number
newLevel: number
totalXp: number
}

interface LevelDownEvent {
guildId: string
userId: string
oldLevel: number
newLevel: number
totalXp: number
}

interface XPChangeEvent {
guildId: string
userId: string
oldXp: number
newXp: number
delta: number
reason?: string
}

Constants

import { constants } from '@robojs/xp'

// Default configuration values
constants.DEFAULT_COOLDOWN // 60
constants.DEFAULT_XP_RATE // 1.0

// Level curve formula coefficients
constants.CURVE_A // 5
constants.CURVE_B // 50
constants.CURVE_C // 100

Additional APIs

config.getDefault()

Returns the default MEE6-compatible configuration before applying any global or guild overrides.

import { config } from '@robojs/xp'

const defaults = config.getDefault()
// Useful for building UIs or generating initial configs

math.xpDeltaForLevelRange()

Computes the XP difference needed to move between two levels (inclusive of starting point), useful for multi-level jumps and grants.

import { math } from '@robojs/xp'

// XP needed to go from level 5 to level 10
const delta = math.xpDeltaForLevelRange(5, 10)

rewards object

The rewards module is also available as an object if you prefer a namespaced import.

import { rewards } from '@robojs/xp'

// Reconcile a user's roles based on their current level
await rewards.reconcile(guildId, userId)

// Alias of reconcile for clarity
await rewards.reconcileRewards(guildId, userId)

Integration Recipes

The @robojs/xp plugin is designed to be extended. These recipes demonstrate common integration patterns using the event system and imperative API.

For a complete, production-ready example, see seed/events/_start/level-announcements.ts which demonstrates MEE6-style level-up announcements with rich embeds, progress bars, and extensive customization options.

Below are additional recipes for common use cases:

Contest Plugin: Award Bonus XP

// src/events/contest-winner.ts
import { XP } from '@robojs/xp'

export default async (interaction) => {
const winnerId = interaction.user.id
const guildId = interaction.guildId

// Award 500 bonus XP
const result = await XP.addXP(guildId, winnerId, 500, { reason: 'contest_winner' })

if (result.leveledUp) {
await interaction.reply({
content: `🎉 You won the contest and leveled up to ${result.newLevel}!`,
ephemeral: true
})
} else {
await interaction.reply({
content: '🎉 You won the contest and earned 500 XP!',
ephemeral: true
})
}
}

Moderation Plugin: Remove XP for Violations

// src/events/warn-issued.ts
import { XP } from '@robojs/xp'

export default async ({ userId, guildId, severity }) => {
const penalties = {
minor: 50,
moderate: 200,
severe: 500
}

const amount = penalties[severity] || 100

await XP.removeXP(guildId, userId, amount, { reason: `moderation_${severity}` })
}

Premium Plugin: Enable +50% XP Boost

// src/events/premium-activated.ts
import { config } from '@robojs/xp'

export default async ({ userId, guildId }) => {
// Set 1.5x multiplier for premium user
const guildConfig = await config.get(guildId)

await config.set(guildId, {
multipliers: {
...guildConfig.multipliers,
user: {
...(guildConfig.multipliers?.user || {}),
[userId]: 1.5
}
}
})
}

Analytics Plugin: Track XP Changes

// src/listeners/xp-analytics.ts
import { events, XP } from '@robojs/xp'
import { logger } from 'robo.js'

// Track all XP changes
events.onXPChange(async ({ guildId, userId, delta, reason }) => {
logger.info(`XP Analytics: ${userId} ${delta > 0 ? 'gained' : 'lost'} ${Math.abs(delta)} XP`, {
guildId,
userId,
delta,
reason
})

// Send to analytics service
await analytics.track('xp_change', {
guild: guildId,
user: userId,
amount: delta,
reason
})
})

// Also track message activity
events.onXPChange(async ({ guildId, userId }) => {
const user = await XP.getUser(guildId, userId)

// Calculate XP efficiency (what % of messages award XP)
const efficiency = user.messages > 0 ? ((user.xpMessages / user.messages) * 100).toFixed(1) : 0

logger.info(`User ${userId} XP efficiency: ${efficiency}%`, {
totalMessages: user.messages,
xpMessages: user.xpMessages
})
})

Note: Track message efficiency to identify users affected by cooldowns or No-XP restrictions.

Announcement Plugin: Send Level-Up Messages

// src/listeners/level-announcements.ts
import { events } from '@robojs/xp'
import { client } from 'robo.js'
import { EmbedBuilder } from 'discord.js'

events.onLevelUp(async ({ guildId, userId, newLevel, totalXp }) => {
const guild = await client.guilds.fetch(guildId)
const user = await guild.members.fetch(userId)

// Find announcement channel
const channel = guild.channels.cache.find((c) => c.name === 'level-ups')
if (!channel?.isTextBased()) return

const embed = new EmbedBuilder()
.setTitle('🎉 Level Up!')
.setDescription(`${user} reached **Level ${newLevel}**!`)
.addFields({ name: 'Total XP', value: totalXp.toString(), inline: true })
.setColor('#00ff00')
.setThumbnail(user.displayAvatarURL())

await channel.send({ embeds: [embed] })
})

Custom Rewards: Build Custom Logic

// src/listeners/custom-rewards.ts
import { events } from '@robojs/xp'
import { client } from 'robo.js'

events.onLevelUp(async ({ guildId, userId, newLevel }) => {
const guild = await client.guilds.fetch(guildId)
const member = await guild.members.fetch(userId)

// Custom reward logic
switch (newLevel) {
case 10:
// Award custom badge
await giveCustomBadge(member, 'veteran')
break
case 25:
// Unlock special channel
await unlockChannel(member, 'vip-lounge')
break
case 50:
// Grant special permissions
await grantPermission(member, 'create_events')
break
}
})

REST API Documentation

Prerequisite: Install @robojs/server to enable HTTP endpoints

Response Format

All endpoints return JSON with the following structure:

Success:

{
"success": true,
"data": { ... }
}

Error:

{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}

Error Codes

CodeHTTP StatusDescription
MISSING_GUILD_ID400Guild ID parameter missing
GUILD_NOT_FOUND404Guild not found or bot not member
MISSING_USER_ID400User ID parameter missing
USER_NOT_FOUND404User has no XP record
METHOD_NOT_ALLOWED405HTTP method not allowed
INVALID_REQUEST400Invalid request body or parameters
INVALID_AMOUNT400Invalid XP amount
INVALID_CONFIG400Invalid configuration
INVALID_LEVEL400Invalid level value
INVALID_ROLE_ID400Invalid Discord role ID
INVALID_MULTIPLIER400Invalid multiplier value
DUPLICATE_REWARD400Role reward already exists at level
REWARD_NOT_FOUND404Role reward not found
INTERNAL_ERROR500Unexpected server error

Endpoints

User XP Data

GET /api/xp/users/:guildId/:userId Get user XP data and level progress.

Terminal
curl http://localhost:3000/api/xp/users/123456789012345678/987654321098765432

Response:

{
"success": true,
"data": {
"user": {
"xp": 5500,
"level": 10,
"messages": 423,
"xpMessages": 156,
"lastAwardedAt": 1234567890000
},
"progress": {
"level": 10,
"inLevel": 495,
"toNext": 1155
},
"percentage": 42.86
}
}

POST /api/xp/users/:guildId/:userId Add XP to user.

Terminal
curl -X POST http://localhost:3000/api/xp/users/123.../987...   -H "Content-Type: application/json"   -d '{"amount": 100, "reason": "api_award"}'

PUT /api/xp/users/:guildId/:userId Set user XP to specific value.

Terminal
curl -X PUT http://localhost:3000/api/xp/users/123.../987...   -H "Content-Type: application/json"   -d '{"xp": 5000, "reason": "api_set"}'

DELETE /api/xp/users/:guildId/:userId Remove XP from user.

Terminal
curl -X DELETE http://localhost:3000/api/xp/users/123.../987...   -H "Content-Type: application/json"   -d '{"amount": 50, "reason": "api_penalty"}'

Recalculate Level

POST /api/xp/users/:guildId/:userId/recalc Recalculate user's level from total XP.

Terminal
curl -X POST http://localhost:3000/api/xp/users/123.../987.../recalc

Guild Configuration

GET /api/xp/config/:guildId Get current guild configuration.

PUT /api/xp/config/:guildId Update guild configuration (partial updates supported).

Terminal
curl -X PUT http://localhost:3000/api/xp/config/123...   -H "Content-Type: application/json"   -d '{"cooldownSeconds": 45, "xpRate": 1.5}'

Role Rewards

GET /api/xp/config/:guildId/rewards List all role rewards.

POST /api/xp/config/:guildId/rewards Add new role reward.

Terminal
curl -X POST http://localhost:3000/api/xp/config/123.../rewards   -H "Content-Type: application/json"   -d '{"level": 10, "roleId": "456789012345678901"}'

DELETE /api/xp/config/:guildId/rewards Remove role reward by level.

Terminal
curl -X DELETE http://localhost:3000/api/xp/config/123.../rewards   -H "Content-Type: application/json"   -d '{"level": 10}'

Multipliers

GET /api/xp/config/:guildId/multipliers Get all multipliers (server, role, user).

PUT /api/xp/config/:guildId/multipliers Set/update multipliers.

Terminal
curl -X PUT http://localhost:3000/api/xp/config/123.../multipliers   -H "Content-Type: application/json"   -d '{"server": 2.0, "role": {"456...": 1.5}}'

DELETE /api/xp/config/:guildId/multipliers Remove specific multipliers.

Terminal
curl -X DELETE http://localhost:3000/api/xp/config/123.../multipliers   -H "Content-Type: application/json"   -d '{"role": ["456..."], "user": ["789..."]}'

Global Configuration

GET /api/xp/config/global Get global configuration defaults.

PUT /api/xp/config/global Update global defaults (affects all guilds).

Terminal
curl -X PUT http://localhost:3000/api/xp/config/global   -H "Content-Type: application/json"   -d '{"cooldownSeconds": 45, "xpRate": 1.2}'

Performance & Caching

Caching Strategy

  • Leaderboard cache: Top 100 users per guild, 60-second TTL
  • Config cache: In-memory with event-driven invalidation
  • Complexity: O(1) cached reads, O(n log n) refresh for n users

Scalability

  • Memory per guild: ~10KB for 100 cached leaderboard entries
  • Recommended limits: 100k users per guild max
  • Cache eviction: TTL-based, auto-refreshes on query after expiry

See PERFORMANCE.md for detailed benchmarks and optimization guide.

Data Model

Flashcore keys live under ['xp', guildId]:

KeyContentsExample
configGuild config merged with global defaults{ cooldownSeconds: 60, xpRate: 1.0, ... }
user:{userId}UserXP record{ xp: 5500, level: 10, messages: 423, xpMessages: 156, lastAwardedAt: 1234567890000 }
membersSet of tracked member IDs['user1', 'user2', ...]
lb:{perPage}:{page}Leaderboard cache[{ userId, xp, level, rank }, ...]
schemaSchema version for future migrations1

Global defaults live at ['xp', 'global', 'config'].

UserXP Structure

interface UserXP {
xp: number // Total XP accumulated
level: number // Current level (computed from xp)
messages: number // Total messages sent in guild text channels
xpMessages: number // Messages that awarded XP (subset of messages)
lastAwardedAt: number // Timestamp of last XP award (Unix ms)
}

Message Counter Distinction:

  • messages tracks all messages sent in guild text channels (increments after basic validation: bot check, guild check, text channel check)
  • xpMessages tracks only messages that actually awarded XP (increments after all checks pass: No-XP channels, No-XP roles, cooldown)

These counters will differ when:

  • User sends messages within cooldown period (default 60s)
  • User has a No-XP role
  • User sends messages in No-XP channels
  • User is a bot (neither counter increments)

Example: A user sends 100 messages in 5 minutes. With a 60s cooldown, only ~5 messages award XP. Result: messages: 100, xpMessages: 5.

GuildConfig Structure

See Configuration Guide above for complete structure.

MEE6 Parity

This plugin provides feature parity with MEE6's XP system:

Parity Features

  • XP per message: 15-25 XP (configurable)
  • Cooldown: 60 seconds (configurable)
  • Level curve: Same quadratic formula
  • Role rewards: Stack or replace modes
  • Multipliers: Server, role, user (MEE6 Pro)
  • No-XP roles/channels
  • Leaderboard pagination
  • Admin commands

Configuration for MEE6-like Behavior

import { config } from '@robojs/xp'

await config.set(guildId, {
cooldownSeconds: 60,
xpRate: 1.0,
rewardsMode: 'stack',
removeRewardOnXpLoss: false,
leaderboard: { public: true }
})

Troubleshooting

XP Not Being Awarded

Possible causes:

  1. Channel type: Only text channels award XP
  2. No-XP roles: User has a role in noXpRoleIds
  3. No-XP channels: Channel is in noXpChannelIds
  4. Cooldown: User sent message within cooldown period (default 60s)
  5. Bot messages: Bots don't earn XP

Debug:

import { config } from '@robojs/xp'
const guildConfig = await config.get(guildId)
console.log('No-XP roles:', guildConfig.noXpRoleIds)
console.log('No-XP channels:', guildConfig.noXpChannelIds)
console.log('Cooldown:', guildConfig.cooldownSeconds)

Message Counter Discrepancy

Issue: The messages count is much higher than xpMessages count.

This is expected behavior. The two counters track different metrics:

  • messages: Total messages sent in guild text channels (after basic validation)
  • xpMessages: Messages that actually awarded XP (after all checks)

Common reasons for discrepancy:

  1. Cooldown blocking XP: With default 60s cooldown, rapid messages don't award XP

    • Example: 10 messages in 1 minute → messages: 10, xpMessages: 1
  2. No-XP channels: Messages in excluded channels increment messages but not xpMessages

    • Check: config.noXpChannelIds
  3. No-XP roles: Users with excluded roles increment messages but not xpMessages

    • Check: config.noXpRoleIds
  4. Temporary restrictions: User had No-XP role or was in No-XP channel for a period

Debug:

import { XP, config } from '@robojs/xp'
const user = await XP.getUser(guildId, userId)
const guildConfig = await config.get(guildId)

console.log('Total messages:', user.messages)
console.log('XP messages:', user.xpMessages)
console.log('Ratio:', ((user.xpMessages / user.messages) * 100).toFixed(1) + '%')
console.log('Cooldown:', guildConfig.cooldownSeconds + 's')
console.log('No-XP channels:', guildConfig.noXpChannelIds)
console.log('No-XP roles:', guildConfig.noXpRoleIds)

Expected ratios:

  • Active chatters with 60s cooldown: 10-30% (depends on chat frequency)
  • Users in No-XP channels frequently: 5-15%
  • Users with temporary No-XP role: varies widely

This is not a bug - it's intentional design to track both total activity and XP-eligible activity.

Roles Not Being Granted

Possible causes:

  1. Bot permissions: Bot lacks Manage Roles permission
  2. Role hierarchy: Reward role is higher than bot's highest role
  3. Managed roles: Cannot assign managed roles (e.g., Nitro Boost)
  4. Missing role: Role was deleted but still in roleRewards

Fix:

import { reconcileRewards, config } from '@robojs/xp'

// Recalculate roles for a user
await reconcileRewards(guildId, userId)

// Remove deleted roles from config
const guildConfig = await config.get(guildId)
const validRewards = guildConfig.roleRewards.filter((r) => guild.roles.cache.has(r.roleId))
await config.set(guildId, { roleRewards: validRewards })

Leaderboard Showing Stale Data

Cause: Cache TTL (60 seconds)

Solution:

import { leaderboard } from '@robojs/xp'
// Manually invalidate cache
await leaderboard.invalidateCache(guildId)

// Or wait for TTL to expire (auto-refreshes on next query)

Performance Issues

Large guilds (10k+ users):

  • First leaderboard query after cache expiry may take 200-500ms
  • Subsequent queries are under 10ms (cached)
  • Consider warming cache during off-peak hours

Cache warming:

import { leaderboard } from '@robojs/xp'
// Warm cache for all guilds (top 100 users)
for (const guildId of guildIds) {
await leaderboard.get(guildId, 0, 100)
}

Development

Run tests or build from the package root:

Terminal
# Run all tests
pnpm test

# Build plugin
pnpm build plugin

More on GitHub