📤 Bulk DM - XActions

Messaging scripts/twitter
v1.0.0 Updated: 2026-07-20 273 lines by @nichxbt

Send a personalized DM to a list of users from one message template.

⚠️ Mass DMing can get your account rate-limited or restricted. Keep the list

How to Use

  1. Go to x.com/messages
  2. Open the browser console (F12 or Cmd+Option+I -> Console)
  3. Edit CONFIG.usernames and CONFIG.message at the top of the script
  4. Paste this entire script and press Enter

Configuration Options

OptionDefaultDescription
maxDMs10hard cap on DMs sent this run
skipExistingtrueskip anyone you already have a conversation with
minDelay2500025s
maxDelay4500045s

Default Configuration

const CONFIG = {
    // Recipients (with or without the leading @)
    usernames: [
      // 'username1',
      // 'username2',
    ],

    // Message template. Placeholders:
    //   {username} -> the @handle (lowercased, no @)
    //   {name}     -> the recipient's display name (falls back to handle)
    message: `Hey {name}! 👋 Thanks for connecting.`,

    // Safety limits
    maxDMs: 10,            // hard cap on DMs sent this run
    skipExisting: true,    // skip anyone you already have a conversation with

    // Timing (randomized between min and max, per DM)
    minDelay: 25000,       // 25s
    maxDelay: 45000        // 45s
  };

Full Script

Copy and paste this entire script into your browser DevTools console on x.com.

// Copyright (c) 2024-2026 nich (@nichxbt). Licensed under the Apache License, Version 2.0.
/**
 * ============================================
 * 💬 Bulk DM - XActions
 * ============================================
 *
 * @name         bulk-dm
 * @description  Send a personalized DM to a list of users from one message template.
 * @author       nichxbt
 * @version      1.0.0
 * @date         2026-07-20
 * @website      https://xactions.app
 *
 * Usage:
 *   1. Go to x.com/messages
 *   2. Open the browser console (F12 or Cmd+Option+I -> Console)
 *   3. Edit CONFIG.usernames and CONFIG.message at the top of the script
 *   4. Paste this entire script and press Enter
 *
 * Example:
 *   usernames: ['jack', 'nichxbt'], message: 'Hey {name}! Loved your recent posts.'
 *   The script opens a new DM to @jack, types the message with {name} filled in
 *   from their display name, sends it, waits, then does @nichxbt. It skips anyone
 *   you already have a conversation with (skipExisting) and stops at maxDMs.
 *
 * ⚠️ Mass DMing can get your account rate-limited or restricted. Keep the list
 *    small, keep delays high, and only message people who want to hear from you.
 *
 * ============================================
 */

(async function bulkDM() {
  'use strict';

  // ============================================
  // 📝 CONFIGURATION - Customize these options
  // ============================================
  const CONFIG = {
    // Recipients (with or without the leading @)
    usernames: [
      // 'username1',
      // 'username2',
    ],

    // Message template. Placeholders:
    //   {username} -> the @handle (lowercased, no @)
    //   {name}     -> the recipient's display name (falls back to handle)
    message: `Hey {name}! 👋 Thanks for connecting.`,

    // Safety limits
    maxDMs: 10,            // hard cap on DMs sent this run
    skipExisting: true,    // skip anyone you already have a conversation with

    // Timing (randomized between min and max, per DM)
    minDelay: 25000,       // 25s
    maxDelay: 45000        // 45s
  };

  // ============================================
  // 🔧 SELECTORS
  // ============================================
  const SELECTORS = {
    newMessageBtn: '[data-testid="NewDM_Button"]',
    searchInput: '[data-testid="searchPeople"]',
    userCell: '[data-testid="UserCell"]',
    typeaheadUser: '[data-testid="TypeaheadUser"]',
    nextButton: '[data-testid="nextButton"]',
    messageInput: '[data-testid="dmComposerTextInput"]',
    sendButton: '[data-testid="dmComposerSendButton"]',
    conversation: '[data-testid="conversation"]'
  };

  // ============================================
  // 🛠️ HELPERS
  // ============================================
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  const randomDelay = () => {
    const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
    return sleep(delay);
  };

  const cleanHandle = (u) => String(u || '').trim().replace(/^@/, '').toLowerCase();

  const fillTemplate = (tpl, username, name) =>
    tpl.split('{username}').join(username).split('{name}').join(name || username);

  const log = {
    info: (msg) => console.log(`ℹ️ ${msg}`),
    success: (msg) => console.log(`✅ ${msg}`),
    warning: (msg) => console.log(`⚠️ ${msg}`),
    error: (msg) => console.log(`❌ ${msg}`),
    progress: (current, total) => console.log(`📊 Progress: ${current}/${total} DMs sent`)
  };

  // ============================================
  // 🎯 STATE
  // ============================================
  const stats = { sent: 0, skipped: 0, notFound: 0, errors: 0 };

  // Stop switch: run window.stopBulkDM() from the console to abort after the
  // recipient currently being processed.
  let stopped = false;
  window.stopBulkDM = () => {
    stopped = true;
    log.warning('Stop requested. Finishing the current DM, then exiting.');
  };

  console.log(`
╔══════════════════════════════════════════════════════════╗
║  💬 BULK DM - XActions                                   ║
║  👤 Author: nichxbt                                      ║
║  🌐 https://xactions.app                                 ║
╚══════════════════════════════════════════════════════════╝
  `);
  log.warning('Use responsibly. Mass DMing can get your account restricted.');
  log.info('To stop early: window.stopBulkDM()');

  // Page guard
  if (!/\/messages/.test(window.location.pathname)) {
    log.error('Not on the Messages page. Go to https://x.com/messages and re-run.');
    return stats;
  }

  const recipients = CONFIG.usernames.map(cleanHandle).filter(Boolean);
  if (recipients.length === 0) {
    log.error('CONFIG.usernames is empty. Add at least one handle and re-run.');
    return stats;
  }

  log.info(`Recipients: ${recipients.length}`);
  log.info(`Max DMs this run: ${CONFIG.maxDMs}`);

  // ============================================
  // 📬 SEND ONE DM
  // ============================================
  const sendOne = async (handle) => {
    // Open the new-message composer
    const newBtn = document.querySelector(SELECTORS.newMessageBtn);
    if (!newBtn) {
      log.error('New Message button not found. Make sure the Messages inbox is open.');
      stats.errors++;
      return false;
    }
    newBtn.click();
    await sleep(1500);

    const searchInput = document.querySelector(SELECTORS.searchInput);
    if (!searchInput) {
      log.error('People search box not found.');
      stats.errors++;
      return false;
    }

    // Type the handle (contenteditable-safe via execCommand)
    searchInput.focus();
    document.execCommand('insertText', false, handle);
    await sleep(2200);

    // Match the exact @handle so a prefix like "john" can't select "johnny".
    const cells = document.querySelectorAll(`${SELECTORS.typeaheadUser}, ${SELECTORS.userCell}`);
    const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const handleRe = new RegExp('@' + escaped + '(?![a-zA-Z0-9_])', 'i');
    let matchCell = null;
    for (const cell of cells) {
      if (handleRe.test(cell.textContent)) { matchCell = cell; break; }
    }
    if (!matchCell) {
      log.error(`@${handle} not found in search results.`);
      stats.notFound++;
      return false;
    }

    // Pull the display name for the {name} placeholder before we navigate away.
    let displayName = handle;
    const nameSpan = matchCell.querySelector('span');
    if (nameSpan && nameSpan.textContent.trim()) displayName = nameSpan.textContent.trim();

    matchCell.click();
    await sleep(1200);

    // Some flows require a "Next" click to open the conversation.
    const nextBtn = document.querySelector(SELECTORS.nextButton);
    if (nextBtn && !nextBtn.disabled) {
      nextBtn.click();
      await sleep(1500);
    }

    const msgInput = document.querySelector(SELECTORS.messageInput);
    if (!msgInput) {
      log.error('DM text input not found.');
      stats.errors++;
      return false;
    }

    const message = fillTemplate(CONFIG.message, handle, displayName);
    msgInput.focus();
    document.execCommand('insertText', false, message);
    await sleep(1000);

    const sendBtn = document.querySelector(SELECTORS.sendButton);
    if (!sendBtn || sendBtn.getAttribute('aria-disabled') === 'true' || sendBtn.disabled) {
      log.error(`Send button unavailable for @${handle}. DMs may be closed for this account.`);
      stats.errors++;
      return false;
    }
    sendBtn.click();
    await sleep(1500);

    log.success(`Sent to @${handle} (${displayName})`);
    return true;
  };

  // ============================================
  // 🔁 MAIN LOOP
  // ============================================
  for (let i = 0; i < recipients.length; i++) {
    if (stopped) { log.warning('Stopped by user.'); break; }
    if (stats.sent >= CONFIG.maxDMs) {
      log.warning(`Reached maxDMs (${CONFIG.maxDMs}). Stopping.`);
      break;
    }

    const handle = recipients[i];

    try {
      if (CONFIG.skipExisting) {
        const openConvos = document.querySelectorAll(SELECTORS.conversation);
        const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const existsRe = new RegExp('@' + escaped + '(?![a-zA-Z0-9_])', 'i');
        const already = Array.from(openConvos).some(c => existsRe.test(c.textContent));
        if (already) {
          log.info(`Skipping @${handle} (existing conversation).`);
          stats.skipped++;
          continue;
        }
      }

      const ok = await sendOne(handle);
      if (ok) {
        stats.sent++;
        log.progress(stats.sent, Math.min(CONFIG.maxDMs, recipients.length));
      }
    } catch (error) {
      log.error(`Error DMing @${handle}: ${error.message}`);
      stats.errors++;
    }

    // Delay before the next recipient (skip the wait after the final one).
    const more = !stopped && stats.sent < CONFIG.maxDMs && i < recipients.length - 1;
    if (more) {
      log.info(`Waiting before the next DM...`);
      await randomDelay();
    }
  }

  // ============================================
  // 📊 SUMMARY
  // ============================================
  console.log(`
╔══════════════════════════════════════════════════════════╗
║  📊 BULK DM - COMPLETE                                   ║
╠══════════════════════════════════════════════════════════╣
║  ✅ Sent:              ${String(stats.sent).padEnd(32)}║
║  ⏭️  Skipped (exists):  ${String(stats.skipped).padEnd(32)}║
║  ❓ Not found:         ${String(stats.notFound).padEnd(32)}║
║  ❌ Errors:            ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
  `);
  log.success('Script completed! by nichxbt');
  return stats;
})();

⚡ More XActions Scripts

Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.

Browse All Scripts