← Back to Blog
Tutorial2025-12-288 min read

Building a Discord Bot That Generates Images

A working /imagine command for Discord, built on discord.js v14 and OpenAI's gpt-image-1 — the actual interaction handler, the deferred reply you need because generation isn't instant, and the env-var setup that keeps your tokens out of git.

By Kyle

If you run a Discord server, someone's going to ask for an image bot eventually, and the actual build is smaller than it sounds: a slash command, one call to OpenAI, and a reply with an attachment. Here's the whole thing using discord.js v14 and gpt-image-1, plus the two mistakes that trip people up the first time — replying too slowly and leaking a token into your repo.

What you need before starting

A Discord bot application (create one at the Discord Developer Portal, grab the bot token, and invite it to your server with the bot and applications.commands scopes), an OpenAI API key, and Node.js. Install the two packages you'll actually use:

npm install discord.js openai dotenv

The command and the handler

This is the core of the bot: registering an /imagine command with a required prompt option, then handling it when someone runs it. The image comes back from OpenAI as base64 in response.data[0].b64_json — decode that straight into a Buffer and Discord will accept it as a file attachment, no separate upload step or URL needed.

const { SlashCommandBuilder, REST, Routes, AttachmentBuilder } = require('discord.js');
const OpenAI = require('openai');

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const commands = [
  new SlashCommandBuilder()
    .setName('imagine')
    .setDescription('Generate an AI image from a text prompt')
    .addStringOption(option =>
      option
        .setName('prompt')
        .setDescription('Describe the image you want')
        .setRequired(true)
    )
];

async function registerCommands() {
  const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
  await rest.put(
    Routes.applicationCommands(process.env.CLIENT_ID),
    { body: commands.map(c => c.toJSON()) }
  );
}

async function handleInteraction(interaction) {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== 'imagine') return;

  const prompt = interaction.options.getString('prompt');

  // Generation takes a few seconds — defer immediately or Discord
  // will time out the interaction before you can reply.
  await interaction.deferReply();

  try {
    const response = await openai.images.generate({
      model: 'gpt-image-1',
      prompt,
      size: '1024x1024',
      quality: 'medium'
    });

    const b64 = response.data[0].b64_json;
    const buffer = Buffer.from(b64, 'base64');
    const attachment = new AttachmentBuilder(buffer, { name: 'imagine.png' });

    await interaction.editReply({
      content: `"${prompt}"`,
      files: [attachment]
    });
  } catch (error) {
    console.error('gpt-image-1 generation failed:', error);
    await interaction.editReply({
      content: 'Could not generate that image. Try a different prompt.'
    });
  }
}

module.exports = { commands, registerCommands, handleInteraction };

Two details matter here. First, deferReply() isn't optional — Discord expects an acknowledgment within 3 seconds, and an image generation call routinely takes longer than that, so without deferring, the interaction just fails before OpenAI even responds. Second, the try/catch around the OpenAI call actually matters: prompts get rejected for policy reasons, OpenAI rate-limits under load, and requests occasionally time out, so editReply needs a real fallback message instead of leaving the interaction hanging.

Wiring up the client

The rest is standard discord.js bootstrapping — read both tokens from environment variables, never hardcode them:

require('dotenv').config();
const { Client, GatewayIntentBits } = require('discord.js');
const { commands, registerCommands, handleInteraction } = require('./commands');

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

client.once('ready', async () => {
  await registerCommands();
  console.log(`Logged in as ${client.user.tag}`);
});

client.on('interactionCreate', handleInteraction);

client.login(process.env.DISCORD_TOKEN);

Put DISCORD_TOKEN, CLIENT_ID, and OPENAI_API_KEY in a .env file, add .env to .gitignore before your first commit, and set the same variables in your host's environment config when you deploy. A leaked OpenAI key gets used by someone else within hours; a leaked Discord token lets anyone control your bot.

Where this gets harder

The version above handles the happy path. Running it for real means adding a per-user cooldown (nothing stops someone from spamming /imagine in a loop and running up your OpenAI bill), deciding what to do when a prompt gets flagged by OpenAI's moderation instead of just showing a generic error, and picking a process manager (PM2, a Docker container, whatever your host expects) so the bot restarts if it crashes. None of that is hard, but it's also not shown here — budget an extra afternoon for it once the core command is working.

If you'd rather not run a bot process at all, the same idea — a prompt in, an image out — is what add image generation to a Next.js app covers for a web frontend instead of Discord, and Imagify exists as a no-backend option if you want a generation UI without maintaining either. If you're choosing which image API to build against in the first place, the API comparison covers price and quality across the current field.

Ready to Start Generating?

Try 2 free images as a guest, no account required. Create a free account to save and download them.

Get Started Free