XActions Control Panel — Floating UI for Browser Automations
How to Use
- Navigate to x.com and log in
- Open DevTools Console (F12 or Cmd+Option+I)
- Paste
src/automation/core.jsfirst - Paste the script below and press Enter
Note: This script requires pasting src/automation/core.js first for shared utilities.
Full Script
Copy and paste this entire script into your browser DevTools console on x.com.
// XActions Control Panel — Floating UI for Browser Automations
// https://github.com/nirholas/XActions
//
// REQUIRES: Paste core.js first!
//
// This creates a draggable floating panel on x.com that lets you:
// - Start / Pause / Resume / Restart automations
// - Configure inputs (keywords, users, limits, time limits)
// - Queue multi-target tasks (like 500 on @nichxbt, then 500 on @doi)
// - Export results (JSON / CSV)
// - Share configurations as copy-paste JSON
// - See live progress & activity log
//
// by nichxbt
(() => {
if (!window.XActions?.Core) {
console.error('❌ Core module not loaded! Paste core.js first.');
return;
}
// Prevent double-inject
if (document.getElementById('xa-panel')) {
console.log('⚡ Panel already open');
return;
}
const { log, sleep, randomDelay, scrollBy, clickElement, SELECTORS, storage, rateLimit } = window.XActions.Core;
// ========================================================================
// PANEL STATE
// ========================================================================
const PANEL = {
status: 'idle', // idle | running | paused | done | error
taskIndex: 0, // current task in queue
actionCount: 0, // total actions this session
startTime: null,
pauseResolve: null, // resolve function when unpausing
abortController: null, // AbortController for current run
logs: [],
results: [], // collected results for export
};
const TASK_QUEUE = []; // Array of task objects
// ========================================================================
// AVAILABLE AUTOMATIONS
// ========================================================================
const AUTOMATIONS = {
'like-timeline': {
label: '❤️ Like Timeline',
description: 'Like posts on a user\'s timeline or your home feed',
inputs: [
{ key: 'target', label: 'Target @username (blank = home feed)', type: 'text', default: '' },
{ key: 'limit', label: 'Max likes', type: 'number', default: 50 },
{ key: 'keywords', label: 'Only posts with keywords (comma-sep, blank = all)', type: 'text', default: '' },
{ key: 'skipReplies', label: 'Skip replies', type: 'checkbox', default: true },
{ key: 'alsoRetweet', label: 'Also retweet', type: 'checkbox', default: false },
{ key: 'minDelay', label: 'Min delay (ms)', type: 'number', default: 2000 },
{ key: 'maxDelay', label: 'Max delay (ms)', type: 'number', default: 5000 },
],
run: runLikeTimeline,
},
'follow-engagers': {
label: '👥 Follow Engagers',
description: 'Follow people who engage with a target account\'s posts',
inputs: [
{ key: 'target', label: 'Target @username', type: 'text', default: '' },
{ key: 'limit', label: 'Max follows', type: 'number', default: 30 },
{ key: 'bioKeywords', label: 'Bio must contain (comma-sep, blank = all)', type: 'text', default: '' },
{ key: 'minDelay', label: 'Min delay (ms)', type: 'number', default: 2000 },
{ key: 'maxDelay', label: 'Max delay (ms)', type: 'number', default: 5000 },
],
run: runFollowEngagers,
},
'unfollow': {
label: '🚫 Smart Unfollow',
description: 'Unfollow accounts that don\'t follow you back',
inputs: [
{ key: 'limit', label: 'Max unfollows', type: 'number', default: 50 },
{ key: 'keepVerified', label: 'Keep verified accounts', type: 'checkbox', default: true },
{ key: 'keepKeywords', label: 'Keep if bio contains (comma-sep)', type: 'text', default: '' },
{ key: 'minDelay', label: 'Min delay (ms)', type: 'number', default: 2000 },
{ key: 'maxDelay', label: 'Max delay (ms)', type: 'number', default: 5000 },
],
run: runSmartUnfollow,
},
'scrape-followers': {
label: '📋 Scrape Followers',
description: 'Collect follower usernames, bios, and stats',
inputs: [
{ key: 'target', label: 'Target @username', type: 'text', default: '' },
{ key: 'limit', label: 'Max to collect', type: 'number', default: 200 },
],
run: runScrapeFollowers,
},
'algo-builder': {
label: '🧠 Algorithm Builder',
description: '24/7 niche algorithm builder — search, engage, follow, comment (LLM)',
inputs: [
{ key: 'keywords', label: 'Niche keywords (comma-sep)', type: 'text', default: 'web3 builder, crypto alpha' },
{ key: 'persona', label: 'Your persona (for LLM comments)', type: 'text', default: 'a crypto & web3 builder' },
{ key: 'bioKeywords', label: 'Bio filter keywords (comma-sep, blank = all)', type: 'text', default: 'crypto, web3, defi, builder' },
{ key: 'targetAccounts', label: 'Target @accounts (comma-sep)', type: 'text', default: '' },
{ key: 'llmApiKey', label: 'OpenRouter API key (blank = no LLM)', type: 'text', default: '' },
{ key: 'sessionMinutes', label: 'Session length (min)', type: 'number', default: 30 },
],
run: runAlgoBuilder,
},
};
// ========================================================================
// CSS
// ========================================================================
const CSS = `
#xa-panel{position:fixed;top:60px;right:16px;width:380px;max-height:calc(100vh - 80px);background:#16181c;border:1px solid #2f3336;border-radius:16px;color:#e7e9ea;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;z-index:999999;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.6);font-size:13px;overflow:hidden}
#xa-panel *{box-sizing:border-box;margin:0;padding:0}
#xa-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#000;cursor:grab;user-select:none;border-bottom:1px solid #2f3336}
#xa-panel-header:active{cursor:grabbing}
.xa-title{font-weight:700;font-size:15px;display:flex;align-items:center;gap:6px}
.xa-title span{color:#1d9bf0}
.xa-close{background:none;border:none;color:#71767b;font-size:18px;cursor:pointer;padding:4px 8px;border-radius:50%;transition:all .15s}
.xa-close:hover{background:#2f3336;color:#e7e9ea}
#xa-panel-body{overflow-y:auto;flex:1;padding:0}
.xa-section{padding:12px 16px;border-bottom:1px solid #2f3336}
.xa-section:last-child{border-bottom:none}
.xa-section-title{font-size:12px;font-weight:600;color:#71767b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}
.xa-select{width:100%;background:#000;border:1px solid #2f3336;border-radius:8px;color:#e7e9ea;padding:8px 12px;font-size:13px;cursor:pointer;outline:none;margin-bottom:8px}
.xa-select:focus{border-color:#1d9bf0}
.xa-select option{background:#000;color:#e7e9ea}
.xa-desc{color:#71767b;font-size:12px;margin-bottom:10px}
.xa-input-group{margin-bottom:8px}
.xa-input-group label{display:flex;align-items:center;gap:6px;color:#e7e9ea;font-size:12px;margin-bottom:4px;font-weight:500}
.xa-input-group input[type=text],.xa-input-group input[type=number]{width:100%;background:#000;border:1px solid #2f3336;border-radius:8px;color:#e7e9ea;padding:7px 10px;font-size:13px;outline:none;transition:border .15s}
.xa-input-group input:focus{border-color:#1d9bf0}
.xa-input-group input[type=checkbox]{accent-color:#1d9bf0;width:16px;height:16px;cursor:pointer}
.xa-checkbox-row{display:flex;align-items:center;gap:8px;padding:4px 0}
.xa-checkbox-row label{cursor:pointer;font-size:13px;color:#e7e9ea}
.xa-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;border-radius:9999px;border:none;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;white-space:nowrap}
.xa-btn:disabled{opacity:.4;cursor:not-allowed}
.xa-btn-primary{background:#1d9bf0;color:#fff}.xa-btn-primary:hover:not(:disabled){background:#1a8cd8}
.xa-btn-danger{background:#f4212e;color:#fff}.xa-btn-danger:hover:not(:disabled){background:#c91a25}
.xa-btn-outline{background:transparent;border:1px solid #2f3336;color:#e7e9ea}.xa-btn-outline:hover:not(:disabled){background:#2f3336}
.xa-btn-sm{padding:5px 12px;font-size:12px}
.xa-btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.xa-status{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;font-size:13px;font-weight:600}
.xa-status-idle{background:#2f3336;color:#71767b}
.xa-status-running{background:rgba(29,155,240,.15);color:#1d9bf0}
.xa-status-paused{background:rgba(255,212,0,.15);color:#ffd400}
.xa-status-done{background:rgba(0,186,124,.15);color:#00ba7c}
.xa-status-error{background:rgba(244,33,46,.15);color:#f4212e}
.xa-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.xa-dot-idle{background:#71767b}
.xa-dot-running{background:#1d9bf0;animation:xa-pulse 1.5s infinite}
.xa-dot-paused{background:#ffd400}
.xa-dot-done{background:#00ba7c}
.xa-dot-error{background:#f4212e}
@keyframes xa-pulse{0%,100%{opacity:1}50%{opacity:.3}}
.xa-progress{margin-top:6px}
.xa-progress-bar{height:4px;background:#2f3336;border-radius:2px;overflow:hidden}
.xa-progress-fill{height:100%;background:#1d9bf0;border-radius:2px;transition:width .3s}
.xa-progress-text{display:flex;justify-content:space-between;font-size:11px;color:#71767b;margin-top:4px}
.xa-log{max-height:140px;overflow-y:auto;background:#000;border:1px solid #2f3336;border-radius:8px;padding:8px;font-family:"Cascadia Code",Consolas,monospace;font-size:11px;line-height:1.5}
.xa-log-entry{color:#71767b;word-break:break-word}
.xa-log-entry.success{color:#00ba7c}
.xa-log-entry.error{color:#f4212e}
.xa-log-entry.warning{color:#ffd400}
.xa-log-entry.action{color:#1d9bf0}
.xa-queue-item{display:flex;align-items:center;gap:8px;padding:6px 10px;background:#000;border:1px solid #2f3336;border-radius:8px;margin-bottom:4px;font-size:12px}
.xa-queue-item.active{border-color:#1d9bf0;background:rgba(29,155,240,.08)}
.xa-queue-item.done{opacity:.5}
.xa-queue-num{width:20px;height:20px;border-radius:50%;background:#2f3336;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}
.xa-queue-item.active .xa-queue-num{background:#1d9bf0;color:#fff}
.xa-queue-item.done .xa-queue-num{background:#00ba7c;color:#fff}
.xa-queue-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.xa-queue-remove{background:none;border:none;color:#71767b;cursor:pointer;font-size:14px;padding:2px 4px}
.xa-queue-remove:hover{color:#f4212e}
.xa-time-limit{display:flex;align-items:center;gap:8px}
.xa-time-limit input{width:70px}
.xa-time-limit span{color:#71767b;font-size:12px}
.xa-tabs{display:flex;border-bottom:1px solid #2f3336}
.xa-tab{flex:1;padding:10px;text-align:center;font-size:12px;font-weight:600;color:#71767b;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
.xa-tab:hover{color:#e7e9ea;background:rgba(255,255,255,.03)}
.xa-tab.active{color:#1d9bf0;border-bottom-color:#1d9bf0}
.xa-tab-content{display:none}.xa-tab-content.active{display:block}
.xa-minimize #xa-panel-body,.xa-minimize .xa-tabs{display:none}
.xa-minimize{max-height:48px!important}
`;
// ========================================================================
// PANEL HTML
// ========================================================================
function buildHTML() {
return `
<div id="xa-panel-header">
<div class="xa-title"><span>⚡</span> XActions Panel</div>
<div style="display:flex;gap:4px">
<button class="xa-close" id="xa-minimize" title="Minimize">─</button>
<button class="xa-close" id="xa-close-panel" title="Close">✕</button>
</div>
</div>
<div class="xa-tabs">
<div class="xa-tab active" data-tab="config">Configure</div>
<div class="xa-tab" data-tab="queue">Queue</div>
<div class="xa-tab" data-tab="log">Log</div>
</div>
<div id="xa-panel-body">
<!-- CONFIG TAB -->
<div class="xa-tab-content active" id="xa-tab-config">
<!-- Status -->
<div class="xa-section">
<div id="xa-status" class="xa-status xa-status-idle">
<div class="xa-dot xa-dot-idle"></div>
<span>Idle — configure and start</span>
</div>
<div class="xa-progress" id="xa-progress" style="display:none">
<div class="xa-progress-bar"><div class="xa-progress-fill" id="xa-progress-fill" style="width:0%"></div></div>
<div class="xa-progress-text"><span id="xa-progress-count">0/0</span><span id="xa-progress-time">0s</span></div>
</div>
</div>
<!-- Automation Selector -->
<div class="xa-section">
<div class="xa-section-title">Automation</div>
<select class="xa-select" id="xa-automation">
${Object.entries(AUTOMATIONS).map(([k, v]) => `<option value="${k}">${v.label}</option>`).join('')}
</select>
<div class="xa-desc" id="xa-auto-desc">${AUTOMATIONS[Object.keys(AUTOMATIONS)[0]].description}</div>
</div>
<!-- Dynamic Inputs -->
<div class="xa-section" id="xa-inputs-section">
<div class="xa-section-title">Settings</div>
<div id="xa-inputs"></div>
</div>
<!-- Time Limit -->
<div class="xa-section">
<div class="xa-section-title">Time Limit (optional)</div>
<div class="xa-time-limit">
<input type="number" id="xa-time-limit" min="0" value="0" class="xa-input-group" style="background:#000;border:1px solid #2f3336;border-radius:8px;color:#e7e9ea;padding:7px 10px;font-size:13px;outline:none;width:70px">
<span>minutes (0 = no limit)</span>
</div>
</div>
<!-- Controls -->
<div class="xa-section">
<div class="xa-btn-row">
<button class="xa-btn xa-btn-primary" id="xa-start">▶ Start</button>
<button class="xa-btn xa-btn-outline" id="xa-pause" disabled>⏸ Pause</button>
<button class="xa-btn xa-btn-danger xa-btn-sm" id="xa-stop" disabled>⏹ Stop</button>
</div>
<div class="xa-btn-row" style="margin-top:6px">
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-add-queue">+ Add to Queue</button>
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-export">📤 Export</button>
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-share">🔗 Share Config</button>
</div>
</div>
</div>
<!-- QUEUE TAB -->
<div class="xa-tab-content" id="xa-tab-queue">
<div class="xa-section">
<div class="xa-section-title">Task Queue</div>
<div id="xa-queue-list"></div>
<div id="xa-queue-empty" class="xa-desc" style="text-align:center;padding:16px 0">No tasks queued. Configure an automation and click "Add to Queue".</div>
<div class="xa-btn-row" style="margin-top:8px">
<button class="xa-btn xa-btn-primary xa-btn-sm" id="xa-run-queue" disabled>▶ Run Queue</button>
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-clear-queue">🗑 Clear</button>
</div>
</div>
</div>
<!-- LOG TAB -->
<div class="xa-tab-content" id="xa-tab-log">
<div class="xa-section">
<div class="xa-section-title">Activity Log</div>
<div class="xa-log" id="xa-log"></div>
<div class="xa-btn-row" style="margin-top:8px">
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-clear-log">Clear</button>
<button class="xa-btn xa-btn-outline xa-btn-sm" id="xa-export-log">Export Log</button>
</div>
</div>
</div>
</div>`;
}
// ========================================================================
// INJECT PANEL
// ========================================================================
const style = document.createElement('style');
style.textContent = CSS;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'xa-panel';
panel.innerHTML = buildHTML();
document.body.appendChild(panel);
// ========================================================================
// DRAG SUPPORT
// ========================================================================
{
const header = panel.querySelector('#xa-panel-header');
let dragging = false, offsetX = 0, offsetY = 0;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.xa-close')) return;
dragging = true;
offsetX = e.clientX - panel.getBoundingClientRect().left;
offsetY = e.clientY - panel.getBoundingClientRect().top;
panel.style.transition = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, e.clientX - offsetX));
const y = Math.max(0, Math.min(window.innerHeight - 48, e.clientY - offsetY));
panel.style.left = x + 'px';
panel.style.top = y + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => { dragging = false; panel.style.transition = ''; });
}
// ========================================================================
// DOM REFS
// ========================================================================
const $ = (sel) => panel.querySelector(sel);
const $$ = (sel) => panel.querySelectorAll(sel);
const els = {
automation: $('#xa-automation'),
desc: $('#xa-auto-desc'),
inputs: $('#xa-inputs'),
status: $('#xa-status'),
progress: $('#xa-progress'),
progressFill: $('#xa-progress-fill'),
progressCount: $('#xa-progress-count'),
progressTime: $('#xa-progress-time'),
timeLimit: $('#xa-time-limit'),
startBtn: $('#xa-start'),
pauseBtn: $('#xa-pause'),
stopBtn: $('#xa-stop'),
addQueueBtn: $('#xa-add-queue'),
exportBtn: $('#xa-export'),
shareBtn: $('#xa-share'),
queueList: $('#xa-queue-list'),
queueEmpty: $('#xa-queue-empty'),
runQueueBtn: $('#xa-run-queue'),
clearQueueBtn: $('#xa-clear-queue'),
logEl: $('#xa-log'),
clearLogBtn: $('#xa-clear-log'),
exportLogBtn: $('#xa-export-log'),
};
// ========================================================================
// PANEL LOG
// ========================================================================
function panelLog(msg, type = 'info') {
const time = new Date().toLocaleTimeString();
const entry = { time, msg, type };
PANEL.logs.push(entry);
const div = document.createElement('div');
div.className = `xa-log-entry ${type}`;
div.textContent = `[${time}] ${msg}`;
els.logEl.appendChild(div);
els.logEl.scrollTop = els.logEl.scrollHeight;
// Also log to real console
log(msg, type);
}
// ========================================================================
// STATUS UPDATES
// ========================================================================
function setStatus(status, text) {
PANEL.status = status;
els.status.className = `xa-status xa-status-${status}`;
const dot = els.status.querySelector('.xa-dot');
dot.className = `xa-dot xa-dot-${status}`;
els.status.querySelector('span').textContent = text;
els.startBtn.disabled = status === 'running';
els.pauseBtn.disabled = status !== 'running' && status !== 'paused';
els.stopBtn.disabled = status === 'idle' || status === 'done';
els.pauseBtn.textContent = status === 'paused' ? '▶ Resume' : '⏸ Pause';
}
function updateProgress(current, total) {
els.progress.style.display = 'block';
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
els.progressFill.style.width = pct + '%';
els.progressCount.textContent = `${current}/${total}`;
if (PANEL.startTime) {
const elapsed = Math.round((Date.now() - PANEL.startTime) / 1000);
const min = Math.floor(elapsed / 60);
const sec = elapsed % 60;
els.progressTime.textContent = min > 0 ? `${min}m ${sec}s` : `${sec}s`;
}
}
// ========================================================================
// INPUT RENDERING
// ========================================================================
function renderInputs() {
const key = els.automation.value;
const auto = AUTOMATIONS[key];
if (!auto) return;
els.desc.textContent = auto.description;
els.inputs.innerHTML = '';
for (const inp of auto.inputs) {
const div = document.createElement('div');
div.className = 'xa-input-group';
if (inp.type === 'checkbox') {
div.innerHTML = `
<div class="xa-checkbox-row">
<input type="checkbox" id="xa-inp-${inp.key}" ${inp.default ? 'checked' : ''}>
<label for="xa-inp-${inp.key}">${inp.label}</label>
</div>`;
} else {
div.innerHTML = `
<label for="xa-inp-${inp.key}">${inp.label}</label>
<input type="${inp.type}" id="xa-inp-${inp.key}" value="${inp.default}" placeholder="${inp.label}">`;
}
els.inputs.appendChild(div);
}
}
function getInputValues() {
const key = els.automation.value;
const auto = AUTOMATIONS[key];
const values = {};
for (const inp of auto.inputs) {
const el = $(`#xa-inp-${inp.key}`);
if (!el) continue;
if (inp.type === 'checkbox') {
values[inp.key] = el.checked;
} else if (inp.type === 'number') {
values[inp.key] = parseInt(el.value) || inp.default;
} else {
values[inp.key] = el.value;
}
}
values.timeLimit = parseInt(els.timeLimit.value) || 0;
return values;
}
// ========================================================================
// PAUSE / ABORT HELPERS
// ========================================================================
async function checkPauseAbort(signal) {
if (signal?.aborted) throw new Error('Aborted');
if (PANEL.status === 'paused') {
panelLog('⏸ Paused — click Resume to continue', 'warning');
await new Promise((resolve) => { PANEL.pauseResolve = resolve; });
if (signal?.aborted) throw new Error('Aborted');
}
}
function checkTimeLimit() {
const limit = parseInt(els.timeLimit.value) || 0;
if (limit > 0 && PANEL.startTime) {
const elapsed = (Date.now() - PANEL.startTime) / 60000;
if (elapsed >= limit) {
panelLog(`⏰ Time limit reached (${limit}m)`, 'warning');
return true;
}
}
return false;
}
// ========================================================================
// AUTOMATION RUNNERS
// ========================================================================
async function runLikeTimeline(config, signal) {
const target = config.target?.replace(/^@/, '').trim();
const limit = config.limit || 50;
const keywords = config.keywords ? config.keywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
const skipReplies = config.skipReplies !== false;
const alsoRetweet = config.alsoRetweet || false;
const minDelay = config.minDelay || 2000;
const maxDelay = config.maxDelay || 5000;
// Navigate to target
if (target) {
panelLog(`📍 Navigating to @${target}'s timeline...`, 'action');
window.location.href = `https://x.com/${target}`;
await sleep(3000);
// Wait for timeline to load
await waitForTimeline();
}
let liked = 0;
let scrolls = 0;
const maxScrolls = limit * 3; // generous scroll budget
const processed = new Set();
panelLog(`🚀 Like Timeline: ${keywords.length ? keywords.join(', ') : 'all posts'}, limit ${limit}`, 'info');
while (liked < limit && scrolls < maxScrolls) {
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tweet of tweets) {
if (liked >= limit) break;
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const tweetId = getTweetId(tweet);
if (tweetId && processed.has(tweetId)) continue;
if (tweetId) processed.add(tweetId);
// Already liked
if (tweet.querySelector(SELECTORS.unlikeButton)) continue;
// Skip replies
if (skipReplies && tweet.textContent?.includes('Replying to')) continue;
// Skip ads
if (tweet.querySelector('[data-testid="placementTracking"]')) continue;
// Keyword filter
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
if (keywords.length > 0 && !keywords.some(kw => text.toLowerCase().includes(kw))) continue;
// Like it
const likeBtn = tweet.querySelector(SELECTORS.likeButton);
if (likeBtn) {
await clickElement(likeBtn);
liked++;
PANEL.actionCount++;
PANEL.results.push({ type: 'like', tweetId, text: text.slice(0, 100), timestamp: new Date().toISOString() });
rateLimit.increment('like', 'day');
updateProgress(liked, limit);
panelLog(`❤️ Liked #${liked}: "${text.slice(0, 50)}..."`, 'success');
if (alsoRetweet) {
await sleep(800);
const rtBtn = tweet.querySelector(SELECTORS.retweetButton);
if (rtBtn) {
await clickElement(rtBtn);
await sleep(500);
const confirmRt = document.querySelector('[data-testid="retweetConfirm"]');
if (confirmRt) await clickElement(confirmRt);
panelLog(' ↳ Also retweeted', 'action');
}
}
await randomDelay(minDelay, maxDelay);
}
}
scrollBy(800);
scrolls++;
await randomDelay(1500, 2500);
}
panelLog(`✅ Done! Liked ${liked} tweets.`, 'success');
return { liked, scrolls };
}
async function runFollowEngagers(config, signal) {
const target = config.target?.replace(/^@/, '').trim();
const limit = config.limit || 30;
const bioKeywords = config.bioKeywords ? config.bioKeywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
const minDelay = config.minDelay || 2000;
const maxDelay = config.maxDelay || 5000;
if (!target) { panelLog('❌ Target username required', 'error'); return { followed: 0 }; }
panelLog(`📍 Navigating to @${target}'s followers...`, 'action');
window.location.href = `https://x.com/${target}/followers`;
await sleep(3000);
await waitForTimeline();
let followed = 0;
let scrolls = 0;
const processed = new Set();
while (followed < limit && scrolls < limit * 3) {
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const cells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of cells) {
if (followed >= limit) break;
await checkPauseAbort(signal);
const user = window.XActions.Core.extractUserFromCell(cell);
if (!user || !user.username || processed.has(user.username)) continue;
processed.add(user.username);
if (user.isFollowing) continue;
// Bio filter
if (bioKeywords.length > 0 && !bioKeywords.some(kw => (user.bio || '').toLowerCase().includes(kw))) continue;
const followBtn = cell.querySelector(SELECTORS.followButton);
if (followBtn) {
await clickElement(followBtn);
followed++;
PANEL.actionCount++;
PANEL.results.push({ type: 'follow', username: user.username, bio: user.bio, timestamp: new Date().toISOString() });
rateLimit.increment('follow', 'day');
updateProgress(followed, limit);
panelLog(`👥 Followed #${followed}: @${user.username}`, 'success');
await randomDelay(minDelay, maxDelay);
}
}
scrollBy(600);
scrolls++;
await randomDelay(1500, 2500);
}
panelLog(`✅ Done! Followed ${followed} users.`, 'success');
return { followed, scrolls };
}
async function runSmartUnfollow(config, signal) {
const limit = config.limit || 50;
const keepVerified = config.keepVerified !== false;
const keepKeywords = config.keepKeywords ? config.keepKeywords.split(',').map(k => k.trim().toLowerCase()).filter(Boolean) : [];
const minDelay = config.minDelay || 2000;
const maxDelay = config.maxDelay || 5000;
// Get current username from page
const myHandle = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]')?.getAttribute('href')?.replace('/', '') || '';
if (!myHandle) {
panelLog('📍 Navigating to your following list... (make sure you\'re logged in)', 'action');
}
const followingUrl = myHandle ? `https://x.com/${myHandle}/following` : 'https://x.com/following';
window.location.href = followingUrl;
await sleep(3000);
await waitForTimeline();
let unfollowed = 0;
let scrolls = 0;
const processed = new Set();
while (unfollowed < limit && scrolls < limit * 3) {
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const cells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of cells) {
if (unfollowed >= limit) break;
await checkPauseAbort(signal);
const user = window.XActions.Core.extractUserFromCell(cell);
if (!user || !user.username || processed.has(user.username)) continue;
processed.add(user.username);
// Skip if they follow you back
if (user.followsYou) continue;
// Keep verified
if (keepVerified && user.isVerified) continue;
// Keep keyword matches
if (keepKeywords.length > 0 && keepKeywords.some(kw => (user.bio || '').toLowerCase().includes(kw))) continue;
const unfollowBtn = cell.querySelector(SELECTORS.unfollowButton);
if (unfollowBtn) {
await clickElement(unfollowBtn);
await sleep(500);
const confirmBtn = document.querySelector(SELECTORS.confirmButton);
if (confirmBtn) await clickElement(confirmBtn);
unfollowed++;
PANEL.actionCount++;
PANEL.results.push({ type: 'unfollow', username: user.username, bio: user.bio, timestamp: new Date().toISOString() });
updateProgress(unfollowed, limit);
panelLog(`🚫 Unfollowed #${unfollowed}: @${user.username}`, 'success');
await randomDelay(minDelay, maxDelay);
}
}
scrollBy(600);
scrolls++;
await randomDelay(1500, 2500);
}
panelLog(`✅ Done! Unfollowed ${unfollowed} users.`, 'success');
return { unfollowed, scrolls };
}
async function runScrapeFollowers(config, signal) {
const target = config.target?.replace(/^@/, '').trim();
const limit = config.limit || 200;
if (!target) { panelLog('❌ Target username required', 'error'); return { collected: 0 }; }
panelLog(`📍 Navigating to @${target}'s followers...`, 'action');
window.location.href = `https://x.com/${target}/followers`;
await sleep(3000);
await waitForTimeline();
const collected = [];
let scrolls = 0;
const seen = new Set();
while (collected.length < limit && scrolls < limit * 2) {
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const cells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of cells) {
if (collected.length >= limit) break;
const user = window.XActions.Core.extractUserFromCell(cell);
if (!user || !user.username || seen.has(user.username)) continue;
seen.add(user.username);
collected.push(user);
PANEL.results.push({ type: 'scrape', ...user, timestamp: new Date().toISOString() });
updateProgress(collected.length, limit);
}
scrollBy(600);
scrolls++;
await randomDelay(1000, 2000);
if (scrolls % 10 === 0) {
panelLog(`📋 Collected ${collected.length}/${limit} followers...`, 'info');
}
}
panelLog(`✅ Done! Scraped ${collected.length} followers.`, 'success');
return { collected: collected.length, data: collected };
}
// ─── Algorithm Builder (panel-integrated) ───
async function runAlgoBuilder(config, signal) {
const keywords = (config.keywords || '').split(',').map(k => k.trim()).filter(Boolean);
const persona = config.persona || 'a knowledgeable person on Twitter';
const bioKw = (config.bioKeywords || '').split(',').map(k => k.trim()).filter(Boolean);
const targets = (config.targetAccounts || '').split(',').map(k => k.trim().replace(/^@/, '')).filter(Boolean);
const llmKey = config.llmApiKey || '';
const sessionMin = config.sessionMinutes || 30;
if (keywords.length === 0) { panelLog('❌ At least one keyword required', 'error'); return; }
panelLog(`🧠 Algorithm Builder started — ${keywords.length} keywords, LLM ${llmKey ? '✅' : '❌'}`, 'action');
const FALLBACK = [
'Solid take', 'Been thinking about this', 'This is underrated',
'Hard agree', 'More people need to see this', 'Facts',
'Interesting perspective', 'Saving this', 'Real ones know',
];
const llmComment = async (text) => {
if (!llmKey) return null;
if (Math.random() > 0.7) return null;
try {
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${llmKey}` },
body: JSON.stringify({
model: 'google/gemini-flash-1.5',
messages: [
{ role: 'system', content: `You are ${persona} on Twitter. Reply in 1-2 casual sentences. No hashtags. Be specific to the tweet.` },
{ role: 'user', content: `Reply to: "${text.substring(0, 500)}"` },
],
max_tokens: 80, temperature: 0.9,
}),
});
if (!res.ok) return null;
const d = await res.json();
return d.choices?.[0]?.message?.content?.trim()?.replace(/^["']|["']$/g, '') || null;
} catch { return null; }
};
const getComment = async (text) => (await llmComment(text)) || FALLBACK[Math.floor(Math.random() * FALLBACK.length)];
const engageFeed = async (source) => {
const maxScrolls = 8 + Math.floor(Math.random() * 8);
const seen = new Set();
let actions = 0;
for (let s = 0; s < maxScrolls; s++) {
await checkPauseAbort(signal);
if (checkTimeLimit()) return actions;
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tw of tweets) {
await checkPauseAbort(signal);
const tid = getTweetId(tw);
if (!tid || seen.has(tid)) continue;
seen.add(tid);
const info = window.XActions.Core.extractTweetInfo(tw);
if (!info?.text) continue;
// Like (35% chance)
if (Math.random() < 0.35 && !tw.querySelector('[data-testid="unlike"]')) {
const btn = tw.querySelector(SELECTORS.likeButton);
if (btn) {
await randomDelay(500, 2000);
await clickElement(btn);
actions++;
PANEL.actionCount++;
PANEL.results.push({ type: 'like', tweet: tid, source, timestamp: new Date().toISOString() });
panelLog(`❤️ Liked (${source})`, 'success');
await randomDelay(2000, 5000);
}
}
// Comment (8% chance)
if (Math.random() < 0.08 && info.text.length > 20) {
const comment = await getComment(info.text);
const replyBtn = tw.querySelector(SELECTORS.replyButton);
if (replyBtn && comment) {
await clickElement(replyBtn);
await sleep(1200);
const input = await waitForElement('[data-testid="tweetTextarea_0"]', 5000);
if (input) {
input.focus();
await sleep(300);
for (const ch of comment) { document.execCommand('insertText', false, ch); await sleep(50 + Math.random() * 40); }
await sleep(600);
const post = await waitForElement('[data-testid="tweetButton"]', 3000);
if (post) {
await clickElement(post);
await sleep(1200);
actions++;
PANEL.actionCount++;
PANEL.results.push({ type: 'comment', tweet: tid, comment, source, timestamp: new Date().toISOString() });
panelLog(`💬 "${comment.substring(0, 40)}..."`, 'success');
}
} else {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
}
await randomDelay(3000, 6000);
}
}
// Follow author (15% chance)
if (Math.random() < 0.15) {
const fBtn = tw.querySelector(SELECTORS.followButton);
if (fBtn) {
await randomDelay(500, 2000);
await clickElement(fBtn);
actions++;
PANEL.actionCount++;
panelLog(`➕ Followed (${source})`, 'success');
await randomDelay(2000, 5000);
}
}
}
scrollBy(300 + Math.floor(Math.random() * 500));
await randomDelay(1500, 4000);
if (Math.random() < 0.15) await sleep(5000 + Math.random() * 7000); // reading pause
}
return actions;
};
// ─── Main cycle loop ───
const sessionEnd = Date.now() + sessionMin * 60 * 1000;
let kwIdx = 0;
let tgtIdx = 0;
let totalActions = 0;
const cycles = ['search', 'home', 'target', 'explore', 'people', 'profile'];
let cycleIdx = 0;
while (Date.now() < sessionEnd) {
await checkPauseAbort(signal);
if (checkTimeLimit()) break;
const cycle = cycles[cycleIdx % cycles.length];
cycleIdx++;
try {
if (cycle === 'search') {
const kw = keywords[kwIdx % keywords.length]; kwIdx++;
panelLog(`🔍 Searching: "${kw}"`, 'action');
window.location.href = `https://x.com/search?q=${encodeURIComponent(kw)}&src=typed_query&f=top`;
await sleep(3000); await waitForTimeline();
totalActions += await engageFeed('search-top');
await checkPauseAbort(signal);
window.location.href = `https://x.com/search?q=${encodeURIComponent(kw)}&src=typed_query&f=live`;
await sleep(3000); await waitForTimeline();
totalActions += await engageFeed('search-latest');
} else if (cycle === 'home') {
panelLog('🏠 Browsing home feed', 'action');
window.location.href = 'https://x.com/home';
await sleep(3000); await waitForTimeline();
totalActions += await engageFeed('home');
} else if (cycle === 'target' && targets.length > 0) {
const t = targets[tgtIdx % targets.length]; tgtIdx++;
panelLog(`🎯 Visiting @${t}`, 'action');
window.location.href = `https://x.com/${t}`;
await sleep(3000); await waitForTimeline();
totalActions += await engageFeed('target');
} else if (cycle === 'explore') {
panelLog('🌍 Browsing Explore', 'action');
window.location.href = 'https://x.com/explore';
await sleep(3000);
for (let i = 0; i < 5; i++) { scrollBy(400 + Math.random() * 300); await randomDelay(1500, 3000); }
} else if (cycle === 'people') {
const kw = keywords[Math.floor(Math.random() * keywords.length)];
panelLog(`👥 Searching people: "${kw}"`, 'action');
window.location.href = `https://x.com/search?q=${encodeURIComponent(kw)}&f=user`;
await sleep(3000); await waitForTimeline();
const cells = document.querySelectorAll(SELECTORS.userCell);
let pFollows = 0;
for (const cell of cells) {
if (pFollows >= 5) break;
const user = window.XActions.Core.extractUserFromCell(cell);
if (!user?.username || user.isFollowing) continue;
if (bioKw.length > 0 && user.bio) {
const bl = user.bio.toLowerCase();
if (!bioKw.some(k => bl.includes(k.toLowerCase()))) continue;
}
const fb = cell.querySelector(SELECTORS.followButton);
if (fb) {
await randomDelay(1000, 3000);
await clickElement(fb);
pFollows++; totalActions++;
PANEL.actionCount++;
panelLog(`➕ Followed @${user.username}`, 'success');
}
}
} else if (cycle === 'profile') {
panelLog('👤 Visiting own profile', 'action');
const pLink = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
if (pLink) { await clickElement(pLink); await sleep(3000); }
for (let i = 0; i < 3; i++) { scrollBy(400); await randomDelay(1000, 2000); }
}
} catch (err) {
panelLog(`⚠️ ${err.message}`, 'warning');
}
// Inter-cycle pause
await randomDelay(3000, 8000);
updateProgress(Math.min(cycleIdx, cycles.length * 3), cycles.length * 3);
}
panelLog(`✅ Algorithm Builder done! ${totalActions} actions across ${cycleIdx} cycles`, 'success');
return { totalActions, cycles: cycleIdx };
}
// ========================================================================
// HELPERS
// ========================================================================
function getTweetId(tweet) {
const link = tweet.querySelector('a[href*="/status/"]');
if (link) {
const m = link.href.match(/status\/(\d+)/);
return m ? m[1] : null;
}
return null;
}
async function waitForTimeline() {
for (let i = 0; i < 30; i++) {
if (document.querySelector(SELECTORS.tweet) || document.querySelector(SELECTORS.userCell)) return;
await sleep(500);
}
panelLog('⚠️ Timeline took too long to load', 'warning');
}
// ========================================================================
// RUN SINGLE TASK
// ========================================================================
async function runTask(autoKey, config) {
const auto = AUTOMATIONS[autoKey];
if (!auto) { panelLog(`❌ Unknown automation: ${autoKey}`, 'error'); return; }
const controller = new AbortController();
PANEL.abortController = controller;
PANEL.startTime = Date.now();
PANEL.actionCount = 0;
setStatus('running', `Running: ${auto.label}`);
updateProgress(0, config.limit || 50);
panelLog(`▶ Starting ${auto.label}`, 'action');
try {
await auto.run(config, controller.signal);
if (PANEL.status !== 'idle') setStatus('done', `Done — ${PANEL.actionCount} actions`);
} catch (err) {
if (err.message === 'Aborted') {
panelLog('⏹ Stopped by user', 'warning');
setStatus('idle', 'Stopped');
} else {
panelLog(`❌ Error: ${err.message}`, 'error');
setStatus('error', `Error: ${err.message.slice(0, 40)}`);
}
} finally {
PANEL.abortController = null;
els.progress.style.display = 'none';
}
}
// ========================================================================
// QUEUE RUNNER
// ========================================================================
async function runQueue() {
if (TASK_QUEUE.length === 0) return;
panelLog(`📋 Running queue: ${TASK_QUEUE.length} task(s)`, 'action');
for (let i = 0; i < TASK_QUEUE.length; i++) {
PANEL.taskIndex = i;
renderQueue(); // highlight current
const task = TASK_QUEUE[i];
await runTask(task.autoKey, task.config);
task.status = 'done';
renderQueue();
// If stopped, don't continue
if (PANEL.status === 'idle' || PANEL.status === 'error') break;
// Brief pause between tasks
if (i < TASK_QUEUE.length - 1) {
panelLog('⏳ Pausing 3s between tasks...', 'info');
await sleep(3000);
}
}
panelLog(`🏁 Queue finished!`, 'success');
setStatus('done', `Queue done — ${PANEL.actionCount} total actions`);
}
// ========================================================================
// QUEUE RENDERING
// ========================================================================
function renderQueue() {
if (TASK_QUEUE.length === 0) {
els.queueList.innerHTML = '';
els.queueEmpty.style.display = 'block';
els.runQueueBtn.disabled = true;
return;
}
els.queueEmpty.style.display = 'none';
els.runQueueBtn.disabled = false;
els.queueList.innerHTML = TASK_QUEUE.map((task, i) => {
const auto = AUTOMATIONS[task.autoKey];
const cls = task.status === 'done' ? 'done' : (i === PANEL.taskIndex && PANEL.status === 'running' ? 'active' : '');
const target = task.config.target ? `@${task.config.target.replace(/^@/,'')}` : '';
const detail = [target, task.config.limit ? `${task.config.limit} max` : ''].filter(Boolean).join(' · ');
return `<div class="xa-queue-item ${cls}">
<div class="xa-queue-num">${i + 1}</div>
<div class="xa-queue-name">${auto?.label || task.autoKey} ${detail ? `<span style="color:#71767b">— ${detail}</span>` : ''}</div>
<button class="xa-queue-remove" data-idx="${i}" title="Remove">✕</button>
</div>`;
}).join('');
els.queueList.querySelectorAll('.xa-queue-remove').forEach(btn => {
btn.addEventListener('click', () => {
TASK_QUEUE.splice(parseInt(btn.dataset.idx), 1);
renderQueue();
});
});
}
// ========================================================================
// EXPORT / SHARE
// ========================================================================
function exportResults(format = 'json') {
if (PANEL.results.length === 0) {
panelLog('Nothing to export yet', 'warning');
return;
}
let content, mime, ext;
if (format === 'csv') {
const headers = Object.keys(PANEL.results[0]);
const rows = PANEL.results.map(r => headers.map(h => `"${String(r[h] || '').replace(/"/g, '""')}"`).join(','));
content = [headers.join(','), ...rows].join('\n');
mime = 'text/csv';
ext = 'csv';
} else {
content = JSON.stringify(PANEL.results, null, 2);
mime = 'application/json';
ext = 'json';
}
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `xactions_export_${Date.now()}.${ext}`;
a.click();
URL.revokeObjectURL(url);
panelLog(`📤 Exported ${PANEL.results.length} results as ${ext.toUpperCase()}`, 'success');
}
function shareConfig() {
const autoKey = els.automation.value;
const config = getInputValues();
const shareData = {
version: 1,
automation: autoKey,
config,
queue: TASK_QUEUE.map(t => ({ autoKey: t.autoKey, config: t.config })),
};
const json = JSON.stringify(shareData, null, 2);
navigator.clipboard.writeText(json).then(() => {
panelLog('🔗 Config copied to clipboard! Share it with others.', 'success');
}).catch(() => {
// Fallback
prompt('Copy this config:', json);
});
}
// ========================================================================
// EVENT HANDLERS
// ========================================================================
// Tabs
$$('.xa-tab').forEach(tab => {
tab.addEventListener('click', () => {
$$('.xa-tab').forEach(t => t.classList.remove('active'));
$$('.xa-tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
$(`#xa-tab-${tab.dataset.tab}`).classList.add('active');
});
});
// Automation selector
els.automation.addEventListener('change', renderInputs);
// Start
els.startBtn.addEventListener('click', () => {
const autoKey = els.automation.value;
const config = getInputValues();
PANEL.results = [];
runTask(autoKey, config);
});
// Pause / Resume
els.pauseBtn.addEventListener('click', () => {
if (PANEL.status === 'running') {
setStatus('paused', 'Paused');
} else if (PANEL.status === 'paused') {
setStatus('running', 'Resuming...');
if (PANEL.pauseResolve) {
PANEL.pauseResolve();
PANEL.pauseResolve = null;
}
}
});
// Stop
els.stopBtn.addEventListener('click', () => {
if (PANEL.abortController) PANEL.abortController.abort();
if (PANEL.pauseResolve) { PANEL.pauseResolve(); PANEL.pauseResolve = null; }
setStatus('idle', 'Stopped');
});
// Add to queue
els.addQueueBtn.addEventListener('click', () => {
const autoKey = els.automation.value;
const config = getInputValues();
TASK_QUEUE.push({ autoKey, config, status: 'pending' });
renderQueue();
panelLog(`+ Added "${AUTOMATIONS[autoKey].label}" to queue`, 'info');
// Flash queue tab
const queueTab = $$('.xa-tab')[1];
queueTab.style.color = '#1d9bf0';
setTimeout(() => { queueTab.style.color = ''; }, 1000);
});
// Run queue
els.runQueueBtn.addEventListener('click', () => {
PANEL.results = [];
PANEL.taskIndex = 0;
TASK_QUEUE.forEach(t => t.status = 'pending');
runQueue();
});
// Clear queue
els.clearQueueBtn.addEventListener('click', () => {
TASK_QUEUE.length = 0;
renderQueue();
});
// Export
els.exportBtn.addEventListener('click', () => {
if (PANEL.results.length === 0) {
panelLog('Nothing to export', 'warning');
return;
}
// Quick format selection
const format = confirm('Export as CSV?\n\nOK = CSV, Cancel = JSON') ? 'csv' : 'json';
exportResults(format);
});
// Share
els.shareBtn.addEventListener('click', shareConfig);
// Clear log
els.clearLogBtn.addEventListener('click', () => {
PANEL.logs = [];
els.logEl.innerHTML = '';
});
// Export log
els.exportLogBtn.addEventListener('click', () => {
const content = PANEL.logs.map(l => `[${l.time}] [${l.type}] ${l.msg}`).join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `xactions_log_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
});
// Minimize
$('#xa-minimize').addEventListener('click', () => {
panel.classList.toggle('xa-minimize');
$('#xa-minimize').textContent = panel.classList.contains('xa-minimize') ? '□' : '─';
});
// Close
$('#xa-close-panel').addEventListener('click', () => {
if (PANEL.abortController) PANEL.abortController.abort();
if (PANEL.pauseResolve) { PANEL.pauseResolve(); PANEL.pauseResolve = null; }
panel.remove();
style.remove();
delete window.XActions.Panel;
panelLog('Panel closed', 'info');
});
// ========================================================================
// INITIAL RENDER
// ========================================================================
renderInputs();
renderQueue();
// Expose to namespace
window.XActions.Panel = {
PANEL,
TASK_QUEUE,
AUTOMATIONS,
addTask: (autoKey, config) => {
TASK_QUEUE.push({ autoKey, config, status: 'pending' });
renderQueue();
},
runQueue,
exportResults,
shareConfig,
close: () => { $('#xa-close-panel').click(); },
};
panelLog('⚡ XActions Panel ready!', 'success');
console.log('⚡ XActions Panel injected. Use window.XActions.Panel to control programmatically.');
console.log('');
console.log('QUICK START:');
console.log(' 1. Pick an automation from the dropdown');
console.log(' 2. Configure keywords, targets, limits');
console.log(' 3. Click Start — or Add to Queue for multi-target runs');
console.log('');
console.log('MULTI-TARGET EXAMPLE (programmatic):');
console.log(' XActions.Panel.addTask("like-timeline", { target: "nichxbt", limit: 500, keywords: "" });');
console.log(' XActions.Panel.addTask("like-timeline", { target: "doi", limit: 500, keywords: "" });');
console.log(' XActions.Panel.runQueue();');
})();
⚡ More XActions Scripts
Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.
Browse All Scripts