ディスコードGuide · Discord
An /anime command for your server.
A discord.js v14 bot with one slash command:
/anime prompt:<text>, with optional model and orientation. Two files, under a hundred lines. You need Node 20+, an AnimeAPI key with some balance, and a Discord server where you can add bots.Create the Discord application
- Open the Discord Developer Portal and click New Application.
- On General Information, copy the Application ID.
- On Bot, click Reset Token and copy the token. Slash commands don’t need any privileged intents.
Invite it to your server
Under OAuth2 → URL Generator, tick the scopes
botandapplications.commands, then the permissionsSend MessagesandEmbed Links. Open the generated URL and pick your server.Set up the project
terminal mkdir anime-bot && cd anime-bot npm init -y && npm pkg set type=module npm install discord.js@14Create
.envnext to it. Keep it out of git..env DISCORD_TOKEN=your-bot-token DISCORD_CLIENT_ID=your-application-id ANIMEAPI_KEY=ank_your_keyRegister the command
Run this once, and again whenever you change the command’s options.
register.mjs // register.mjs · run once: node --env-file=.env register.mjs import { REST, Routes, SlashCommandBuilder } from "discord.js"; const anime = new SlashCommandBuilder() .setName("anime") .setDescription("Draw an anime image") .addStringOption((o) => o.setName("prompt").setDescription("What to draw").setRequired(true).setMaxLength(1000), ) .addStringOption((o) => o .setName("model") .setDescription("Quality tier (default: anime)") .addChoices( { name: "anime · $0.003", value: "anime" }, { name: "ultra · $0.01", value: "ultra" }, { name: "ultra-max · $0.02", value: "ultra-max" }, ), ) .addStringOption((o) => o .setName("orientation") .setDescription("Shape (default: portrait)") .addChoices( { name: "portrait", value: "portrait" }, { name: "square", value: "square" }, { name: "landscape", value: "landscape" }, ), ); const rest = new REST().setToken(process.env.DISCORD_TOKEN); await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), { body: [anime.toJSON()], }); console.log("Registered /anime");It registers a global command, which can take a few minutes to appear in your server the first time.
Run the bot
bot.mjs // bot.mjs · run: node --env-file=.env bot.mjs import { Client, EmbedBuilder, Events, GatewayIntentBits } from "discord.js"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.once(Events.ClientReady, (c) => console.log(`Ready as ${c.user.tag}`)); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand() || interaction.commandName !== "anime") return; const prompt = interaction.options.getString("prompt", true); const model = interaction.options.getString("model") ?? "anime"; const orientation = interaction.options.getString("orientation") ?? "portrait"; // Images take 4-13s. Discord wants an answer within 3s, so defer first. await interaction.deferReply(); try { const res = await fetch("https://animeapi.ai/api/generate", { method: "POST", headers: { Authorization: `Bearer ${process.env.ANIMEAPI_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ prompt, model, orientation, // Only allow mature output in channels marked age-restricted. allowNSFW: interaction.channel?.nsfw === true, }), }); const data = await res.json(); if (!res.ok) { await interaction.editReply(explain(res.status, data)); return; } const embed = new EmbedBuilder() .setDescription(prompt.slice(0, 4000)) .setImage(data.image_url) .setFooter({ text: `${model} · ${(data.generation_time_ms / 1000).toFixed(1)}s` }); await interaction.editReply({ embeds: [embed] }); } catch (err) { console.error(err); await interaction.editReply("Couldn't reach AnimeAPI. Try again in a moment."); } }); function explain(status, data) { if (status === 402) return "The bot's AnimeAPI balance is empty. Ask the owner to top up."; if (status === 400) return `Couldn't draw that: ${data.error}`; return `AnimeAPI error ${status}. Try again.`; } client.login(process.env.DISCORD_TOKEN);terminal node --env-file=.env bot.mjsType
/animein your server. The bot shows “thinking…” and replies with an embed a few seconds later.
→Why it’s written this way
deferReply()first. Discord drops interactions that aren’t acknowledged within 3 seconds, and images take 4–13 seconds depending on the model.- NSFW follows the channel.
allowNSFWis only true in channels marked age-restricted. The API defaults it totrue, so the bot sets it explicitly. - Errors become sentences. 402 (out of balance) is the one your users will actually hit.