NapNodes

← Guías de Bots de Discord

Bots de Discord · actualizada el 22 de octubre de 2026

Bot de niveles para Discord: XP por mensajes, ranking y roles (Python y JavaScript)

Un bot de niveles da XP a quien escribe en tu servidor de Discord, avisa al subir de nivel y entrega roles a los más activos. Aquí está entero, unas 100 líneas, en discord.py y en discord.js: XP contra el spam, /nivel con barra de progreso, /ranking y roles por nivel, todo guardado en un fichero SQLite que no necesita instalar ninguna base de datos.

¿Se lo pasas a tu grupo?WhatsApp

Cómo funciona

Cada mensaje da entre 15 y 25 puntos de XP al azar, pero solo cuenta uno por minuto y por persona: escribir diez mensajes seguidos da lo mismo que uno, así que el spam no sube a nadie. Los bots no ganan XP y cada servidor de Discord lleva su propia cuenta.

Para pasar de un nivel al siguiente hacen falta 5·n² + 50·n + 100 puntos (100 para el nivel 1, 155 más para el 2, 220 para el 3…), la curva que popularizó MEE6. Con un mensaje por minuto, el nivel 5 llega tras una hora de conversación, el 10 tras unas 4 horas y el 20 tras unas 20: los primeros niveles salen pronto y los altos distinguen a los de siempre.

El bot no lee lo que escribe nadie: le basta con saber que alguien ha escrito. Por eso no hace falta activar el «Message Content Intent», que es un permiso especial.

¿Prefieres no montarlo tú? Hosting de bots de Discord desde 1,50 € al mes, sin permanencia y con devolución íntegra en 72 horas. O crea un bot de Discord gratis, sin tarjeta.

1. Crea el bot en Discord

  1. Crea la aplicación y saca el token

    En discord.com/developers/applications, «New Application»; en la pestaña «Bot», «Reset Token» y copia el token. Es la contraseña del bot: no lo compartas ni lo subas a GitHub. Si es tu primer bot, el proceso paso a paso está en tu primer bot en Python.

  2. Invítalo con los permisos justos

    En «OAuth2» → «URL Generator», marca «bot» y «applications.commands», y en los permisos «Send Messages», «Embed Links» y, si va a dar roles, «Manage Roles». Abre el enlace que sale y elige tu servidor.

  3. Si va a dar roles, súbelo de puesto

    En «Ajustes del servidor» → «Roles», arrastra el rol del bot por encima de los roles que va a dar. Un bot no puede dar un rol que esté por encima del suyo, aunque tenga «Manage Roles».

2. El código en Python (discord.py)

Tres ficheros en una carpeta. El token va en .env, sin comillas. La base de datos (niveles.db) la crea el bot solo la primera vez, en la misma carpeta; sqlite3 viene con Python, no hay que instalar nada más. Hace falta Python 3.10 o más nuevo.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=pega-aquí-tu-token
app.py
import os
import random
import sqlite3
import time

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()  # lee el token del fichero .env

# Roles que se ganan al llegar a un nivel: {nivel: id del rol}. Vacío, sin roles.
ROLES_POR_NIVEL = {
    # 5: 123456789012345678,
    # 10: 234567890123456789,
}
ESPERA = 60  # segundos: solo cuenta un mensaje por minuto (contra el spam)

bd = sqlite3.connect("niveles.db")
bd.execute(
    "CREATE TABLE IF NOT EXISTS xp (servidor INTEGER, usuario INTEGER, nombre TEXT,"
    " xp INTEGER NOT NULL, ultimo REAL NOT NULL, PRIMARY KEY (servidor, usuario))"
)


def xp_para_subir(nivel):
    """XP que hace falta para pasar de este nivel al siguiente."""
    return 5 * nivel**2 + 50 * nivel + 100


def nivel_de(xp):
    """El nivel, la XP que lleva dentro de él y la que pide para subir."""
    nivel = 0
    while xp >= xp_para_subir(nivel):
        xp -= xp_para_subir(nivel)
        nivel += 1
    return nivel, xp, xp_para_subir(nivel)


intents = discord.Intents.default()  # trae los mensajes, sin leer su texto
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)


@client.event
async def on_message(message):
    if message.author.bot or message.guild is None:
        return
    ahora = time.time()
    fila = bd.execute(
        "SELECT xp, ultimo FROM xp WHERE servidor = ? AND usuario = ?",
        (message.guild.id, message.author.id),
    ).fetchone()
    xp, ultimo = fila or (0, 0)
    if ahora - ultimo < ESPERA:
        return
    antes = nivel_de(xp)[0]
    xp += random.randint(15, 25)
    bd.execute(
        "INSERT OR REPLACE INTO xp VALUES (?, ?, ?, ?, ?)",
        (message.guild.id, message.author.id, message.author.display_name, xp, ahora),
    )
    bd.commit()
    nivel = nivel_de(xp)[0]
    if nivel == antes:
        return
    try:
        await message.channel.send(f"🎉 {message.author.mention} sube a nivel {nivel}")
        rol = message.guild.get_role(ROLES_POR_NIVEL.get(nivel, 0))
        if rol:
            await message.author.add_roles(rol, reason=f"Nivel {nivel}")
    except discord.HTTPException as error:
        print(f"No he podido avisar o dar el rol: {error}")


@tree.command(name="nivel", description="Tu nivel (o el de otro)")
@app_commands.guild_only()
async def ver_nivel(interaction: discord.Interaction, usuario: discord.Member | None = None):
    usuario = usuario or interaction.user
    fila = bd.execute(
        "SELECT xp FROM xp WHERE servidor = ? AND usuario = ?",
        (interaction.guild_id, usuario.id),
    ).fetchone()
    xp = fila[0] if fila else 0
    puesto = bd.execute(
        "SELECT COUNT(*) + 1 FROM xp WHERE servidor = ? AND xp > ?",
        (interaction.guild_id, xp),
    ).fetchone()[0]
    nivel, dentro, falta = nivel_de(xp)
    llenos = dentro * 10 // falta
    barra = "▰" * llenos + "▱" * (10 - llenos)
    tarjeta = discord.Embed(title=f"Nivel {nivel}", color=0x5B4AE8)
    tarjeta.set_author(name=usuario.display_name, icon_url=usuario.display_avatar.url)
    tarjeta.description = f"{barra} {dentro}/{falta} XP\nPuesto {puesto} · {xp} XP en total"
    await interaction.response.send_message(embed=tarjeta)


@tree.command(name="ranking", description="Los 10 con más nivel del servidor")
@app_commands.guild_only()
async def ranking(interaction: discord.Interaction):
    filas = bd.execute(
        "SELECT nombre, xp FROM xp WHERE servidor = ? ORDER BY xp DESC LIMIT 10",
        (interaction.guild_id,),
    ).fetchall()
    if not filas:
        await interaction.response.send_message("Aún nadie tiene XP: ¡a escribir!")
        return
    lineas = [f"**{i}.** {nombre} · nivel {nivel_de(xp)[0]} ({xp} XP)" for i, (nombre, xp) in enumerate(filas, 1)]
    tarjeta = discord.Embed(title="🏆 Ranking", description="\n".join(lineas), color=0x5B4AE8)
    await interaction.response.send_message(embed=tarjeta)


@client.event
async def on_ready():
    await tree.sync()  # publica /nivel y /ranking
    print(f"Conectado como {client.user}")


client.run(os.environ["DISCORD_TOKEN"])

3. El mismo bot en JavaScript (discord.js)

Usa node:sqlite, la base de datos que trae Node.js desde la versión 22.13: nada que compilar ni instalar. Al arrancar, Node.js escribe «ExperimentalWarning: SQLite is an experimental feature»; es solo un aviso de que esa parte aún puede cambiar, y el bot funciona igual.

package.json
{
  "name": "bot-niveles",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14.16.0",
    "dotenv": "^16.4.0"
  }
}
.env
DISCORD_TOKEN=pega-aquí-tu-token
index.js
require("dotenv").config(); // lee el token del fichero .env
const { DatabaseSync } = require("node:sqlite"); // viene con Node.js 22.13 o más nuevo
const { Client, EmbedBuilder, Events, GatewayIntentBits, InteractionContextType, SlashCommandBuilder } = require("discord.js");

// Roles que se ganan al llegar a un nivel: { nivel: "id del rol" }. Vacío, sin roles.
const ROLES_POR_NIVEL = {
  // 5: "123456789012345678",
  // 10: "234567890123456789",
};
const ESPERA = 60_000; // milisegundos: solo cuenta un mensaje por minuto (contra el spam)

const bd = new DatabaseSync("niveles.db");
bd.exec(
  "CREATE TABLE IF NOT EXISTS xp (servidor TEXT, usuario TEXT, nombre TEXT," +
    " xp INTEGER NOT NULL, ultimo INTEGER NOT NULL, PRIMARY KEY (servidor, usuario))",
);
const leer = bd.prepare("SELECT xp, ultimo FROM xp WHERE servidor = ? AND usuario = ?");
const guardar = bd.prepare("INSERT OR REPLACE INTO xp VALUES (?, ?, ?, ?, ?)");
const puestoDe = bd.prepare("SELECT COUNT(*) + 1 AS puesto FROM xp WHERE servidor = ? AND xp > ?");
const primeros = bd.prepare("SELECT nombre, xp FROM xp WHERE servidor = ? ORDER BY xp DESC LIMIT 10");

// XP que hace falta para pasar de este nivel al siguiente.
const xpParaSubir = (nivel) => 5 * nivel ** 2 + 50 * nivel + 100;

// El nivel, la XP que lleva dentro de él y la que pide para subir.
function nivelDe(xp) {
  let nivel = 0;
  while (xp >= xpParaSubir(nivel)) xp -= xpParaSubir(nivel++);
  return { nivel, dentro: xp, falta: xpParaSubir(nivel) };
}

// Los mensajes llegan sin leer su texto: no hace falta el «Message Content Intent».
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });

client.on(Events.MessageCreate, async (message) => {
  if (message.author.bot || !message.inGuild()) return;
  const ahora = Date.now();
  const fila = leer.get(message.guildId, message.author.id) ?? { xp: 0, ultimo: 0 };
  if (ahora - fila.ultimo < ESPERA) return;
  const antes = nivelDe(fila.xp).nivel;
  const xp = fila.xp + 15 + Math.floor(Math.random() * 11);
  guardar.run(message.guildId, message.author.id, message.member?.displayName ?? message.author.username, xp, ahora);
  const { nivel } = nivelDe(xp);
  if (nivel === antes) return;
  try {
    await message.channel.send(`🎉 ${message.author} sube a nivel ${nivel}`);
    const rol = ROLES_POR_NIVEL[nivel];
    if (rol && message.member) await message.member.roles.add(rol, `Nivel ${nivel}`);
  } catch (error) {
    console.log(`No he podido avisar o dar el rol: ${error.message}`);
  }
});

const comandos = [
  new SlashCommandBuilder()
    .setName("nivel")
    .setDescription("Tu nivel (o el de otro)")
    .setContexts(InteractionContextType.Guild)
    .addUserOption((o) => o.setName("usuario").setDescription("De quién")),
  new SlashCommandBuilder().setName("ranking").setDescription("Los 10 con más nivel del servidor").setContexts(InteractionContextType.Guild),
];

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand() || !i.inGuild()) return;
  if (i.commandName === "nivel") {
    const usuario = i.options.getMember("usuario") ?? i.member;
    const xp = leer.get(i.guildId, usuario.id)?.xp ?? 0;
    const { puesto } = puestoDe.get(i.guildId, xp);
    const { nivel, dentro, falta } = nivelDe(xp);
    const llenos = Math.floor((dentro * 10) / falta);
    const tarjeta = new EmbedBuilder()
      .setColor(0x5b4ae8)
      .setTitle(`Nivel ${nivel}`)
      .setAuthor({ name: usuario.displayName, iconURL: usuario.displayAvatarURL() })
      .setDescription(`${"▰".repeat(llenos)}${"▱".repeat(10 - llenos)} ${dentro}/${falta} XP\nPuesto ${puesto} · ${xp} XP en total`);
    await i.reply({ embeds: [tarjeta] });
  }
  if (i.commandName === "ranking") {
    const filas = primeros.all(i.guildId);
    if (!filas.length) return i.reply("Aún nadie tiene XP: ¡a escribir!");
    const lineas = filas.map((f, n) => `**${n + 1}.** ${f.nombre} · nivel ${nivelDe(f.xp).nivel} (${f.xp} XP)`);
    await i.reply({ embeds: [new EmbedBuilder().setColor(0x5b4ae8).setTitle("🏆 Ranking").setDescription(lineas.join("\n"))] });
  }
});

client.once(Events.ClientReady, async (c) => {
  await c.application.commands.set(comandos.map((x) => x.toJSON())); // publica /nivel y /ranking
  console.log(`Conectado como ${c.user.tag}`);
});

client.login(process.env.DISCORD_TOKEN);

4. Roles por nivel

Arriba del código está ROLES_POR_NIVEL, vacío. Para que al llegar al nivel 5 alguien reciba un rol, quita la almohadilla (o las dos barras) de esa línea y pon el id del rol. El id se copia con el modo desarrollador de Discord encendido («Ajustes de usuario» → «Avanzado» → «Modo desarrollador»): clic derecho sobre el rol en «Ajustes del servidor» → «Roles» → «Copiar ID del rol». En Python el id va sin comillas; en JavaScript, entre comillas.

Si el bot no da el rol, mira su consola: «Missing Permissions» (error 50013) es que le falta «Manage Roles» o que su rol está por debajo del que intenta dar. El aviso de subida sale igual; solo falla el rol.

5. Pruébalo y cámbialo a tu gusto

En tu ordenador: «pip install -r requirements.txt» y «python app.py» (o «npm install» y «node index.js»). Cuando diga «Conectado como…», escribe en tu servidor: el primer nivel llega a los 5 o 6 minutos de ir escribiendo. /nivel enseña tu tarjeta (o la de otro, eligiéndolo) y /ranking los diez primeros. Los comandos con barra pueden tardar unos minutos en aparecer la primera vez.

Para que se suba más rápido, baja ESPERA (los segundos entre mensajes que cuentan) o sube los puntos por mensaje; para que cueste más, al revés. Si cambias la curva cuando ya hay gente con XP, los niveles se recalculan solos con los puntos que tiene cada uno: nadie pierde lo ganado.

Todo está en niveles.db. Si lo borras, todo el mundo vuelve a cero. Si subes el bot desde GitHub, añade niveles.db y .env al .gitignore para no subir ni los datos ni el token.

6. Déjalo encendido siempre (gratis)

Un bot de niveles tiene una pega que no tienen otros: solo cuenta la XP de los mensajes que ve. Lo que se escribe mientras está apagado no suma, así que si vive en tu ordenador, cada noche que lo apagas es una noche que tu servidor no cuenta, y el ranking acaba premiando a quien escribe a tu misma hora.

En NapNodes: crea el bot gratis (nace en Node.js; si es el de Python, en «Configuración» → «Lenguaje», «Pasar a Python»), sube los tres ficheros en «Archivos» y pulsa «Encender». Las librerías de requirements.txt o package.json se instalan solas al arrancar, y app.py o index.js es el fichero de inicio que viene puesto. Con Node.js, deja la versión 24 o la 22 en «Configuración avanzada» → «Versión y arranque» (node:sqlite no está en la 20). niveles.db se queda en la carpeta del bot entre reinicios, y puedes descargarlo cuando quieras desde «Archivos» para tener tu copia.

El bot gratis tiene 256 MB, de sobra para un bot de niveles, y hay que renovarlo con un clic cada semana: si se te pasa, se apaga y deja de contar hasta que lo enciendes. Para no tener ese hueco, el plan Mini (512 MB) está encendido siempre sin renovar nada (precios de los bots). Todo el montaje, en la guía de hosting para tu bot.

Ideas para ampliarlo

XP por estar en un canal de voz: con el evento de cambio de estado de voz apuntas cuándo entra y sale cada uno y das puntos por minuto (hace falta el intent de estados de voz, que no es especial). Un /dar-xp solo para quien gestiona el servidor, para premiar eventos. Un ranking del mes, guardando también la XP de cada mes y poniéndola a cero el día 1. O canales que no cuentan (el de comandos, el de memes), comprobando el id del canal antes de sumar.

Preguntas frecuentes

¿Cómo hago un bot de niveles para Discord?

Con un evento que salta en cada mensaje: sumas XP a quien lo escribe (con una espera entre mensajes para que el spam no cuente), lo guardas en una base de datos como SQLite y avisas cuando cambia de nivel. El código completo, en Python y en JavaScript, está en esta guía.

¿Hace falta el Message Content Intent para un bot de niveles?

No. El bot recibe el aviso de que alguien ha escrito aunque no pueda leer el texto, y para dar XP no necesita leerlo.

¿Por qué mi bot de niveles no da los roles?

Casi siempre es la jerarquía: el rol del bot tiene que estar por encima del rol que da, y el bot necesita el permiso «Manage Roles». La consola lo dice con «Missing Permissions» (50013).

¿Cuánta XP hace falta para cada nivel?

Con la curva de esta guía, 5·n² + 50·n + 100 para pasar del nivel n al siguiente: 100 para el 1, 155 más para el 2, 220 más para el 3. Al nivel 10 se llega con 4675 puntos en total.

¿Cuenta la XP si el bot está apagado?

No: solo cuenta los mensajes que ve mientras está encendido. Por eso un bot de niveles conviene tenerlo alojado en un servidor y no en tu ordenador.

¿Te ha servido esta guía?

¿Lo montamos?

Hosting de bots de Discord desde 1,50 € al mes, sin sobreventa: tu RAM es solo tuya. Y si quieres empezar sin pagar, tienes un bot de Discord gratis, sin tarjeta.

Sigue leyendo

Todas las guías de Bots de Discord, ordenadas →