ExamplesDiscord Bot

Discord Bot Example

A complete Discord.js bot that lets server members sign XRPL transactions with any supported wallet via slash commands.

💡

This example uses polling for simplicity. For production bots with many users, use webhooks to avoid holding interaction responses open.

Prerequisites

Install dependencies

npm install discord.js @xrplrequest/sdk
npm install -D typescript @types/node tsx

Full example

// bot.ts
import {
  Client,
  GatewayIntentBits,
  REST,
  Routes,
  SlashCommandBuilder,
  ButtonBuilder,
  ButtonStyle,
  ActionRowBuilder,
  ComponentType,
} from 'discord.js';
import { XRPLRequest } from '@xrplrequest/sdk';
 
const DISCORD_TOKEN = process.env.DISCORD_TOKEN!;
const CLIENT_ID = process.env.DISCORD_CLIENT_ID!;
const XRPL_API_KEY = process.env.XRPL_REQUEST_API_KEY!;
 
const discord = new Client({ intents: [GatewayIntentBits.Guilds] });
const xrpl = new XRPLRequest({ apiKey: XRPL_API_KEY });
 
// Register slash commands
const commands = [
  new SlashCommandBuilder()
    .setName('tip')
    .setDescription('Tip someone XRP')
    .addStringOption((o) =>
      o.setName('destination').setDescription('Destination XRPL address').setRequired(true)
    )
    .addIntegerOption((o) =>
      o.setName('amount').setDescription('Amount in XRP').setRequired(true)
    ),
 
  new SlashCommandBuilder()
    .setName('verify')
    .setDescription('Verify ownership of your XRPL wallet'),
].map((c) => c.toJSON());
 
const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);
await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands });
 
// Handle interactions
discord.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
 
  if (interaction.commandName === 'tip') {
    await handleTip(interaction);
  } else if (interaction.commandName === 'verify') {
    await handleVerify(interaction);
  }
});
 
async function handleTip(interaction: any) {
  const destination = interaction.options.getString('destination', true);
  const amountXrp = interaction.options.getInteger('amount', true);
 
  // Create the signing payload
  const payload = await xrpl.payloads.create({
    type: 'signAndSubmit',
    transaction: {
      TransactionType: 'Payment',
      Destination: destination,
      Amount: String(amountXrp * 1_000_000), // XRP → drops
    },
    options: {
      expiresIn: 300,
      customInstructions: `Tip of ${amountXrp} XRP via Discord`,
    },
  });
 
  // Build a link button
  const button = new ButtonBuilder()
    .setLabel('Sign Transaction')
    .setStyle(ButtonStyle.Link)
    .setURL(payload.signingUrl);
 
  const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
 
  // Defer so we have time to poll (up to 15 min with followUp)
  await interaction.reply({
    content: `💸 Please sign to send **${amountXrp} XRP** to \`${destination}\`\n\nExpires in 5 minutes.`,
    components: [row],
    ephemeral: true,
  });
 
  // Poll for result
  const result = await xrpl.payloads.poll(payload.uuid, {
    timeout: 300_000,
    interval: 3_000,
  });
 
  if (result.status === 'signed') {
    await interaction.editReply({
      content: `✅ **Sent!** ${amountXrp} XRP → \`${destination}\`\nTx: \`${result.txHash}\``,
      components: [],
    });
  } else if (result.status === 'rejected') {
    await interaction.editReply({
      content: '❌ Transaction rejected.',
      components: [],
    });
  } else {
    await interaction.editReply({
      content: '⏱ Signing request expired.',
      components: [],
    });
  }
}
 
async function handleVerify(interaction: any) {
  const payload = await xrpl.payloads.create({
    type: 'connect',
    options: {
      expiresIn: 180,
      customInstructions: 'Verify your XRPL wallet ownership for this Discord server.',
    },
  });
 
  const button = new ButtonBuilder()
    .setLabel('Verify Wallet')
    .setStyle(ButtonStyle.Link)
    .setURL(payload.signingUrl);
 
  const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
 
  await interaction.reply({
    content: '🔐 Click below to verify your XRPL wallet. Expires in 3 minutes.',
    components: [row],
    ephemeral: true,
  });
 
  const result = await xrpl.payloads.poll(payload.uuid, {
    timeout: 180_000,
    interval: 3_000,
  });
 
  if (result.status === 'signed') {
    await interaction.editReply({
      content: `✅ Wallet verified! Your address: \`${result.signerAddress}\``,
      components: [],
    });
  } else {
    await interaction.editReply({
      content: '❌ Verification cancelled or expired.',
      components: [],
    });
  }
}
 
discord.login(DISCORD_TOKEN);

Environment variables

DISCORD_TOKEN=your-bot-token
DISCORD_CLIENT_ID=your-application-id
XRPL_REQUEST_API_KEY=xrplr_live_...

Running the bot

npx tsx bot.ts

Using webhooks instead of polling

For bots with many concurrent users, polling ties up resources. Use webhooks to handle results asynchronously:

// 1. Create the payload and post the link (same as above)
const payload = await xrpl.payloads.create({ ... });
await interaction.reply({ content: payload.signingUrl });
 
// Store the mapping: payload.uuid → Discord interaction token
pendingPayloads.set(payload.uuid, interaction.token);
 
// 2. In your webhook handler, look up the interaction and edit it
app.post('/xrplrequest-webhook', async (req, res) => {
  // ... verify signature ...
  const { event, payload } = req.body;
  const token = pendingPayloads.get(payload.uuid);
  if (!token) return res.sendStatus(200);
 
  const message = event === 'payload.signed'
    ? `✅ Tx: \`${payload.txHash}\``
    : event === 'payload.rejected' ? '❌ Rejected.' : '⏱ Expired.';
 
  await rest.patch(Routes.webhookMessage(CLIENT_ID, token, '@original'), {
    body: { content: message, components: [] },
  });
 
  res.sendStatus(200);
});

Discord interaction tokens are valid for 15 minutes. If your payload TTL exceeds that, store enough state to send a DM or channel message instead.