Objetivos de aprendizaje
Después de leer este artículo podrás:
- Armar un comando /ask en discord.js que llama a una API de LLM
- Evitar los errores típicos: interacción expirada, mensaje de más de 2000 caracteres y abuso de costos
- Poner el bot en línea con las claves en variables de entorno
Un bot de IA para Discord es un bot normal con una llamada extra: en lugar de responder con un texto fijo, envía la pregunta del usuario a un modelo de lenguaje (LLM) mediante una API y publica lo que vuelve. El usuario escribe /ask, Discord entrega la interacción al bot por el Gateway, el bot llama a la API del modelo y edita la respuesta con el texto.
El bot no ejecuta ningún modelo: guarda los secretos, aplica límites y da formato a la salida, por eso sigue siendo ligero. A cambio, aparecen problemas que un bot común no tiene: la respuesta tarda, puede pasarse del tamaño permitido y cada llamada cuesta dinero.
Los ejemplos usan discord.js v14 y el SDK @anthropic-ai/sdk (la API de Anthropic, con un modelo pequeño y barato de la línea Haiku). El diseño sirve para cualquier proveedor: solo cambia la llamada. Si todavía no tienes un bot funcionando, empieza por cómo hospedar un bot de discord.js. Para entender el modelo que hay detrás, mira qué es un LLM.
¿Qué necesitas antes de empezar?#
- La versión LTS activa de Node.js y una aplicación creada en el Discord Developer Portal, con el token del bot. Mira cómo guardar el token.
- Una clave de API de un proveedor de LLM. Aquí usamos una de la Claude Console, leída de la variable
ANTHROPIC_API_KEY, el nombre que el SDK busca por sí solo. - Un servidor de prueba donde puedas invitar al bot con los scopes
botyapplications.commands.
¿Cómo configurar el proyecto?#
mkdir ai-bot && cd ai-bot
npm init -y
npm pkg set type=module
npm install discord.js@14 @anthropic-ai/sdkCrea el .env (y añade .env al .gitignore):
DISCORD_TOKEN=pega_el_token_del_bot
CLIENT_ID=id_de_la_aplicacion
GUILD_ID=id_del_servidor_de_prueba
ANTHROPIC_API_KEY=pega_la_clave_de_la_api
ANTHROPIC_MODEL=id_de_un_modelo_pequenoNode.js lee ese archivo con la flag --env-file, sin paquetes extra. En ANTHROPIC_MODEL, usa el ID de un modelo pequeño, rápido y barato (de la línea Haiku, por ejemplo), copiado de la lista de modelos de Anthropic. El modelo queda fuera del código porque Anthropic retira modelos con el tiempo: cambiar de modelo pasa a ser cambiar la variable.
¿Cómo escribir el bot?#
Crea index.js. El bot usa solo el intent Guilds, mantiene los últimos 10 mensajes de cada canal e impone 10 segundos de espera por usuario:
import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import Anthropic from "@anthropic-ai/sdk";
const MODEL = process.env.ANTHROPIC_MODEL;
if (!MODEL) throw new Error("Define ANTHROPIC_MODEL con un ID de la lista de modelos de Anthropic");
const SYSTEM =
"Eres un asistente de un servidor de Discord. Responde en español, " +
"de forma directa y con un máximo de 1500 caracteres.";
const anthropic = new Anthropic(); // lee ANTHROPIC_API_KEY
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const history = new Map(); // channelId -> [{ role, content }]
const lastUse = new Map(); // userId -> timestamp
client.once(Events.ClientReady, (c) => console.log(`En línea como ${c.user.tag}`));
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "ask") return;
const wait = 10_000 - (Date.now() - (lastUse.get(interaction.user.id) ?? 0));
if (wait > 0) {
await interaction.reply({
content: `Espera ${Math.ceil(wait / 1000)}s para volver a preguntar.`,
flags: MessageFlags.Ephemeral,
});
return;
}
lastUse.set(interaction.user.id, Date.now());
await interaction.deferReply(); // Discord exige respuesta en 3 s
const question = `${interaction.user.displayName}: ${interaction.options.getString("pregunta", true)}`;
const past = history.get(interaction.channelId) ?? [];
try {
const response = await anthropic.messages.create({
model: MODEL,
max_tokens: 2000,
output_config: { effort: "low" }, // piensa poco: respuesta corta y barata
system: SYSTEM,
messages: [...past, { role: "user", content: question }],
});
const text = response.content
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("")
.trim();
const answer = text || "No pude generar una respuesta.";
history.set(
interaction.channelId,
[...past, { role: "user", content: question }, { role: "assistant", content: answer }].slice(-10),
);
await interaction.editReply(answer.slice(0, 2000));
} catch (error) {
console.error(error);
await interaction.editReply("La IA no respondió ahora. Inténtalo de nuevo en unos instantes.");
}
});
client.login(process.env.DISCORD_TOKEN);Cuatro detalles importan:
deferReply()va antes de la llamada a la API. Según la documentación de Discord, la primera respuesta tiene que salir en un máximo de 3 segundos; si no, el token de la interacción se invalida. Después del defer, el token vale 15 minutos para eleditReply.slice(-10)guarda siempre pares de pregunta y respuesta, así que el historial empieza por un mensaje del usuario, como exige la API.effort: "low"ymax_tokens: 2000van juntos. Si el modelo piensa antes de responder, ese razonamiento cuenta enmax_tokens. El esfuerzo bajo acorta el pensamiento, y el tope deja espacio para él y para la respuesta. Si aun así el texto llega vacío, el bot usa el mensaje de reserva. Si el modelo elegido no acepta el parámetroeffort, la API devuelve un error 400: quita esa línea.slice(0, 2000)respeta el límite de 2000 caracteres del contenido de un mensaje de Discord.
¿Cómo registrar el comando?#
Iniciar el bot no crea el comando. Ejecuta un script de registro una vez, y de nuevo cuando cambies la definición (deploy-commands.js):
import { REST, Routes, SlashCommandBuilder } from "discord.js";
const ask = new SlashCommandBuilder()
.setName("ask")
.setDescription("Hazle una pregunta a la IA")
.addStringOption((o) =>
o.setName("pregunta").setDescription("Tu pregunta").setRequired(true).setMaxLength(1000),
);
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID), {
body: [ask.toJSON()],
});
console.log("Comando registrado.");El setMaxLength(1000) limita el texto de entrada, lo que también limita el costo de cada llamada. Para que valga en todos los servidores, cambia la ruta por Routes.applicationCommands(process.env.CLIENT_ID).
¿Cómo probarlo?#
node --env-file=.env deploy-commands.js
node --env-file=.env index.jsLa terminal debe mostrar En línea como NombreDelBot. En el servidor, ejecuta /ask pregunta: ¿qué es un contenedor?. Verás "el bot está pensando" y después la respuesta. Haz una segunda pregunta que dependa de la primera ("¿y una VM?") para confirmar que la memoria del canal funciona.
¿Qué errores aparecen y cómo evitar costos y abusos?#
Unknown interaction(código 10062): la primera respuesta superó los 3 segundos. Confirma quedeferReply()es la primera llamada asíncrona del handler.- Error 401 (
authentication_error) de la API: laANTHROPIC_API_KEYno llegó al proceso. El.envlocal no existe en el servidor, así que configura la variable allí. - Error 404 (
not_found_error) que menciona el modelo: el ID deANTHROPIC_MODELestá mal o el modelo fue retirado. Elige otro de la lista de modelos y reinicia el bot, sin tocar el código. Invalid Form Bodyconcontentde más de 2000: enviaste un texto largo sin recortarlo. Mantén elslicey pide respuestas cortas en el prompt de sistema.- Factura inesperada: el costo viene de los tokens de entrada y salida. Los modelos pequeños, como los de la línea Haiku, son los más baratos de la tabla de Anthropic, pero el precio por millón de tokens cambia: consulta el valor actual antes de publicar. Usa el tiempo de espera, el
max_tokensy el límite de entrada, y mira cuánto cuesta una API de LLM. - Usuarios manipulando al bot: cualquier texto que llega al modelo puede intentar cambiar sus instrucciones. Nunca le des al bot permisos que no le darías al usuario. Lee qué es el prompt injection.
- Clave filtrada: si la clave terminó en un repositorio, revócala y genera otra en la consola del proveedor.
¿Dónde hospedar el bot?#
Un bot de IA necesita estar encendido todo el tiempo y guardar dos claves en secreto. En la computadora de casa se cae cuando apagas la máquina; en un VPS te encargas del supervisor, las actualizaciones y el firewall (mira cómo mantener un bot en línea 24/7). En cuanto a la RAM, el proceso sigue siendo pequeño, porque el modelo se ejecuta en el proveedor: mira cuánta RAM necesita un bot. Para un asistente que atiende un sitio web y no solo Discord, mira también cómo alojar un chatbot con IA.
Cómo hacerlo en Square Cloud#
El bot que escribiste depende de dos cosas: un proceso siempre encendido y las llamadas al modelo. En Square Cloud resuelves las dos en el mismo lugar.
El proceso corre en un contenedor aislado y siempre encendido. Con el reinicio automático (activado por defecto en el deploy desde el panel), vuelve solo si termina con error; si se queda sin memoria, se detiene hasta que subas la RAM y lo inicies de nuevo.
- Sube el proyecto (
index.js,package.json) como ZIP en el panel, o usa la CLI, GitHub o la extensión de VS Code. El archivo de configuraciónsquarecloud.appes opcional: el panel lo genera con runtime, memoria y archivo principal, o lo escribes en la raíz del proyecto. - Elige Node.js e
index.jscomo archivo principal. Un bot usa al menos 256 MB del pool de RAM del plan. - Registra
DISCORD_TOKEN,CLIENT_ID,ANTHROPIC_API_KEYyANTHROPIC_MODELen las variables de entorno de la aplicación. El cambio se aplica en el siguiente reinicio, sin rebuild. - Sigue los logs en tiempo real y confirma la línea
En línea como ....
Desde el plan Standard (R$44,99 al mes), la llamada al modelo también puede pasar por el AI Gateway, una API compatible con la de OpenAI para el modelo cubic, el mismo del asistente del panel. Está en beta, con acceso anticipado sin costo extra. Instala el paquete openai, reemplaza la parte de anthropic.messages.create y borra las dos líneas de MODEL, que dejan de usarse:
import OpenAI from "openai";
const ai = new OpenAI({
baseURL: "https://api.squarecloud.app/v2/ai",
apiKey: process.env.SQUARECLOUD_API_KEY, // clave de la cuenta con el scope ai:chat
});
const completion = await ai.chat.completions.create({
model: "cubic",
messages: [{ role: "system", content: SYSTEM }, ...past, { role: "user", content: question }],
});
const text = completion.choices[0].message.content?.trim() ?? "";El historial por canal sigue en el bot, porque la API no guarda estado. Antes de cambiar, revisa los límites del Standard: hasta 2,5 millones de tokens y hasta 1000 solicitudes por día, una solicitud a la vez. Una pregunta que llega mientras otra está en curso recibe un error 429; el SDK de OpenAI reintenta algunas veces, y el catch del bot cubre el resto. Los límites y los planes pueden cambiar cuando termine la beta. El Hobby, desde R$12,49 al mes, aloja el bot, pero no incluye el gateway. Mira cómo alojar el bot en bots de Discord.
IA
Aloja apps con IA en contenedores siempre encendidos
Preguntas frecuentes
¿Necesito el intent MessageContent para un bot de IA?
No, si el bot responde a slash commands. El intent Guilds basta. MessageContent es privilegiado y solo hace falta para leer el texto de los mensajes normales.
¿Por qué la respuesta de la IA da error de interacción desconocida?
Discord exige una primera respuesta en un máximo de 3 segundos, y un LLM suele tardar más. Llama a deferReply() al principio y usa editReply() cuando llegue la respuesta.
¿Cómo recuerda el bot lo que se dijo antes?
La API de modelos no guarda estado: envías el historial completo en cada llamada. Guarda los últimos mensajes de cada canal y envíalos junto con la pregunta nueva.
¿Cuánto cuesta mantener un bot de IA en Discord?
Hay dos costos: el hosting del proceso y el consumo de tokens de la API, que crece con el uso. Limita el tamaño de las respuestas y crea un tiempo de espera por usuario.
¿La respuesta de la IA puede superar los 2000 caracteres?
Discord acepta hasta 2000 caracteres por mensaje. Pide respuestas cortas en el prompt de sistema y recorta el texto en el código por si acaso, o divídelo en varios mensajes.
Más en IA
Ver los 13 artículos- ¿Cuánto cuesta crear y mantener un chatbot con IA?Mantener un chatbot con IA cuesta tokens del modelo, hosting, base de datos y, con RAG, contexto extra. Mira cómo hacer la cuenta por conversación.
- ¿Cuánto cuesta un sistema RAG? Embeddings, base de datos y LLMUn RAG tiene tres costos: embeddings, base de datos vectorial y LLM. Mira la cuenta para mil preguntas al día, con precios hipotéticos, y dónde crece la factura.
- ¿Cuánto cuesta usar la API de un LLM?La API de un LLM cobra por token y la salida cuesta más que la entrada. Mira un cálculo paso a paso para un bot y cómo recortar el gasto.
- Cómo hospedar n8nHospedar n8n es ejecutar su servidor en un proceso siempre encendido, con URL pública HTTPS, base de datos y clave de cifrado fija. Paso a paso.