LogoRobo.js

Recipes

Common integration patterns for building custom XP-based features.

The XP plugin is designed for extension. These recipes demonstrate common patterns using the event system and imperative API.

contest-winner.tsContest bonus XP
reputation.tsMulti-dimensional reputation
seasons.tsSeasonal battle pass
setup.tsProgrammatic setup wizard
level-announcements.tsLevel-up messages
xp-analytics.tsAnalytics tracking
custom-rewards.tsMilestone rewards
economy.tsMulti-currency economy
warn-issued.tsModeration penalties
premium-activated.tsPremium XP boost

Level-up announcements

Send a message when users level up. The plugin seeds a starter template at seed/robo/start/level-announcements.ts with rich embeds and customization options.

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

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

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

	const embed = new EmbedBuilder()
		.setTitle('Level Up')
		.setDescription(`${member} reached **Level ${newLevel}**.`)
		.addFields({ name: 'Total XP', value: totalXp.toLocaleString(), inline: true })
		.setColor(0x00ff00)
		.setThumbnail(member.displayAvatarURL())

	await channel.send({ embeds: [embed] })
})
src/robo/start/level-announcements.js
import { events } from '@robojs/xp'
import { getClient } from '@robojs/discordjs'
import { EmbedBuilder } from 'discord.js'

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

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

	const embed = new EmbedBuilder()
		.setTitle('Level Up')
		.setDescription(`${member} reached **Level ${newLevel}**.`)
		.addFields({ name: 'Total XP', value: totalXp.toLocaleString(), inline: true })
		.setColor(0x00ff00)
		.setThumbnail(member.displayAvatarURL())

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

Contest bonus XP

Award bonus XP to contest winners or event participants.

src/commands/contest-winner.ts
import { XP } from '@robojs/xp'
import type { CommandResult } from '@robojs/discordjs'

export default async (interaction) => {
	const winner = interaction.options.getUser('user', true)
	const guildId = interaction.guildId

	const result = await XP.addXP(guildId, winner.id, 500, { reason: 'contest_winner' })

	if (result.leveledUp) {
		return `${winner} won the contest, earned 500 XP, and leveled up to ${result.newLevel}.`
	}
	return `${winner} won the contest and earned 500 XP.`
}
src/commands/contest-winner.js
import { XP } from '@robojs/xp'

export default async (interaction) => {
	const winner = interaction.options.getUser('user', true)
	const guildId = interaction.guildId

	const result = await XP.addXP(guildId, winner.id, 500, { reason: 'contest_winner' })

	if (result.leveledUp) {
		return `${winner} won the contest, earned 500 XP, and leveled up to ${result.newLevel}.`
	}
	return `${winner} won the contest and earned 500 XP.`
}

Moderation penalties

Remove XP when users receive warnings or infractions.

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

const penalties: Record<string, number> = {
	minor: 50,
	moderate: 200,
	severe: 500
}

export default async ({ userId, guildId, severity }: WarnEvent) => {
	const amount = penalties[severity] ?? 100
	const result = await XP.removeXP(guildId, userId, amount, {
		reason: `moderation_${severity}`
	})

	if (result.leveledDown) {
		console.log(`${userId} dropped to level ${result.newLevel} after ${severity} warning`)
	}
}
src/events/warn-issued.js
import { XP } from '@robojs/xp'

const penalties = {
	minor: 50,
	moderate: 200,
	severe: 500
}

export default async ({ userId, guildId, severity }) => {
	const amount = penalties[severity] ?? 100
	const result = await XP.removeXP(guildId, userId, amount, {
		reason: `moderation_${severity}`
	})

	if (result.leveledDown) {
		console.log(`${userId} dropped to level ${result.newLevel} after ${severity} warning`)
	}
}

Premium XP boost

Give premium/booster users a 50% XP multiplier.

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

export default async ({ userId, guildId }: PremiumEvent) => {
	const guildConfig = await config.get(guildId)

	await config.set(guildId, {
		multipliers: {
			...guildConfig.multipliers,
			user: {
				...(guildConfig.multipliers?.user ?? {}),
				[userId]: 1.5
			}
		}
	})
}
src/events/premium-activated.js
import { config } from '@robojs/xp'

export default async ({ userId, guildId }) => {
	const guildConfig = await config.get(guildId)

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

Analytics tracking

Forward XP events to an analytics service.

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

events.onXPChange(async ({ guildId, userId, delta, reason, storeId }) => {
	if (storeId !== 'default') return

	logger.info(`XP: ${userId} ${delta > 0 ? 'gained' : 'lost'} ${Math.abs(delta)} XP`, {
		guildId,
		userId,
		delta,
		reason
	})

	// Forward to external analytics
	await analyticsService.track('xp_change', {
		guild: guildId,
		user: userId,
		amount: delta,
		reason
	})
})
src/robo/start/xp-analytics.js
import { events, XP } from '@robojs/xp'
import { logger } from 'robo.js'

events.onXPChange(async ({ guildId, userId, delta, reason, storeId }) => {
	if (storeId !== 'default') return

	logger.info(`XP: ${userId} ${delta > 0 ? 'gained' : 'lost'} ${Math.abs(delta)} XP`, {
		guildId,
		userId,
		delta,
		reason
	})

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

Manual XP control

Disable automatic XP earning and only award XP through admin commands or the API. Set the server multiplier to 0.

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

// Disable automatic XP for entire guild
await config.set(guildId, {
	multipliers: { server: 0 }
})

// Admin commands still work
await XP.addXP(guildId, userId, 1000, { reason: 'contest_winner' })
await XP.addXP(guildId, userId, 500, { reason: 'event_participation' })
import { config, XP } from '@robojs/xp'

// Disable automatic XP for entire guild
await config.set(guildId, {
	multipliers: { server: 0 }
})

// Admin commands still work
await XP.addXP(guildId, userId, 1000, { reason: 'contest_winner' })
await XP.addXP(guildId, userId, 500, { reason: 'event_participation' })

The messages counter still increments (tracking activity), but xpMessages stays at 0 since no automatic XP is awarded.

Use cases:

  • Contest-only XP systems
  • Event-based rewards (reactions, voice time)
  • Admin-reviewed XP grants
  • Temporary freeze during maintenance

Custom milestone rewards

Trigger custom actions at specific level milestones.

src/robo/start/custom-rewards.ts
import { events } from '@robojs/xp'
import { getClient } from '@robojs/discordjs'

events.onLevelUp(async ({ guildId, userId, newLevel, storeId }) => {
	if (storeId !== 'default') return

	const client = getClient()
	const guild = await client.guilds.fetch(guildId)
	const member = await guild.members.fetch(userId)

	switch (newLevel) {
		case 10:
			await giveCustomBadge(member, 'veteran')
			break
		case 25:
			await unlockChannel(member, 'vip-lounge')
			break
		case 50:
			await grantPermission(member, 'create_events')
			break
	}
})
src/robo/start/custom-rewards.js
import { events } from '@robojs/xp'
import { getClient } from '@robojs/discordjs'

events.onLevelUp(async ({ guildId, userId, newLevel, storeId }) => {
	if (storeId !== 'default') return

	const client = getClient()
	const guild = await client.guilds.fetch(guildId)
	const member = await guild.members.fetch(userId)

	switch (newLevel) {
		case 10:
			await giveCustomBadge(member, 'veteran')
			break
		case 25:
			await unlockChannel(member, 'vip-lounge')
			break
		case 50:
			await grantPermission(member, 'create_events')
			break
	}
})

Multi-currency economy

Award XP to multiple stores simultaneously for parallel progression systems.

src/events/messageCreate/economy.ts
import { XP } from '@robojs/xp'

export default async (message) => {
	const guildId = message.guildId
	const userId = message.author.id

	// Default store handles leveling + role rewards
	await XP.addXP(guildId, userId, 20, { reason: 'message' })

	// Server currency
	await XP.addXP(guildId, userId, 10, { reason: 'message', storeId: 'coins' })

	// Premium currency for boosters
	if (message.member?.roles.cache.has(PREMIUM_ROLE_ID)) {
		await XP.addXP(guildId, userId, 5, {
			reason: 'premium_message',
			storeId: 'tokens'
		})
	}
}
src/events/messageCreate/economy.js
import { XP } from '@robojs/xp'

export default async (message) => {
	const guildId = message.guildId
	const userId = message.author.id

	// Default store handles leveling + role rewards
	await XP.addXP(guildId, userId, 20, { reason: 'message' })

	// Server currency
	await XP.addXP(guildId, userId, 10, { reason: 'message', storeId: 'coins' })

	// Premium currency for boosters
	if (message.member?.roles.cache.has(PREMIUM_ROLE_ID)) {
		await XP.addXP(guildId, userId, 5, {
			reason: 'premium_message',
			storeId: 'tokens'
		})
	}
}

Reputation system

Track multiple types of reputation independently.

src/core/reputation.ts
import { XP, events } from '@robojs/xp'

export async function awardHelpfulness(guildId: string, userId: string) {
	await XP.addXP(guildId, userId, 25, {
		reason: 'helped_member',
		storeId: 'helpfulness'
	})
}

export async function awardCreativity(guildId: string, userId: string) {
	await XP.addXP(guildId, userId, 50, {
		reason: 'created_content',
		storeId: 'creativity'
	})
}

// Listen for reputation level-ups
events.onLevelUp(({ userId, newLevel, storeId }) => {
	if (storeId === 'helpfulness') {
		console.log(`${userId} reached helpfulness level ${newLevel}`)
	}
})
src/core/reputation.js
import { XP, events } from '@robojs/xp'

export async function awardHelpfulness(guildId, userId) {
	await XP.addXP(guildId, userId, 25, {
		reason: 'helped_member',
		storeId: 'helpfulness'
	})
}

export async function awardCreativity(guildId, userId) {
	await XP.addXP(guildId, userId, 50, {
		reason: 'created_content',
		storeId: 'creativity'
	})
}

// Listen for reputation level-ups
events.onLevelUp(({ userId, newLevel, storeId }) => {
	if (storeId === 'helpfulness') {
		console.log(`${userId} reached helpfulness level ${newLevel}`)
	}
})

Seasonal battle pass

Run time-limited progression with isolated stores.

src/core/seasons.ts
import { XP, config } from '@robojs/xp'

const CURRENT_SEASON = 'season3'

export async function awardSeasonalXP(guildId: string, userId: string, amount: number) {
	return XP.addXP(guildId, userId, amount, {
		reason: 'seasonal_activity',
		storeId: CURRENT_SEASON
	})
}

export async function getSeasonalProgress(guildId: string, userId: string) {
	const xp = await XP.getXP(guildId, userId, { storeId: CURRENT_SEASON })
	const level = await XP.getLevel(guildId, userId, { storeId: CURRENT_SEASON })
	return { xp, level, season: CURRENT_SEASON }
}
src/core/seasons.js
import { XP, config } from '@robojs/xp'

const CURRENT_SEASON = 'season3'

export async function awardSeasonalXP(guildId, userId, amount) {
	return XP.addXP(guildId, userId, amount, {
		reason: 'seasonal_activity',
		storeId: CURRENT_SEASON
	})
}

export async function getSeasonalProgress(guildId, userId) {
	const xp = await XP.getXP(guildId, userId, { storeId: CURRENT_SEASON })
	const level = await XP.getLevel(guildId, userId, { storeId: CURRENT_SEASON })
	return { xp, level, season: CURRENT_SEASON }
}

Old season data stays in its store indefinitely. Start a new season by using a new store ID. The default store (permanent progression) is unaffected.

Programmatic setup wizard

Configure XP settings programmatically for new guilds (e.g., from a web dashboard or setup wizard).

src/core/setup.ts
import { config } from '@robojs/xp'

export async function setupGuild(guildId: string) {
	await config.set(guildId, {
		cooldownSeconds: 45,
		xpRate: 1.2,
		leaderboard: { public: true },
		noXpRoleIds: ['MUTED_ROLE_ID'],
		noXpChannelIds: ['BOT_COMMANDS_CHANNEL_ID'],
		roleRewards: [
			{ level: 5, roleId: 'BRONZE_ROLE_ID' },
			{ level: 10, roleId: 'SILVER_ROLE_ID' },
			{ level: 25, roleId: 'GOLD_ROLE_ID' }
		],
		rewardsMode: 'stack',
		multipliers: {
			server: 1.0,
			role: { 'BOOSTER_ROLE_ID': 2.0 }
		}
	})
}
src/core/setup.js
import { config } from '@robojs/xp'

export async function setupGuild(guildId) {
	await config.set(guildId, {
		cooldownSeconds: 45,
		xpRate: 1.2,
		leaderboard: { public: true },
		noXpRoleIds: ['MUTED_ROLE_ID'],
		noXpChannelIds: ['BOT_COMMANDS_CHANNEL_ID'],
		roleRewards: [
			{ level: 5, roleId: 'BRONZE_ROLE_ID' },
			{ level: 10, roleId: 'SILVER_ROLE_ID' },
			{ level: 25, roleId: 'GOLD_ROLE_ID' }
		],
		rewardsMode: 'stack',
		multipliers: {
			server: 1.0,
			role: { 'BOOSTER_ROLE_ID': 2.0 }
		}
	})
}

This uses the same API as the /xp config command, making it easy to build custom management interfaces.

XP efficiency tracking

Monitor how many messages actually award XP versus total messages sent.

import { XP } from '@robojs/xp'

const user = await XP.getUser(guildId, userId)
if (user && user.messages > 0) {
	const efficiency = ((user.xpMessages / user.messages) * 100).toFixed(1)
	console.log(`XP efficiency: ${efficiency}%`)
	console.log(`Total messages: ${user.messages}`)
	console.log(`XP messages: ${user.xpMessages}`)
}
import { XP } from '@robojs/xp'

const user = await XP.getUser(guildId, userId)
if (user && user.messages > 0) {
	const efficiency = ((user.xpMessages / user.messages) * 100).toFixed(1)
	console.log(`XP efficiency: ${efficiency}%`)
	console.log(`Total messages: ${user.messages}`)
	console.log(`XP messages: ${user.xpMessages}`)
}

Expected ratios:

  • Active chatters with 60s cooldown: 10-30%
  • Users in no-XP channels frequently: 5-15%
  • Users with temporary no-XP role: varies

Next steps

On this page