LogoRobo.js

Role rewards

Automatically assign Discord roles based on user level.

Role rewards grant Discord roles when users reach specific levels. The plugin handles role assignment, removal, and conflict resolution automatically.

How it works

Configure level-to-role mappings via /xp config or the API. When a user's level changes, the plugin reconciles their roles based on the configured rewards and mode.

import { config } from '@robojs/xp'

await config.set(guildId, {
	roleRewards: [
		{ level: 5, roleId: '111111111111111111' },
		{ level: 10, roleId: '222222222222222222' },
		{ level: 25, roleId: '333333333333333333' },
		{ level: 50, roleId: '444444444444444444' }
	]
})
import { config } from '@robojs/xp'

await config.set(guildId, {
	roleRewards: [
		{ level: 5, roleId: '111111111111111111' },
		{ level: 10, roleId: '222222222222222222' },
		{ level: 25, roleId: '333333333333333333' },
		{ level: 50, roleId: '444444444444444444' }
	]
})

Reward modes

Stack mode (default)

Users keep all role rewards from previous levels. A level 25 user has the level 5, 10, and 25 roles.

await config.set(guildId, { rewardsMode: 'stack' })
await config.set(guildId, { rewardsMode: 'stack' })

Replace mode

Users only keep the highest qualifying role. A level 25 user has only the level 25 role -- lower rewards are removed.

await config.set(guildId, { rewardsMode: 'replace' })
await config.set(guildId, { rewardsMode: 'replace' })

Replace mode is useful when rewards represent tiers (Bronze, Silver, Gold) where only the current tier should be visible.

Remove on XP loss

By default, users keep their role rewards even if they lose levels (e.g., from /xp remove). Enable removeRewardOnXpLoss to remove roles when users drop below the required level.

await config.set(guildId, { removeRewardOnXpLoss: true })
await config.set(guildId, { removeRewardOnXpLoss: true })

In stack mode, roles above the user's new level are removed. In replace mode, the user's role is updated to match their new highest qualifying level.

Managing rewards via commands

Use /xp config and navigate to the Role Rewards category:

  • Add reward -- Select a role and enter the level requirement (1-1000)
  • Remove reward -- Choose from a list of configured rewards to remove
  • Set mode -- Switch between stack and replace
  • Toggle remove-on-loss -- Enable/disable role removal on level loss

Reconciliation

Role reconciliation runs automatically when:

  • A user levels up (via message XP or admin commands)
  • A user levels down (via XP removal)
  • An admin runs /xp recalc @user

Manual reconciliation

Trigger reconciliation programmatically when needed (e.g., after changing reward configuration):

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

const guildConfig = await config.get(guildId)
const userLevel = await XP.getLevel(guildId, userId)

await rewards.reconcile(guildId, userId, userLevel, guildConfig)
import { rewards, config, XP } from '@robojs/xp'

const guildConfig = await config.get(guildId)
const userLevel = await XP.getLevel(guildId, userId)

await rewards.reconcile(guildId, userId, userLevel, guildConfig)

The reconcile function requires 4 arguments: guildId, userId, newLevel, and guildConfig. Pass the full guild config and the user's current level.

Permission requirements

The bot needs these permissions for role rewards to work:

  1. Manage Roles -- Required to add/remove roles
  2. Role hierarchy -- The bot's highest role must be above all reward roles
  3. Not managed -- Cannot assign managed roles (e.g., Nitro Booster, integration-managed)

The plugin checks all three conditions before each role operation. Failed operations are logged but don't throw errors, so other rewards still process.

Default store only

Role rewards only trigger for the default store. Custom stores (e.g., 'reputation', 'coins') never grant or remove Discord roles, even if they have roleRewards configured.

This prevents conflicts where multiple progression systems would compete over the same roles. If you need role rewards for a custom store, build a custom listener:

import { events } from '@robojs/xp'
import { getClient } from '@robojs/discordjs'

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

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

	if (newLevel >= 10) {
		await member.roles.add('REPUTATION_ROLE_ID')
	}
})
import { events } from '@robojs/xp'
import { getClient } from '@robojs/discordjs'

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

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

	if (newLevel >= 10) {
		await member.roles.add('REPUTATION_ROLE_ID')
	}
})

Duplicate handling

If the same role appears at multiple levels, the plugin keeps only the highest level entry. This is deduplicated automatically during reconciliation.

Troubleshooting

Roles not being granted

  1. Check that the bot has the Manage Roles permission
  2. Verify the bot's highest role is above all reward roles in the server settings
  3. Confirm the reward roles aren't managed (integration or boost roles)
  4. Run /xp recalc @user to force reconciliation

Roles not removed after level loss

Check that removeRewardOnXpLoss is enabled:

const guildConfig = await config.get(guildId)
console.log('Remove on loss:', guildConfig.removeRewardOnXpLoss) // false by default
const guildConfig = await config.get(guildId)
console.log('Remove on loss:', guildConfig.removeRewardOnXpLoss) // false by default

Next steps

On this page