NapNodes

← Guías de Bots de Discord

Bots de Discord · actualizada el 31 de octubre de 2026

Bot de economía para Discord: monedas, /diario, tienda y ranking (Python y JavaScript)

Un bot de economía da monedas a tu comunidad de Discord: se ganan con /diario y /trabajar, se pasan con /pagar y se gastan en una tienda de roles. Aquí lo tienes entero en discord.py y en discord.js, guardado en un fichero SQLite, con esperas que no se pierden al reiniciar, un ranking y lo que casi todos los bots caseros hacen mal: que nadie pueda gastar dos veces las mismas monedas.

¿Se lo pasas a tu grupo?WhatsApp

Cómo funciona

Cada miembro de tu servidor tiene un saldo de monedas del propio servidor: las gana con /diario (una vez al día) y /trabajar (una vez por hora), se las pasa a otros con /pagar y las gasta con /comprar en lo que enseña /tienda, donde cada artículo es un rol. /saldo dice cuántas tiene cada uno, /ranking enseña a los más ricos y /dar deja a los administradores dar o quitar monedas. Tú pones los precios, los premios y lo que se vende.

Cada servidor de Discord tiene su propia economía (la clave es servidor y usuario) y todo se guarda en un fichero SQLite, economia.db, que sobrevive a los reinicios sin instalar ninguna base de datos. Por qué SQLite y no un JSON, en la guía de bases de datos para bots. Solo usa comandos de barra, así que no necesita ningún intent privilegiado (ni «Message Content» ni «Server Members»).

Si es tu primer bot, empieza por tu primer bot en Python o en JavaScript: ahí está cómo crearlo en el portal de desarrolladores, sacar el token e invitarlo. Invítalo con los ámbitos bot y applications.commands y, para la tienda, con el permiso «Gestionar roles» (el enlace, ya hecho, en la calculadora de permisos de bot).

¿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.

El bot en Python (discord.py)

Tres ficheros en una carpeta. El token va en .env, sin comillas. Hace falta Python 3.10 o más nuevo; sqlite3 viene con Python, no hay que instalar nada más. Cambia los ID de TIENDA por los de tus roles: con el «Modo desarrollador» de Discord activado («Ajustes de usuario» → «Avanzado»), clic derecho en el rol y «Copiar ID del rol».

Cada comando abre la base de datos con with db:: lo que hay dentro se guarda junto al salir y, si algo falla a medias, no se guarda nada. cobrar resta solo si el saldo llega (en la sección de gastar dos veces, por qué así).

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

import discord
from discord import app_commands
from dotenv import load_dotenv

load_dotenv()  # lee el token del fichero .env

MONEDA = "🪙"
DIARIO = 200  # lo que da /diario, una vez cada 24 horas
TRABAJO = (50, 150)  # lo que da /trabajar: entre estas dos cantidades
ESPERA_TRABAJO = 3600  # segundos entre un /trabajar y el siguiente
# Lo que se compra con /comprar: cada artículo da un rol (pon los ID de tus roles)
TIENDA = {
    "vip": {"nombre": "Rol VIP", "precio": 5000, "rol": 123456789012345678},
    "color": {"nombre": "Nombre de color", "precio": 1500, "rol": 234567890123456789},
}

# El fichero, junto a app.py: así da igual desde qué carpeta se arranque
db = sqlite3.connect(Path(__file__).with_name("economia.db"))
db.execute(
    "CREATE TABLE IF NOT EXISTS cuentas (servidor INTEGER, usuario INTEGER,"
    " saldo INTEGER NOT NULL DEFAULT 0, diario REAL NOT NULL DEFAULT 0,"
    " trabajo REAL NOT NULL DEFAULT 0, PRIMARY KEY (servidor, usuario))"
)
db.commit()

client = discord.Client(intents=discord.Intents.default())  # sin intents privilegiados
tree = app_commands.CommandTree(client)


def cuenta(servidor, usuario):
    """La fila de esa persona en ese servidor; si no existe, la crea a cero."""
    db.execute("INSERT OR IGNORE INTO cuentas (servidor, usuario) VALUES (?, ?)", (servidor, usuario))
    return db.execute(
        "SELECT saldo, diario, trabajo FROM cuentas WHERE servidor = ? AND usuario = ?",
        (servidor, usuario),
    ).fetchone()


def cobrar(servidor, usuario, cantidad):
    """Resta solo si llega el saldo, en una sola orden: dos comandos a la vez no gastan lo mismo."""
    cursor = db.execute(
        "UPDATE cuentas SET saldo = saldo - ? WHERE servidor = ? AND usuario = ? AND saldo >= ?",
        (cantidad, servidor, usuario, cantidad),
    )
    return cursor.rowcount == 1


def ingresar(servidor, usuario, cantidad):
    db.execute(
        "UPDATE cuentas SET saldo = saldo + ? WHERE servidor = ? AND usuario = ?",
        (cantidad, servidor, usuario),
    )


@tree.command(name="saldo", description="Tus monedas (o las de otro)")
@app_commands.guild_only()
async def saldo(interaction: discord.Interaction, usuario: discord.Member | None = None):
    usuario = usuario or interaction.user
    with db:  # guarda al salir del bloque; si algo falla, no guarda nada a medias
        monedas = cuenta(interaction.guild_id, usuario.id)[0]
    await interaction.response.send_message(f"{usuario.display_name} tiene {monedas} {MONEDA}")


@tree.command(name="diario", description="Recoge tus monedas del día")
@app_commands.guild_only()
async def diario(interaction: discord.Interaction):
    ahora = time.time()
    with db:
        ultimo = cuenta(interaction.guild_id, interaction.user.id)[1]
        # suma y apunta la hora solo si han pasado 24 horas: la espera sobrevive a los reinicios
        cobrado = db.execute(
            "UPDATE cuentas SET saldo = saldo + ?, diario = ? WHERE servidor = ? AND usuario = ? AND diario <= ?",
            (DIARIO, ahora, interaction.guild_id, interaction.user.id, ahora - 86400),
        ).rowcount
    if not cobrado:
        # <t:…:R> lo pinta Discord como «dentro de 3 horas», en el idioma de cada uno
        await interaction.response.send_message(f"Ya lo has recogido. Vuelve <t:{int(ultimo + 86400)}:R>", ephemeral=True)
        return
    await interaction.response.send_message(f"+{DIARIO} {MONEDA} del día")


@tree.command(name="trabajar", description="Gana unas monedas (una vez por hora)")
@app_commands.guild_only()
async def trabajar(interaction: discord.Interaction):
    ahora = time.time()
    paga = random.randint(*TRABAJO)
    with db:
        ultimo = cuenta(interaction.guild_id, interaction.user.id)[2]
        cobrado = db.execute(
            "UPDATE cuentas SET saldo = saldo + ?, trabajo = ? WHERE servidor = ? AND usuario = ? AND trabajo <= ?",
            (paga, ahora, interaction.guild_id, interaction.user.id, ahora - ESPERA_TRABAJO),
        ).rowcount
    if not cobrado:
        await interaction.response.send_message(f"Estás descansando. Vuelve <t:{int(ultimo + ESPERA_TRABAJO)}:R>", ephemeral=True)
        return
    await interaction.response.send_message(f"Has trabajado y ganas {paga} {MONEDA}")


@tree.command(name="pagar", description="Da monedas a otra persona")
@app_commands.guild_only()
async def pagar(interaction: discord.Interaction, usuario: discord.Member, cantidad: app_commands.Range[int, 1, 1_000_000]):
    if usuario.bot or usuario.id == interaction.user.id:
        await interaction.response.send_message("A ti mismo o a un bot, no.", ephemeral=True)
        return
    with db:  # las dos cosas o ninguna: si algo falla, nadie pierde monedas
        cuenta(interaction.guild_id, usuario.id)
        tenia = cuenta(interaction.guild_id, interaction.user.id)[0]
        pagado = cobrar(interaction.guild_id, interaction.user.id, cantidad)
        if pagado:
            ingresar(interaction.guild_id, usuario.id, cantidad)
    if not pagado:
        await interaction.response.send_message(f"No te llega: tienes {tenia} {MONEDA}", ephemeral=True)
        return
    await interaction.response.send_message(f"{interaction.user.mention} le da {cantidad} {MONEDA} a {usuario.mention}")


@tree.command(name="tienda", description="Lo que se puede comprar")
async def tienda(interaction: discord.Interaction):
    lineas = [f"**{a['nombre']}** · {a['precio']} {MONEDA}" for a in TIENDA.values()]
    tarjeta = discord.Embed(title="🛒 Tienda", description="\n".join(lineas) + "\n\nCompra con /comprar", color=0x5B4AE8)
    await interaction.response.send_message(embed=tarjeta)


@tree.command(name="comprar", description="Compra algo de la tienda")
@app_commands.guild_only()
@app_commands.choices(articulo=[app_commands.Choice(name=a["nombre"], value=clave) for clave, a in TIENDA.items()])
async def comprar(interaction: discord.Interaction, articulo: app_commands.Choice[str]):
    a = TIENDA[articulo.value]
    rol = interaction.guild.get_role(a["rol"])
    if rol is None:
        await interaction.response.send_message("Ese rol no existe: revisa TIENDA en el código.", ephemeral=True)
        return
    if rol in interaction.user.roles:
        await interaction.response.send_message("Ya lo tienes.", ephemeral=True)
        return
    with db:
        cuenta(interaction.guild_id, interaction.user.id)
        pagado = cobrar(interaction.guild_id, interaction.user.id, a["precio"])
    if not pagado:
        await interaction.response.send_message(f"No te llega: cuesta {a['precio']} {MONEDA}", ephemeral=True)
        return
    try:
        await interaction.user.add_roles(rol, reason="Comprado en la tienda")
    except discord.HTTPException:
        with db:  # no se ha podido dar el rol: se devuelven las monedas
            ingresar(interaction.guild_id, interaction.user.id, a["precio"])
        await interaction.response.send_message(
            "No he podido darte el rol (¿tengo «Gestionar roles» y mi rol está por encima?). Te devuelvo las monedas.",
            ephemeral=True,
        )
        return
    await interaction.response.send_message(f"Comprado: {a['nombre']} ✅")


@tree.command(name="ranking", description="Los 10 más ricos del servidor")
@app_commands.guild_only()
async def ranking(interaction: discord.Interaction):
    filas = db.execute(
        "SELECT usuario, saldo FROM cuentas WHERE servidor = ? AND saldo > 0 ORDER BY saldo DESC LIMIT 10",
        (interaction.guild_id,),
    ).fetchall()
    if not filas:
        await interaction.response.send_message("Aún nadie tiene monedas: prueba /diario")
        return
    # una mención dentro de un embed se ve con el nombre y no avisa a nadie
    lineas = [f"**{n}.** <@{usuario}> · {monedas} {MONEDA}" for n, (usuario, monedas) in enumerate(filas, 1)]
    tarjeta = discord.Embed(title="🏆 Los más ricos", description="\n".join(lineas), color=0x5B4AE8)
    await interaction.response.send_message(embed=tarjeta)


@tree.command(name="dar", description="(Admins) Da o quita monedas")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)  # de serie, solo quien puede gestionar el servidor
async def dar(interaction: discord.Interaction, usuario: discord.Member, cantidad: int):
    with db:
        cuenta(interaction.guild_id, usuario.id)
        # en negativo quita, sin bajar de cero
        db.execute(
            "UPDATE cuentas SET saldo = MAX(0, saldo + ?) WHERE servidor = ? AND usuario = ?",
            (cantidad, interaction.guild_id, usuario.id),
        )
    await interaction.response.send_message(f"Hecho: {cantidad:+} {MONEDA} a {usuario.display_name}", ephemeral=True)


@client.event
async def on_ready():
    await tree.sync()  # publica los comandos
    print(f"Conectado como {client.user}")


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

El mismo bot en JavaScript (discord.js)

package.json, .env e index.js. La base de datos es node:sqlite, que viene con Node.js 22.13 o más nuevo: no hay que instalar nada para ella (al arrancar, Node puede avisar de que es experimental; es normal). Los ID de servidores y usuarios se guardan como texto, porque los de Discord no caben en un número de JavaScript.

Las órdenes de SQL se preparan una vez al principio (db.prepare) y run devuelve en changes cuántas filas ha cambiado: si es 0, no había saldo o aún no había pasado la espera.

package.json
{
  "name": "bot-economia",
  "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 path = require("node:path");
const { DatabaseSync } = require("node:sqlite"); // viene con Node.js 22.13 o más nuevo
const { Client, EmbedBuilder, Events, GatewayIntentBits, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");

const MONEDA = "🪙";
const DIARIO = 200; // lo que da /diario, una vez cada 24 horas
const TRABAJO = [50, 150]; // lo que da /trabajar: entre estas dos cantidades
const ESPERA_TRABAJO = 3600; // segundos entre un /trabajar y el siguiente
// Lo que se compra con /comprar: cada artículo da un rol (pon los ID de tus roles)
const TIENDA = {
  vip: { nombre: "Rol VIP", precio: 5000, rol: "123456789012345678" },
  color: { nombre: "Nombre de color", precio: 1500, rol: "234567890123456789" },
};

// El fichero, junto a index.js: así da igual desde qué carpeta se arranque
const db = new DatabaseSync(path.join(__dirname, "economia.db"));
db.exec(
  "CREATE TABLE IF NOT EXISTS cuentas (servidor TEXT, usuario TEXT," +
    " saldo INTEGER NOT NULL DEFAULT 0, diario INTEGER NOT NULL DEFAULT 0," +
    " trabajo INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (servidor, usuario))",
);
const crear = db.prepare("INSERT OR IGNORE INTO cuentas (servidor, usuario) VALUES (?, ?)");
const leer = db.prepare("SELECT saldo, diario, trabajo FROM cuentas WHERE servidor = ? AND usuario = ?");
// Resta solo si llega el saldo, en una sola orden: dos comandos a la vez no gastan lo mismo
const cobrar = db.prepare("UPDATE cuentas SET saldo = saldo - ? WHERE servidor = ? AND usuario = ? AND saldo >= ?");
const ingresar = db.prepare("UPDATE cuentas SET saldo = saldo + ? WHERE servidor = ? AND usuario = ?");
// Suman y apuntan la hora solo si ha pasado la espera: sobrevive a los reinicios
const cobrarDiario = db.prepare("UPDATE cuentas SET saldo = saldo + ?, diario = ? WHERE servidor = ? AND usuario = ? AND diario <= ?");
const cobrarTrabajo = db.prepare("UPDATE cuentas SET saldo = saldo + ?, trabajo = ? WHERE servidor = ? AND usuario = ? AND trabajo <= ?");
const ajustar = db.prepare("UPDATE cuentas SET saldo = MAX(0, saldo + ?) WHERE servidor = ? AND usuario = ?");
const primeros = db.prepare("SELECT usuario, saldo FROM cuentas WHERE servidor = ? AND saldo > 0 ORDER BY saldo DESC LIMIT 10");

// La fila de esa persona en ese servidor; si no existe, la crea a cero.
// Los ID van como texto: los de Discord no caben en un número de JavaScript.
function cuenta(servidor, usuario) {
  crear.run(servidor, usuario);
  return leer.get(servidor, usuario);
}

const client = new Client({ intents: [GatewayIntentBits.Guilds] }); // sin intents privilegiados

const comando = (nombre, texto) => new SlashCommandBuilder().setName(nombre).setDescription(texto).setContexts(InteractionContextType.Guild);
const comandos = [
  comando("saldo", "Tus monedas (o las de otro)").addUserOption((o) => o.setName("usuario").setDescription("De quién")),
  comando("diario", "Recoge tus monedas del día"),
  comando("trabajar", "Gana unas monedas (una vez por hora)"),
  comando("pagar", "Da monedas a otra persona")
    .addUserOption((o) => o.setName("usuario").setDescription("A quién").setRequired(true))
    .addIntegerOption((o) => o.setName("cantidad").setDescription("Cuántas").setRequired(true).setMinValue(1).setMaxValue(1_000_000)),
  comando("tienda", "Lo que se puede comprar"),
  comando("comprar", "Compra algo de la tienda").addStringOption((o) =>
    o
      .setName("articulo")
      .setDescription("Qué")
      .setRequired(true)
      .addChoices(...Object.entries(TIENDA).map(([value, a]) => ({ name: a.nombre, value }))),
  ),
  comando("ranking", "Los 10 más ricos del servidor"),
  comando("dar", "(Admins) Da o quita monedas")
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) // de serie, solo quien puede gestionar el servidor
    .addUserOption((o) => o.setName("usuario").setDescription("A quién").setRequired(true))
    .addIntegerOption((o) => o.setName("cantidad").setDescription("En negativo, quita").setRequired(true)),
];

const soloTu = (content) => ({ content, flags: MessageFlags.Ephemeral });

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand() || !i.inCachedGuild()) return;
  const g = i.guildId;
  const ahora = Math.floor(Date.now() / 1000); // en segundos, como <t:…:R>

  if (i.commandName === "saldo") {
    const usuario = i.options.getUser("usuario") ?? i.user;
    await i.reply(`${usuario.displayName} tiene ${cuenta(g, usuario.id).saldo} ${MONEDA}`);
  } else if (i.commandName === "diario") {
    const { diario } = cuenta(g, i.user.id);
    if (cobrarDiario.run(DIARIO, ahora, g, i.user.id, ahora - 86400).changes === 0) {
      // <t:…:R> lo pinta Discord como «dentro de 3 horas», en el idioma de cada uno
      return i.reply(soloTu(`Ya lo has recogido. Vuelve <t:${diario + 86400}:R>`));
    }
    await i.reply(`+${DIARIO} ${MONEDA} del día`);
  } else if (i.commandName === "trabajar") {
    const { trabajo } = cuenta(g, i.user.id);
    const paga = TRABAJO[0] + Math.floor(Math.random() * (TRABAJO[1] - TRABAJO[0] + 1));
    if (cobrarTrabajo.run(paga, ahora, g, i.user.id, ahora - ESPERA_TRABAJO).changes === 0) {
      return i.reply(soloTu(`Estás descansando. Vuelve <t:${trabajo + ESPERA_TRABAJO}:R>`));
    }
    await i.reply(`Has trabajado y ganas ${paga} ${MONEDA}`);
  } else if (i.commandName === "pagar") {
    const destino = i.options.getUser("usuario", true);
    const cantidad = i.options.getInteger("cantidad", true);
    if (destino.bot || destino.id === i.user.id) return i.reply(soloTu("A ti mismo o a un bot, no."));
    cuenta(g, destino.id);
    const { saldo } = cuenta(g, i.user.id);
    // sin await entre cobrar e ingresar: nada puede colarse en medio
    if (cobrar.run(cantidad, g, i.user.id, cantidad).changes === 0) return i.reply(soloTu(`No te llega: tienes ${saldo} ${MONEDA}`));
    ingresar.run(cantidad, g, destino.id);
    await i.reply(`${i.user} le da ${cantidad} ${MONEDA} a ${destino}`);
  } else if (i.commandName === "tienda") {
    const lineas = Object.values(TIENDA).map((a) => `**${a.nombre}** · ${a.precio} ${MONEDA}`);
    const tarjeta = new EmbedBuilder().setColor(0x5b4ae8).setTitle("🛒 Tienda").setDescription(`${lineas.join("\n")}\n\nCompra con /comprar`);
    await i.reply({ embeds: [tarjeta] });
  } else if (i.commandName === "comprar") {
    const a = TIENDA[i.options.getString("articulo", true)];
    const rol = i.guild.roles.cache.get(a.rol);
    if (!rol) return i.reply(soloTu("Ese rol no existe: revisa TIENDA en el código."));
    if (i.member.roles.cache.has(rol.id)) return i.reply(soloTu("Ya lo tienes."));
    cuenta(g, i.user.id);
    if (cobrar.run(a.precio, g, i.user.id, a.precio).changes === 0) return i.reply(soloTu(`No te llega: cuesta ${a.precio} ${MONEDA}`));
    try {
      await i.member.roles.add(rol, "Comprado en la tienda");
    } catch {
      ingresar.run(a.precio, g, i.user.id); // no se ha podido dar el rol: se devuelven las monedas
      return i.reply(soloTu("No he podido darte el rol (¿tengo «Gestionar roles» y mi rol está por encima?). Te devuelvo las monedas."));
    }
    await i.reply(`Comprado: ${a.nombre} ✅`);
  } else if (i.commandName === "ranking") {
    const filas = primeros.all(g);
    if (!filas.length) return i.reply("Aún nadie tiene monedas: prueba /diario");
    // una mención dentro de un embed se ve con el nombre y no avisa a nadie
    const lineas = filas.map((f, n) => `**${n + 1}.** <@${f.usuario}> · ${f.saldo} ${MONEDA}`);
    await i.reply({ embeds: [new EmbedBuilder().setColor(0x5b4ae8).setTitle("🏆 Los más ricos").setDescription(lineas.join("\n"))] });
  } else if (i.commandName === "dar") {
    const destino = i.options.getUser("usuario", true);
    const cantidad = i.options.getInteger("cantidad", true);
    cuenta(g, destino.id);
    ajustar.run(cantidad, g, destino.id); // en negativo quita, sin bajar de cero
    await i.reply(soloTu(`Hecho: ${cantidad > 0 ? "+" : ""}${cantidad} ${MONEDA} a ${destino.displayName}`));
  }
});

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

client.login(process.env.DISCORD_TOKEN);

Que nadie gaste dos veces las mismas monedas

El fallo de casi todos los bots de economía caseros: leer el saldo, comprobar que llega y restar después, en tres pasos con un await en medio. Si alguien lanza /pagar dos veces muy seguidas (o /pagar y /comprar a la vez), los dos comandos leen el mismo saldo antes de que el otro reste, y se gasta dos veces lo que solo había una. Así salen saldos negativos y monedas de la nada.

La solución es comprobar y restar en una sola orden de SQL: UPDATE cuentas SET saldo = saldo - ? WHERE servidor = ? AND usuario = ? AND saldo >= ?. Si no llega, no cambia ninguna fila, y el bot lo sabe por rowcount en Python o changes en node:sqlite. Es lo que hace cobrar en los dos ejemplos. Con /diario y /trabajar, igual: la espera va en el WHERE, así que lanzarlo dos veces a la vez solo paga una.

Y cuando hay que llamar a Discord entre cobrar y entregar (dar un rol puede fallar), cobra primero y devuelve si falla, como /comprar. Al revés, dar el rol y cobrar después, alguien podría quedarse con el rol sin pagar.

Esperas que no se pierden al reiniciar

Muchos bots guardan la espera de /diario en memoria (un diccionario, un Map o el cooldown de la librería): al reiniciar el bot, todo el mundo puede volver a cobrar. Aquí la hora del último cobro va en la base de datos (las columnas diario y trabajo, en segundos), así que da igual cuántas veces lo reinicies.

Para decir cuándo se puede volver, el bot escribe la hora así: <t:1793426844:R>. Discord lo enseña como «dentro de 5 horas», en el idioma y la zona horaria de cada uno, y lo va actualizando solo. Con :F en vez de :R sale la fecha y la hora completas.

La tienda: vender roles

Cada artículo de TIENDA da un rol. El bot necesita el permiso «Gestionar roles» y que su propio rol esté por encima de los que vende, en «Ajustes del servidor» → «Roles»: Discord no deja dar un rol más alto que el tuyo. Si falta una de las dos cosas, /comprar devuelve las monedas y lo dice.

Las opciones de /comprar salen de TIENDA como una lista para elegir, y Discord admite hasta 25. Al cambiar la tienda, reinicia el bot para que vuelva a publicar los comandos; si el cambio no sale en Discord, por qué los comandos de barra no aparecen.

Un rol comprado puede ser solo de adorno (un color, un nombre) o abrir un canal privado: dale a ese rol permiso para ver el canal y quítaselo a @everyone. Para roles que cada uno se pone y se quita gratis con botones, mejor el bot de roles.

Que la economía no se rompa

Si solo entran monedas y nada las saca, en un mes todo el mundo es rico y la tienda deja de importar. Haz la cuenta: con /diario de 200 y /trabajar de 50 a 150 cada hora, alguien muy activo saca más de 1000 al día. Pon los precios a esa escala y dales en qué gastar: roles caros, artículos nuevos de vez en cuando, sorteos con entrada.

/dar sale de serie solo a quien tiene «Gestionar servidor» (es el default_member_permissions del comando), pero los administradores pueden cambiar quién lo ve en «Ajustes del servidor» → «Integraciones» → tu bot. Úsalo para premios de eventos, no para arreglar saldos a mano cada día.

Para ganar monedas escribiendo, junta este bot con el bot de niveles: suma monedas donde suma XP. Y que las monedas sean solo de juego: no las vendas por dinero real ni montes apuestas con nada que se compre.

Tenerlo encendido siempre

Un bot de economía apagado es un /diario que falla: los comandos de barra solo contestan con el bot encendido, y en tu ordenador se apaga contigo. Opciones gratis y de pago, comparadas, en dónde alojar tu bot de Discord.

En NapNodes hay un bot gratis de 256 MB que no se apaga por estar vacío; se renueva cada 7 días con «Encender» (sus pegas, en esta guía). Créalo, elige Node.js o Python en «Configuración» → «Lenguaje» y sube tus ficheros en «Archivos» (o pon tu repositorio en «Configuración» → «Repositorio de GitHub», con economia.db en el .gitignore); las librerías de requirements.txt o package.json se instalan solas al arrancar. Con Node.js, deja la versión 24 o la 22 en «Versión y arranque» (node:sqlite no está en la 20). economia.db se queda en la carpeta del bot entre reinicios, y puedes descargarlo desde «Archivos» para tener tu copia.

Preguntas frecuentes

¿Cómo se hace un comando diario en un bot de Discord?

Guardando en la base de datos la hora del último cobro y sumando solo si han pasado 24 horas, en una sola orden (… WHERE diario <= ahora - 86400). Así la espera no se pierde al reiniciar el bot y lanzarlo dos veces a la vez solo paga una.

¿Por qué mi bot de economía da saldos negativos?

Porque lee el saldo, comprueba y resta en pasos separados: dos comandos a la vez leen el mismo saldo y gastan dos veces. Comprueba y resta en una sola orden, UPDATE … SET saldo = saldo - ? WHERE … AND saldo >= ?, y mira si ha cambiado alguna fila.

¿Un bot de economía necesita intents privilegiados?

No, si solo usa comandos de barra, como este. Solo harían falta para dar monedas por escribir (Message Content, si lee el texto) o al entrar alguien (Server Members).

¿Dónde se guardan las monedas del bot?

En economia.db, un fichero SQLite junto al código. Sobrevive a los reinicios; para no perderlo, descárgalo de vez en cuando y no lo subas a GitHub.

¿Cuántos artículos caben en la tienda?

Con la lista para elegir de /comprar, hasta 25, que es lo que admite Discord en una opción. Para más, cambia la opción por un texto libre o pon la tienda en varias categorías.

¿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.

Herramienta gratis: Permisos de un bot de Discord. Marca los permisos de tu bot en castellano y sal con el número y el enlace de invitación; o pega un número y mira qué permisos da.

Sigue leyendo

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