@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.
➞ 📚 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
/xpcommand 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
npx robo add @robojs/xpNew to Robo.js? Start your project with this plugin pre-installed:
npx create-robo <project-name> -p @robojs/xpOptional: REST API
To enable the HTTP API endpoints, install @robojs/server:
npx robo add @robojs/serverSlash Commands Reference
Admin Commands (Require Manage Guild Permission)
XP Manipulation
| Command | Description | Options |
|---|---|---|
/xp give <user> <amount> [reason] | Award XP to a user | user (required), amount (1-1,000,000), reason (optional) |
/xp remove <user> <amount> [reason] | Remove XP from a user | user (required), amount (1-1,000,000), reason (optional) |
/xp set <user> <amount> [reason] | Set exact XP value | user (required), amount (0-1,000,000), reason (optional) |
/xp recalc <user> | Recalculate level and reconcile roles | user (required) |
Role Rewards
| Command | Description | Options |
|---|---|---|
/xp rewards add <level> <role> | Add role reward at level | level (1-1000), role (required) |
/xp rewards remove <level> | Remove role reward | level (required) |
/xp rewards list | List all role rewards | None |
/xp rewards mode <mode> | Set stack or replace mode | mode (stack or replace) |
/xp rewards remove-on-loss <enabled> | Toggle reward removal on XP loss | enabled (boolean) |
Stack Mode: Users keep all role rewards from previous levels Replace Mode: Users only get the highest level role reward
Configuration
| Command | Description | Options |
|---|---|---|
/xp config get | View current configuration | None |
/xp config set-cooldown <seconds> | Set XP award cooldown | seconds (0-3600) |
/xp config set-xp-rate <rate> | Set XP rate multiplier | rate (0.1-10.0) |
/xp config add-no-xp-role <role> | Add No-XP role | role (required) |
/xp config remove-no-xp-role <role> | Remove No-XP role | role (required) |
/xp config add-no-xp-channel <channel> | Add No-XP channel | channel (required) |
/xp config remove-no-xp-channel <channel> | Remove No-XP channel | channel (required) |
/xp config set-leaderboard-public <enabled> | Toggle public leaderboard | enabled (boolean) |
Multipliers
| Command | Description | Options |
|---|---|---|
/xp multiplier server <multiplier> | Set server-wide XP multiplier | multiplier (0.1-10.0) |
/xp multiplier role <role> <multiplier> | Set role XP multiplier | role, multiplier (0.1-10.0) |
/xp multiplier user <user> <multiplier> | Set user XP multiplier | user, multiplier (0.1-10.0) |
/xp multiplier remove-role <role> | Remove role multiplier | role (required) |
/xp multiplier remove-user <user> | Remove user multiplier | user (required) |
User Commands (No Permission Required)
| Command | Description | Options |
|---|---|---|
/rank [user] | View rank card with progress | user (optional, defaults to self) |
/leaderboard [page] | View server leaderboard | page (optional, default: 1) |
Note:
/leaderboardrequires admin permission whenleaderboard.publicisfalse
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/serverto 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
| Code | HTTP Status | Description |
|---|---|---|
MISSING_GUILD_ID | 400 | Guild ID parameter missing |
GUILD_NOT_FOUND | 404 | Guild not found or bot not member |
MISSING_USER_ID | 400 | User ID parameter missing |
USER_NOT_FOUND | 404 | User has no XP record |
METHOD_NOT_ALLOWED | 405 | HTTP method not allowed |
INVALID_REQUEST | 400 | Invalid request body or parameters |
INVALID_AMOUNT | 400 | Invalid XP amount |
INVALID_CONFIG | 400 | Invalid configuration |
INVALID_LEVEL | 400 | Invalid level value |
INVALID_ROLE_ID | 400 | Invalid Discord role ID |
INVALID_MULTIPLIER | 400 | Invalid multiplier value |
DUPLICATE_REWARD | 400 | Role reward already exists at level |
REWARD_NOT_FOUND | 404 | Role reward not found |
INTERNAL_ERROR | 500 | Unexpected server error |
Endpoints
User XP Data
GET /api/xp/users/:guildId/:userId
Get user XP data and level progress.
curl http://localhost:3000/api/xp/users/123456789012345678/987654321098765432Response:
{
"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.
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.
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.
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.
curl -X POST http://localhost:3000/api/xp/users/123.../987.../recalcGuild Configuration
GET /api/xp/config/:guildId
Get current guild configuration.
PUT /api/xp/config/:guildId
Update guild configuration (partial updates supported).
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.
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.
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.
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.
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).
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]:
| Key | Contents | Example |
|---|---|---|
config | Guild config merged with global defaults | { cooldownSeconds: 60, xpRate: 1.0, ... } |
user:{userId} | UserXP record | { xp: 5500, level: 10, messages: 423, xpMessages: 156, lastAwardedAt: 1234567890000 } |
members | Set of tracked member IDs | ['user1', 'user2', ...] |
lb:{perPage}:{page} | Leaderboard cache | [{ userId, xp, level, rank }, ...] |
schema | Schema version for future migrations | 1 |
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:
messagestracks all messages sent in guild text channels (increments after basic validation: bot check, guild check, text channel check)xpMessagestracks 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:
- Channel type: Only text channels award XP
- No-XP roles: User has a role in
noXpRoleIds - No-XP channels: Channel is in
noXpChannelIds - Cooldown: User sent message within cooldown period (default 60s)
- 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:
-
Cooldown blocking XP: With default 60s cooldown, rapid messages don't award XP
- Example: 10 messages in 1 minute →
messages: 10,xpMessages: 1
- Example: 10 messages in 1 minute →
-
No-XP channels: Messages in excluded channels increment
messagesbut notxpMessages- Check:
config.noXpChannelIds
- Check:
-
No-XP roles: Users with excluded roles increment
messagesbut notxpMessages- Check:
config.noXpRoleIds
- Check:
-
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:
- Bot permissions: Bot lacks
Manage Rolespermission - Role hierarchy: Reward role is higher than bot's highest role
- Managed roles: Cannot assign managed roles (e.g., Nitro Boost)
- 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:
# Run all tests
pnpm test
# Build plugin
pnpm build plugin