Goal: Build a Roblox verification system using Dock Systems API — users link their Roblox account to gain access to the server.
Dock Systems (docksys.xyz) is a third-party API that links Discord accounts to Roblox accounts. When a user connects their Roblox on the Dock Systems website, the API can return their Roblox ID and username given their Discord ID. This is how the bot knows who someone is on Roblox without asking them to type their name.
The DOCK_API key in config.json is your private authorization token for Dock Systems. It gets loaded onto client.config when the bot starts, so every file can access it via client.config.DOCK_API. Never hardcode this key directly in your code — always read it from config.
The getRobloxInfo() utility makes two separate requests:
users.roblox.com. This is a public API, no key needed.Both calls are wrapped in a try/catch so if either fails, it returns an error object instead of crashing the bot.
deferReply({ ephemeral: true }) — used in startVerify.js. This tells Discord "I'm thinking, send a private placeholder." The user sees a loading state only visible to them.
deferUpdate() — used in continueVerify.js. This acknowledges a button click without replacing the original message yet. You then call editReply() to update it. Required because API calls take time and Discord times out interactions after 3 seconds.
This system uses Discord's Components V2 — a newer layout system using ContainerBuilder, TextDisplayBuilder, SectionBuilder, and MediaGalleryBuilder. You must pass the MessageFlags.IsComponentsV2 flag on every message that uses these or Discord will reject it.
In verify.js, the panel uses a SectionBuilder with .setButtonAccessory(). This places a button inline beside text rather than below it in an ActionRow — a V2-exclusive layout feature that looks much cleaner than a standard row.
Each button has a customId that your bot listens for. The flow is:
The button IDs must match exactly what you pass to setCustomId(). One typo = button does nothing.
In continueVerify.js, two role IDs are defined: ADD_ROLE_ID (the verified role) and REMOVE_ROLE_ID (the unverified/gate role). On success, the bot adds one and removes the other. Both use .catch(() => {}) to silently ignore errors if the user already has or doesn't have the role — preventing crashes.
After verifying, the bot sends an EmbedBuilder to a private log channel. This uses Math.floor(Date.now() / 1000) to convert JavaScript's millisecond timestamps into Discord's Unix format (<t:...:R>) for relative timestamps like "5 minutes ago." The "View Profile" button is a Link button — link buttons don't have customIds, they just open a URL.
Add your Dock Systems API key to config.json. Your bot should load this file on startup and attach it to client.config.
Build docksystem.js — the utility that calls the Dock Systems API and the Roblox API. Export getRobloxInfo() so any file can import and use it.
Create verify.js as a prefix command. When triggered, it deletes the command message and sends the verification panel to the channel using Components V2 with the start-verify button.
In startVerify.js, defer the reply as ephemeral, call getRobloxInfo(), then show the user their linked account with Continue / Change Account buttons.
// startVerify.js — key pattern
await interaction.deferReply({ ephemeral: true });
const data = await getRobloxInfo(interaction.user.id, interaction, client);
if (!data || data.error) {
return interaction.reply({ content: "Couldn't find your Roblox account.", flags: MessageFlags.Ephemeral });
}
// Show confirmation panel with Continue / Change Account buttons
await interaction.reply({ flags: MessageFlags.IsComponentsV2, ephemeral: true, components });
In continueVerify.js, defer the update, re-fetch their info, add/remove roles, edit the reply to show success, then send a log embed to your log channel.
// continueVerify.js — key pattern
await interaction.deferUpdate();
await member.roles.add(ADD_ROLE_ID).catch(() => {});
await member.roles.remove(REMOVE_ROLE_ID).catch(() => {});
await interaction.editReply({ flags: MessageFlags.IsComponentsV2, components });
// Send log embed to log channel
await logChannel.send({ embeds: [logEmbed], components: [actionRow] });
{
"DOCK_API": "YOUR_DOCK_API_KEY_HERE"
}
const axios = require("axios");
async function getRobloxInfo(userId, interaction, client) {
try {
// Step 1: Discord ID → Roblox ID via Dock Systems
const res = await axios.get(
"https://api.docksys.xyz/api/v1/public/discord-to-roblox",
{
headers: {
Authorization: `Bearer ${client.config.DOCK_API}`,
},
params: {
discordId: userId,
guildId: interaction.guild.id,
},
},
);
if (!res.data || !res.data.data || !res.data.data.robloxId) {
return { error: "No Roblox ID found for this Discord user." };
}
const robloxId = res.data.data.robloxId;
// Step 2: Roblox ID → Username via public Roblox API
const res2 = await axios.get(
`https://users.roblox.com/v1/users/${robloxId}`,
);
const username = res2.data.name;
return { robloxId, username };
} catch (err) {
console.error("Error in getRobloxInfo:", err.response?.data || err.message);
return { error: "Failed to fetch Roblox info." };
}
}
module.exports = { getRobloxInfo };
const {
MediaGalleryBuilder,
MediaGalleryItemBuilder,
TextDisplayBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ContainerBuilder,
MessageFlags,
ButtonBuilder,
ButtonStyle,
SectionBuilder,
} = require("discord.js");
module.exports = {
name: "verify",
description: "Sends the verification panel.",
async execute(message) {
await message.delete();
const components = [
new ContainerBuilder()
.addMediaGalleryComponents(
new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(
"https://media.discordapp.net/attachments/1456184292983177349/1456768543335911424/Verification.png?ex=695990ee&is=69583f6e&hm=206f896b015af597f73dc59e475070e172d4e3b755c1b9f9b5ed48ea25e1ed59&=&format=webp&quality=lossless&width=1315&height=383"
)
)
)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent("### Verification Panel")
)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
"At **Ascend**, we use **Dock Systems** to verify users. In order to gain access to the rest of the server, you must verify your Roblox account. Please click the **verification** button below this message to get started in the verification process."
)
)
.addSeparatorComponents(
new SeparatorBuilder()
.setDivider(true)
.setSpacing(SeparatorSpacingSize.Small)
)
.addSectionComponents(
new SectionBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
"Verify using the button here:"
)
)
.setButtonAccessory(
new ButtonBuilder()
.setCustomId("start-verify")
.setLabel("Begin Verification")
.setStyle(ButtonStyle.Secondary)
)
),
];
await message.channel.send({
flags: MessageFlags.IsComponentsV2,
components,
});
},
};
const {
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
SeparatorBuilder,
SeparatorSpacingSize,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
} = require("discord.js");
const { getRobloxInfo } = require("../Utils/docksystem");
module.exports = {
customID: "start-verify",
async execute(interaction, client) {
await interaction.deferReply({ ephemeral: true });
const data = await getRobloxInfo(
interaction.user.id,
interaction,
client
);
if (!data || data.error) {
return interaction.reply({
content:
"Couldn't verify your Roblox account. Make sure your Discord is linked in Docksys.",
flags: MessageFlags.Ephemeral,
});
}
const { robloxId, username } = data;
const profileLink = `https://www.roblox.com/users/${robloxId}/profile`;
const components = [
new ContainerBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent("## Roblox Verification")
)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`You already have **[${username}](${profileLink})** linked.\n\nTo switch to a different account, click **Change Account**.\nTo continue using this account, click **Continue**.`
)
)
.addSeparatorComponents(
new SeparatorBuilder()
.setDivider(true)
.setSpacing(SeparatorSpacingSize.Small)
)
.addActionRowComponents(
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel("Change Account")
.setEmoji("<:refresh:1456476619580375041>")
.setStyle(ButtonStyle.Link)
.setURL("https://docksys.xyz/account"),
new ButtonBuilder()
.setCustomId("continue-verify")
.setLabel("Continue")
.setEmoji("<:rightarrow:1456476621157433355>")
.setStyle(ButtonStyle.Success)
)
),
];
await interaction.reply({
flags: MessageFlags.IsComponentsV2,
ephemeral: true,
components: components,
});
},
};
const {
ContainerBuilder,
TextDisplayBuilder,
MessageFlags,
EmbedBuilder,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder
} = require("discord.js");
const { getRobloxInfo } = require("../Utils/docksystem");
const ADD_ROLE_ID = "1456077337534791701"; // Verified role
const REMOVE_ROLE_ID = "1456077351199838282"; // Unverified/gate role
const LOG_CHANNEL_ID = "1456770628156653850"; // Where to send logs
module.exports = {
customID: "continue-verify",
async execute(interaction, client) {
if (!interaction.deferred && !interaction.replied) {
await interaction.deferUpdate();
}
const { robloxId, username, error } = await getRobloxInfo(
interaction.user.id,
interaction,
client
);
if (error) {
return interaction.editReply({
content: "Verification failed.",
components: [],
});
}
const member = await interaction.guild.members.fetch(interaction.user.id);
// Add verified role, remove unverified role
await member.roles.add(ADD_ROLE_ID).catch(() => { });
await member.roles.remove(REMOVE_ROLE_ID).catch(() => { });
const profileLink = `https://www.roblox.com/users/${robloxId}/profile`;
const components = [
new ContainerBuilder()
.addTextDisplayComponents(
new TextDisplayBuilder().setContent("### Verification Successful")
)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`Verified under **[${username}](${profileLink})**`
)
),
];
await interaction.editReply({
flags: MessageFlags.IsComponentsV2,
components,
});
// Build and send log embed
const logEmbed = new EmbedBuilder()
.setTitle("New Account Verification")
.setDescription(
`**${interaction.user.username}** has **successfully** linked their Roblox account.`
)
.addFields(
{ name: "**Joined**", value: ``, inline: true },
{ name: "**Verified At**", value: ``, inline: true },
{ name: "**Roblox Username**", value: `\`${username}\``, inline: true },
{ name: "**Roblox ID**", value: `\`${robloxId.toString()}\``, inline: true },
{ name: "**Discord User**", value: `\`${interaction.user.id}\``, inline: true },
{ name: "**Discord User**", value: `<@${interaction.user.id}>`, inline: true }
)
.setImage('https://media.discordapp.net/attachments/1456184292983177349/1456764244245876918/Footer.png?ex=69598ced&is=69583b6d&hm=94f2070aea13e011c2932507cec5ca3360d4ad6847b2c2d496afe3043704a1a4&=&format=webp&quality=lossless&width=1315&height=71')
.setColor("#2b2d31");
const profile = new ButtonBuilder()
.setLabel("View Profile")
.setURL(profileLink)
.setStyle(ButtonStyle.Link);
const actionRow = new ActionRowBuilder().addComponents(profile);
const logChannel = interaction.guild.channels.cache.get(LOG_CHANNEL_ID);
if (logChannel) {
await logChannel.send({ embeds: [logEmbed], components: [actionRow] });
}
},
};