Star ✨ on GitHub

Creating Your First Bot

A complete (not simplified) Discord music bot foundation using Moonlink.js + discord.js.

This page builds a real starter bot, not a toy example.

By the end you will have:

  • a full index.js (client + manager + loaders)
  • event files (ready, messageCreate)
  • command loading from commands/
  • Moonlink debug + node/playback event listeners
  • the correct voice packet wiring (send + raw)

1) index.js — the main file

Your main file has a single job: wire everything together.

What it does (high level)

  1. Create the Discord client.
  2. Create the Moonlink manager.
  3. Load commands.
  4. Load events.
  5. Login.

2) index.js — complete code

Create index.js:

// index.js
const { Client, GatewayIntentBits, Collection } = require("discord.js");
const { Manager } = require("moonlink.js");
const fs = require("fs");
const config = require("./config.json");

// 1) Discord client
const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildVoiceStates,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

// 2) Shared state
client.commands = new Collection();
client.config = config;

// 3) Moonlink Manager
client.manager = new Manager({
  nodes: config.lavalink.nodes,

  // Keep node selection / failover / defaults in config.json
  options: config.moonlink?.options,

  // Required when you are NOT using a Connector
  send: (guildId, payload) => {
    const guild = client.guilds.cache.get(guildId);
    if (guild) guild.shard.send(payload);
  },
});

// 4) Load commands
for (const file of fs.readdirSync("./commands").filter((f) => f.endsWith(".js"))) {
  const command = require(`./commands/${file}`);

  // Register command name
  client.commands.set(command.name, command);

  // Register aliases (optional)
  if (Array.isArray(command.aliases)) {
    for (const alias of command.aliases) {
      client.commands.set(alias, command);
    }
  }
}

// 5) Load events
for (const file of fs.readdirSync("./events").filter((f) => f.endsWith(".js"))) {
  const event = require(`./events/${file}`);

  if (event.once) {
    client.once(event.name, (...args) => event.execute(...args, client));
  } else {
    client.on(event.name, (...args) => event.execute(...args, client));
  }
}

// 6) Moonlink debug (config-driven)
if (client.config?.debug) {
  client.manager.on("debug", (msg) => console.log(`[Moonlink] ${msg}`));
}

// 7) Node lifecycle logs
client.manager.on("nodeConnect", (node) => {
  console.log(`Node ${node.identifier} connected`);
});

client.manager.on("nodeDisconnect", (node) => {
  console.log(`Node ${node.identifier} disconnected`);
});

client.manager.on("nodeError", (node, error) => {
  console.error(`Node ${node.identifier} error:`, error);
});

// 8) Playback logs
client.manager.on("trackStart", (player, track) => {
  const channel = client.channels.cache.get(player.textChannelId);
  if (channel) channel.send(`Now playing: **${track.title}**`);
});

client.manager.on("queueEnd", (player) => {
  const channel = client.channels.cache.get(player.textChannelId);
  if (channel) channel.send("Queue ended. Disconnecting soon if idle.");

  // Basic idle cleanup
  setTimeout(() => {
    if (!player.playing && player.queue.size === 0) {
      player.destroy();
      if (channel) channel.send("Disconnected due to inactivity.");
    }
  }, 30000);
});

// 9) Raw voice packets (required without a Connector)
client.on("raw", (packet) => client.manager.packetUpdate(packet));

// 10) Login
client.login(config.token);

3) Why send() exists

Moonlink must be able to send voice state payloads to the Discord gateway.

That’s what send(guildId, payload) is for: it tells Moonlink how to reach Discord’s gateway layer through your library.

4) Why the raw event is required

Discord voice uses low-level gateway events (VOICE_STATE_UPDATE, VOICE_SERVER_UPDATE).

Moonlink needs those packets to complete the handshake. If you don’t pass them to manager.packetUpdate(...), the bot may join a channel but never actually stream.

Create events/ready.js:

// events/ready.js
module.exports = {
  name: "clientReady",
  once: true,
  async execute(client) {
    console.log(`Logged in as ${client.user.tag}`);

    // Connect nodes, prepare sessions, start internal services
    await client.manager.init(client.user.id);
    console.log("Moonlink Manager initialized");
  },
};

6) events/messageCreate.js — command dispatcher

Create events/messageCreate.js:

// events/messageCreate.js
module.exports = {
  name: "messageCreate",
  async execute(message, client) {
    const prefix = client.config?.prefix || "!";

    if (message.author.bot) return;
    if (!message.guild) return;
    if (!message.content.startsWith(prefix)) return;

    const args = message.content.slice(prefix.length).trim().split(/ +/g);
    const commandName = args.shift()?.toLowerCase();
    if (!commandName) return;

    const command = client.commands.get(commandName);
    if (!command) return;

    try {
      await command.execute(message, args, client);
    } catch (error) {
      console.error(error);
      await message.reply("There was an error executing that command.");
    }
  },
};

Full project snapshot (so far)

At this point, your project should look like this:

my-moonlink-bot/
├─ commands/
│  └─ (we will add these next)
├─ events/
│  ├─ ready.js
│  └─ messageCreate.js
├─ config.json
├─ index.js
└─ package.json

Next step

Now your bot boots, connects to nodes, and is ready to execute commands.

Next, we’ll implement the full music command set (play/queue/filters/etc.) with consistent validations.