NapNodes

← Guías de Bots de Discord

Bots de Discord · actualizada el 31 de octubre de 2026

Bot contador de miembros para Discord: canales con miembros, conectados y mejoras (Python y JavaScript)

Los canales de estadísticas («👥 Miembros: 1.234») son lo primero que se ve al entrar en un servidor de Discord, y los bots que los ponen suelen llevar su marca o cobrar por más de un contador. Este lo haces tú en discord.py o en discord.js: un /contador crea la categoría con tres canales (miembros, conectados y mejoras) y el bot los pone al día cada 10 minutos, que es lo que deja Discord. Sin base de datos y sin intents privilegiados.

¿Se lo pasas a tu grupo?WhatsApp

Qué hace el bot

Quien puede gestionar canales escribe /contador una vez. El bot crea arriba del todo una categoría «📊 Estadísticas» con tres canales de voz: «👥 Miembros», «🟢 Conectados» y «🚀 Mejoras», cada uno con su cifra. Todo el mundo los ve, pero nadie puede entrar: son solo un cartel. Se usan canales de voz porque su nombre admite espacios, mayúsculas y emojis, y se ven siempre en la lista sin que nadie tenga que abrirlos.

Cada 10 minutos el bot le pide a Discord las cifras del servidor y cambia el nombre de los canales que hayan cambiado. Los canales que crea se apuntan en contadores.json, así que un reinicio no los pierde, y un mismo bot puede llevar los contadores de varios servidores a la vez.

No necesita ningún intent privilegiado (ni «Server Members» ni «Presence»): no cuenta a nadie a mano, sino que usa los recuentos que Discord ya calcula para cada servidor, los mismos que enseña una invitación («1.234 miembros · 210 en línea»).

¿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 e invítalo

  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 con calma está en tu primer bot en Python o en JavaScript. Los «Privileged Gateway Intents» se quedan apagados.

  2. Invítalo con tres permisos

    En «OAuth2» → «URL Generator», marca «bot» y «applications.commands», y en los permisos «View Channels», «Manage Channels» y «Connect» (en total, el número 1049616). «Connect» hace falta aunque el bot no entre nunca: para quitarle a @everyone el permiso de entrar en un canal, Discord exige que el bot lo tenga. Con la calculadora de permisos de bot los ves en castellano y te da el enlace.

2. 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. Para cambiar lo que pone cada canal, toca TEXTOS: {n} es donde va la cifra.

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

import discord
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv

load_dotenv()  # lee el token del fichero .env

# Los canales de cada servidor se guardan junto al bot: un reinicio no los pierde.
CONTADORES = Path("contadores.json")
# Lo que pone cada canal. {n} es la cifra.
TEXTOS = {
    "miembros": "👥 Miembros: {n}",
    "conectados": "🟢 Conectados: {n}",
    "mejoras": "🚀 Mejoras: {n}",
}


def leer() -> dict:
    return json.loads(CONTADORES.read_text("utf-8")) if CONTADORES.exists() else {}


def guardar(datos: dict) -> None:
    CONTADORES.write_text(json.dumps(datos, indent=2), "utf-8")


def cifra(n: int) -> str:
    return f"{n:,}".replace(",", ".")  # 1.234, con el punto de los miles


class Bot(discord.Client):
    def __init__(self):
        # Sin intents privilegiados: las cifras se piden a Discord, no se cuentan a mano.
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        await self.tree.sync()  # publica /contador
        actualizar.start()


bot = Bot()


async def cifras(guild_id: int) -> dict[str, int]:
    # with_counts=True trae los recuentos aproximados de Discord: miembros y conectados.
    guild = await bot.fetch_guild(guild_id, with_counts=True)
    return {
        "miembros": guild.approximate_member_count or 0,
        "conectados": guild.approximate_presence_count or 0,
        "mejoras": guild.premium_subscription_count or 0,
    }


@tasks.loop(minutes=10)  # no menos: Discord solo deja renombrar un canal 2 veces cada 10 minutos
async def actualizar():
    for guild_id, canales in leer().items():
        try:
            cuentas = await cifras(int(guild_id))
            for clave, id_canal in canales.items():
                canal = bot.get_channel(id_canal)
                if canal is None:
                    continue  # han borrado el canal: no se hace nada
                nombre = TEXTOS[clave].format(n=cifra(cuentas[clave]))
                if canal.name != nombre:  # solo si ha cambiado: cada cambio gasta uno de los 2
                    await canal.edit(name=nombre, reason="Contador de miembros")
        except discord.HTTPException as error:
            print(f"No he podido actualizar el servidor {guild_id}: {error!r}")


@actualizar.before_loop
async def antes_de_actualizar():
    await bot.wait_until_ready()


@bot.tree.command(name="contador", description="Crea los canales con los miembros, los conectados y las mejoras")
@app_commands.guild_only()
@app_commands.default_permissions(manage_channels=True)
async def contador(interaction: discord.Interaction):
    guild = interaction.guild
    datos = leer()
    if any(guild.get_channel(c) for c in datos.get(str(guild.id), {}).values()):
        await interaction.response.send_message("Ya están creados. Para empezar de cero, borra la categoría y vuelve a escribir /contador.", ephemeral=True)
        return
    await interaction.response.defer(ephemeral=True)  # crear canales puede tardar unos segundos
    # Todos ven los canales, pero nadie puede entrar en ellos: son solo un cartel.
    permisos = {
        guild.default_role: discord.PermissionOverwrite(view_channel=True, connect=False),
        guild.me: discord.PermissionOverwrite(view_channel=True, manage_channels=True),
    }
    try:
        categoria = await guild.create_category("📊 Estadísticas", overwrites=permisos, position=0)
        cuentas = await cifras(guild.id)
        canales = {}
        for clave, texto in TEXTOS.items():
            canal = await guild.create_voice_channel(texto.format(n=cifra(cuentas[clave])), category=categoria, overwrites=permisos)
            canales[clave] = canal.id
    except discord.Forbidden:
        await interaction.followup.send("Me faltan permisos: necesito «Ver canales», «Gestionar canales» y «Conectarse».", ephemeral=True)
        return
    datos[str(guild.id)] = canales
    guardar(datos)
    await interaction.followup.send("Hecho: arriba del todo tienes la categoría «📊 Estadísticas». Las cifras se ponen al día cada 10 minutos.", ephemeral=True)


@bot.event
async def on_ready():
    print(f"Conectado como {bot.user}")


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

El mismo bot en JavaScript (discord.js)

package.json, .env e index.js. Hace falta Node.js 20 o más nuevo. Los números se escriben con toLocaleString("es-ES", { useGrouping: "always" }): sin useGrouping, el castellano no pone el punto de los miles hasta las cinco cifras (1234, pero 12.345).

package.json
{
  "name": "bot-contador",
  "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 fs = require("node:fs");
const path = require("node:path");
const { ChannelType, Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");

// Los canales de cada servidor se guardan junto al bot: un reinicio no los pierde.
const CONTADORES = path.join(__dirname, "contadores.json");
// Lo que pone cada canal. {n} es la cifra.
const TEXTOS = {
  miembros: "👥 Miembros: {n}",
  conectados: "🟢 Conectados: {n}",
  mejoras: "🚀 Mejoras: {n}",
};
const DIEZ_MINUTOS = 10 * 60 * 1000; // no menos: Discord solo deja renombrar un canal 2 veces cada 10 minutos

const leer = () => (fs.existsSync(CONTADORES) ? JSON.parse(fs.readFileSync(CONTADORES, "utf8")) : {});
const guardar = (datos) => fs.writeFileSync(CONTADORES, JSON.stringify(datos, null, 2));
const cifra = (n) => n.toLocaleString("es-ES", { useGrouping: "always" }); // 1.234, con el punto de los miles
const nombre = (clave, n) => TEXTOS[clave].replace("{n}", cifra(n));

// Sin intents privilegiados: las cifras se piden a Discord, no se cuentan a mano.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

async function cifras(guildId) {
  // withCounts trae los recuentos aproximados de Discord: miembros y conectados.
  const guild = await client.guilds.fetch({ guild: guildId, withCounts: true, force: true });
  return {
    miembros: guild.approximateMemberCount ?? 0,
    conectados: guild.approximatePresenceCount ?? 0,
    mejoras: guild.premiumSubscriptionCount ?? 0,
  };
}

async function actualizar() {
  for (const [guildId, canales] of Object.entries(leer())) {
    try {
      const cuentas = await cifras(guildId);
      for (const [clave, idCanal] of Object.entries(canales)) {
        const canal = client.channels.cache.get(idCanal);
        if (!canal) continue; // han borrado el canal: no se hace nada
        const nuevo = nombre(clave, cuentas[clave]);
        if (canal.name !== nuevo) await canal.setName(nuevo, "Contador de miembros"); // solo si ha cambiado
      }
    } catch (error) {
      console.error(`No he podido actualizar el servidor ${guildId}:`, error);
    }
  }
}

const comando = new SlashCommandBuilder()
  .setName("contador")
  .setDescription("Crea los canales con los miembros, los conectados y las mejoras")
  .setContexts(InteractionContextType.Guild)
  .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels); // de serie, solo quien puede gestionar canales

client.on(Events.InteractionCreate, async (i) => {
  if (!i.isChatInputCommand() || i.commandName !== "contador") return;
  const datos = leer();
  if (Object.values(datos[i.guildId] ?? {}).some((id) => i.guild.channels.cache.has(id))) {
    await i.reply({ content: "Ya están creados. Para empezar de cero, borra la categoría y vuelve a escribir /contador.", flags: MessageFlags.Ephemeral });
    return;
  }
  await i.deferReply({ flags: MessageFlags.Ephemeral }); // crear canales puede tardar unos segundos
  // Todos ven los canales, pero nadie puede entrar en ellos: son solo un cartel.
  const permisos = [
    { id: i.guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel], deny: [PermissionFlagsBits.Connect] },
    { id: client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels] },
  ];
  try {
    const categoria = await i.guild.channels.create({ name: "📊 Estadísticas", type: ChannelType.GuildCategory, permissionOverwrites: permisos, position: 0 });
    const cuentas = await cifras(i.guildId);
    const canales = {};
    for (const clave of Object.keys(TEXTOS)) {
      const canal = await i.guild.channels.create({ name: nombre(clave, cuentas[clave]), type: ChannelType.GuildVoice, parent: categoria.id, permissionOverwrites: permisos });
      canales[clave] = canal.id;
    }
    datos[i.guildId] = canales;
    guardar(datos);
    await i.editReply("Hecho: arriba del todo tienes la categoría «📊 Estadísticas». Las cifras se ponen al día cada 10 minutos.");
  } catch (error) {
    console.error(error);
    await i.editReply("Me faltan permisos: necesito «Ver canales», «Gestionar canales» y «Conectarse».");
  }
});

client.once(Events.ClientReady, async (c) => {
  await c.application.commands.set([comando.toJSON()]); // publica /contador
  console.log(`Conectado como ${c.user.tag}`);
  setInterval(actualizar, DIEZ_MINUTOS);
  actualizar();
});

client.login(process.env.DISCORD_TOKEN);

3. Pruébalo

Quién ve /contador se cambia sin tocar el código en «Ajustes del servidor» → «Integraciones» → tu bot. Si borras un canal, el bot deja de tocarlo; para empezar de cero, borra la categoría con sus canales y vuelve a escribir /contador.

  1. Arranca el bot

    En una terminal, en la carpeta: pip install -r requirements.txt y python app.py (o npm install y node index.js). Cuando diga «Conectado como…», ya está. El comando puede tardar unos minutos en salir la primera vez.

  2. Crea los contadores

    Escribe /contador en cualquier canal. En unos segundos aparece arriba la categoría «📊 Estadísticas» con los tres canales. Muévela donde quieras: el bot solo cambia los nombres, no el sitio.

  3. Mira cómo cambia

    Invita a alguien o sal y entra con otra cuenta. La cifra no cambia al momento: en la siguiente vuelta del bot (como mucho, 10 minutos) y, a veces, alguna más, porque los recuentos de Discord son aproximados y tardan un poco en ponerse al día.

Por qué cada 10 minutos y no al momento

Discord deja cambiar el nombre de un mismo canal solo 2 veces cada 10 minutos. Un bot que renombra el canal cada vez que alguien entra o sale se queda sin cambios en cuanto llegan tres personas seguidas, y a partir de ahí cada cambio espera en la cola: la cifra va cada vez más atrasada. discord.py lo apunta en la consola como «We are being rate limited… responded with 429».

Por eso este bot da una vuelta cada 10 minutos y solo renombra el canal si la cifra ha cambiado: como mucho, un cambio por canal y vuelta, la mitad de lo que permite Discord. Si bajas el intervalo, no irá más rápido: irá a trompicones.

«Conectados» es quien no aparece como desconectado: en línea, ausente o no molestar. Quien se pone invisible cuenta como desconectado, igual que en la lista de miembros.

Si algo no va

«Me faltan permisos»: al bot le falta «View Channels», «Manage Channels» o «Connect». Vuelve a invitarlo con el enlace de la calculadora, o dale esos permisos a su rol en «Ajustes del servidor» → «Roles».

Los canales se crearon pero el nombre no cambia nunca: alguien ha quitado al bot de los permisos de la categoría o del canal. En el canal, «Editar canal» → «Permisos», añade el rol del bot con «Ver canal» y «Gestionar canal». La consola lo dice con «Missing Permissions» (el código 50013).

No sale /contador: qué mirar cuando los comandos no aparecen. Si el bot está en línea y no contesta, mi bot no responde.

Contar otras cosas

Las cifras salen de cifras(): añade una clave con su texto en TEXTOS y su valor en cifras(), y /contador creará un canal más. Con lo que ya trae el servidor, sin permisos nuevos: el número de canales (len(guild.channels) en Python, con el servidor de la caché) o de roles.

Separar personas de bots, o contar a quien tiene un rol, sí pide el intent «Server Members»: hay que activarlo en el portal y en el código, y el bot tiene que repasar a todos los miembros. Merece la pena si ya lo usas, por ejemplo con el bot de bienvenida; qué intent pide cada cosa, en la calculadora de intents.

Seguidores de YouTube o de Twitch: eso lo da la API de cada sitio, no Discord. Para avisar de directos y vídeos nuevos, el bot de avisos de Twitch y YouTube.

4. Déjalo encendido siempre (gratis)

Si el bot vive en tu ordenador, los contadores se quedan quietos en cuanto lo apagas: siguen en el servidor con la última cifra, pero ya no se mueven. Hay que alojarlo en un servidor; 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», sube tus ficheros en «Archivos» y pulsa «Encender»: las librerías de requirements.txt o package.json se instalan solas al arrancar. contadores.json se queda en la carpeta del bot entre reinicios. Este bot gasta muy poco, así que cabe junto a otros en el mismo: para no tener que renovar, el plan Mini (512 MB) está encendido siempre (precios de los bots).

Preguntas frecuentes

¿Cómo pongo el número de miembros en un canal de Discord?

Con un bot que cambie el nombre de un canal de voz, por ejemplo a «👥 Miembros: 1.234», y lo ponga al día cada 10 minutos. El código completo en Python y en JavaScript está en esta guía.

¿Por qué mi contador de miembros no se actualiza?

Porque Discord solo deja cambiar el nombre de un canal 2 veces cada 10 minutos. Un bot que lo renombra con cada entrada o salida se queda atascado en la cola; renombrar cada 10 minutos y solo si la cifra cambia lo evita.

¿Un bot contador de miembros necesita intents privilegiados?

No, si usa los recuentos que Discord calcula para cada servidor (miembros y conectados), como este. Solo hace falta el de miembros para separar personas de bots o contar a quien tiene un rol.

¿Qué permisos necesita el bot contador?

Ver canales, gestionar canales y conectarse (el número de permisos es 1049616). El de conectarse hace falta para poder quitar a todos el permiso de entrar en los canales del contador.

¿Dónde dejo el bot contador encendido siempre?

En un servidor. En NapNodes hay un bot gratis de 256 MB que se renueva con un clic cada semana, o el plan Mini, encendido siempre.

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