Skip to content

ディスコード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.
  1. 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.
  2. Invite it to your server

    Under OAuth2 → URL Generator, tick the scopes bot and applications.commands, then the permissions Send Messages and Embed Links. Open the generated URL and pick your server.

  3. Set up the project

    terminal
    mkdir anime-bot && cd anime-bot
    npm init -y && npm pkg set type=module
    npm install discord.js@14

    Create .env next to it. Keep it out of git.

    .env
    DISCORD_TOKEN=your-bot-token
    DISCORD_CLIENT_ID=your-application-id
    ANIMEAPI_KEY=ank_your_key
  4. Register 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.

  5. 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.mjs

    Type /anime in 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. allowNSFW is only true in channels marked age-restricted. The API defaults it to true, so the bot sets it explicitly.
  • Errors become sentences. 402 (out of balance) is the one your users will actually hit.