Everything below was built with Gradle 9.8 and run against a test copy of Discord with JDA 6.7: every command, the button and the message reply answer as shown.
1. Create the application
- Go to the Discord Developer Portal and press New Application. The name is what people will see.
- Open Bot. Press Reset Token and copy it. This is the bot's password: anyone with it can run your bot. Keep it somewhere private for now.
- Privileged Gateway Intents can stay off until step 8, which needs Message Content Intent.
2. Invite it to your server
- Open OAuth2, then URL Generator.
- Tick
botandapplications.commands. - Under bot permissions, tick Send Messages (and whatever else your bot will need later). The invite link builder does the same.
- Open the URL at the bottom, pick your server, and authorise.
The bot shows up in the member list, offline. It comes online when your code runs.
3. Set up the project
You need a JDK, 17 or newer (java -version), and Gradle (gradle -v); IntelliJ IDEA comes with both. Make a folder called my-bot with this layout:
my-bot/
settings.gradle
build.gradle
src/main/java/com/example/bot/Main.javasettings.gradle names the project:
rootProject.name = 'my-bot'build.gradle adds JDA, a way to run the bot, and a plugin that later packs everything into one jar:
plugins {
id 'java'
id 'application'
id 'com.gradleup.shadow' version '8.3.5'
}
version = '1.0'
repositories { mavenCentral() }
dependencies {
implementation 'net.dv8tion:JDA:6.7.0'
}
// Class files for Java 17, so the jar runs on 17, 21 or 25 whichever JDK built it.
tasks.withType(JavaCompile).configureEach { options.release = 17 }
application {
mainClass = 'com.example.bot.Main'
}Then, in the folder, run gradle wrapper once. It writes gradlew, which pins the Gradle version for this project; from now on use ./gradlew (.\gradlew on Windows).
4. Keep the token out of the code
The bot reads its token from an environment variable called BOT_TOKEN, so it is never in a file you might share or commit. Set it in the terminal you run the bot from. On macOS or Linux:
export BOT_TOKEN=paste-your-token-hereIn Windows PowerShell:
$env:BOT_TOKEN = "paste-your-token-here"5. Write the bot
All of it goes in Main.java: four slash commands, an embed and a button.
package com.example.bot;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.components.actionrow.ActionRow;
import net.dv8tion.jda.api.components.buttons.Button;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.InteractionContextType;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.utils.TimeFormat;
import java.util.concurrent.ThreadLocalRandom;
public class Main extends ListenerAdapter {
public static void main(String[] args) throws InterruptedException {
String token = System.getenv("BOT_TOKEN");
if (token == null || token.isBlank()) {
System.err.println("Set BOT_TOKEN first.");
System.exit(1);
}
JDA jda = JDABuilder.createLight(token)
.addEventListeners(new Main())
.build();
jda.awaitReady();
// Tell Discord which slash commands exist. Once per start is plenty.
jda.updateCommands().addCommands(
Commands.slash("hello", "Say hello"),
Commands.slash("roll", "Roll a die")
.addOptions(new OptionData(OptionType.INTEGER, "sides", "How many sides").setMinValue(2)),
Commands.slash("serverinfo", "Show some facts about this server").setContexts(InteractionContextType.GUILD),
Commands.slash("counter", "A button that counts its clicks")
).queue();
System.out.println("Logged in as " + jda.getSelfUser().getName());
}
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
switch (event.getName()) {
case "hello" -> event.reply("Hello, " + event.getUser().getAsMention() + "!").queue();
case "roll" -> {
int sides = event.getOption("sides", 6, OptionMapping::getAsInt);
event.reply("You rolled " + ThreadLocalRandom.current().nextInt(1, sides + 1)).queue();
}
case "serverinfo" -> {
Guild guild = event.getGuild();
EmbedBuilder embed = new EmbedBuilder()
.setTitle(guild.getName())
.setColor(0x5865F2)
.addField("Members", String.valueOf(guild.getMemberCount()), true)
.addField("Created", TimeFormat.DATE_LONG.format(guild.getTimeCreated()), true);
if (guild.getIconUrl() != null) embed.setThumbnail(guild.getIconUrl());
event.replyEmbeds(embed.build()).queue();
}
case "counter" -> event.reply("Press it:")
.addComponents(ActionRow.of(Button.primary("count:0", "Clicked 0 times")))
.queue();
}
}
@Override
public void onButtonInteraction(ButtonInteractionEvent event) {
String id = event.getComponentId();
if (!id.startsWith("count:")) return;
// The count lives in the button's own ID, so it survives a restart.
int count = Integer.parseInt(id.substring("count:".length())) + 1;
event.editButton(Button.primary("count:" + count, "Clicked " + count + " times")).queue();
}
}createLightstarts JDA with no member or presence caches, which a bot like this does not need, and asks for no privileged intents.awaitReady()waits until the bot is connected, soupdateCommands()runs once it knows who it is.- The counter keeps its count in the button's ID (
count:3). Nothing is stored in memory, so the button carries on counting after a restart. - Every JDA call that talks to Discord ends in
.queue(). Without it, nothing is sent.
6. Run it
./gradlew runThe console says Logged in as and your bot's name, the bot turns online, and the commands appear when you type / in your server. If they do not show up straight away, press Ctrl+R in Discord to reload it.
7. Make new commands appear instantly while you test
jda.updateCommands() registers commands globally, for every server the bot is in, and the Discord app can be slow to show changes. While you build, register them to your own server instead, where they change the moment you restart. Copy your server's ID (Developer Mode on under User Settings, Advanced, then right-click the server) and replace the jda.updateCommands().addCommands( line with:
Guild myServer = jda.getGuildById("123456789012345678"); // your server's ID
myServer.updateCommands().addCommands(Switch back to jda.updateCommands() when the bot is ready for other servers.
8. Reply to ordinary messages
Slash commands need nothing more. To read what people type, the bot needs two more intents, and Message Content Intent switched on in the Developer Portal on the Bot page. Change the builder line in main to:
JDA jda = JDABuilder.createLight(token, GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)add import net.dv8tion.jda.api.requests.GatewayIntent; and import net.dv8tion.jda.api.events.message.MessageReceivedEvent; at the top, and this method next to the other two:
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return; // never answer bots, including yourself
if (event.getMessage().getContentRaw().equalsIgnoreCase("ping")) {
event.getChannel().sendMessage("pong").queue();
}
}9. Build one jar for hosting
./gradlew shadowJarThis makes build/libs/my-bot-1.0-all.jar, with JDA and everything it needs inside, and runs with java -jar build/libs/my-bot-1.0-all.jar wherever Java 17 or newer is installed. A plain ./gradlew jar leaves the libraries out, and the bot stops with NoClassDefFoundError. The options.release = 17 line keeps the jar runnable on Java 17 even if you build it with a newer JDK.
Older examples and JDA 6
Many examples online are for JDA 5, and some of their code no longer compiles on JDA 6:
ButtonandActionRowmoved tonet.dv8tion.jda.api.components.buttonsandnet.dv8tion.jda.api.components.actionrow. The oldnet.dv8tion.jda.api.interactions.componentsimports fail withpackage ... does not exist.setGuildOnly(true)is gone: usesetContexts(InteractionContextType.GUILD).- Components are added with
addComponents(ActionRow.of(...)).
If it did not work
InvalidTokenException: The provided token is invalid!:BOT_TOKENholds something other than the bot token (the client secret is a common mix-up), or the token was reset. Copy a fresh one from the Bot page.Was shutdown trying to await status. Reason: You tried turning on an intent you aren't allowed to use(close code 4014): the code asks for Message Content, but it is off in the Developer Portal.Set BOT_TOKEN first.: the variable is not set in this terminal. Set it again; it does not carry over to a new window.UnsupportedClassVersionError: the jar was built for a newer Java than the one running it. Keep theoptions.releaseline, or run a newer Java.NoClassDefFoundError: the jar was built withjarinstead ofshadowJar.- Commands do not appear: the bot was invited without
applications.commands. Make a new invite link with both scopes and open it again.
Where to go next
- Keeping it online: the bot stops when you close the terminal. Your options for running it 24/7, free ones included.
- Hosting the jar: getting a JDA bot ready for hosting, memory settings included.
- Before you share it: keeping the token safe.
When you want it running all the time, SnowServers runs Java bots on Java 17, 21 or 25 from $3 a month, with a 7-day free trial: upload the jar from step 9 in the panel's Files, paste the token in, and it stays online. How Java bots run here.
Run it here. Flurry keeps a bot like this online all day for $3 a month, and the first week is free.
Start the free weekDid this page help?
Thanks for the feedback.