XActions Algorithm Builder — a free, open-source browser console script for X/Twitter automation. No API keys or fees required.
How to Use
- Navigate to x.com and log in
- Open DevTools Console (F12 or Cmd+Option+I)
- Paste the script below and press Enter
Full Script
Copy and paste this entire script into your browser DevTools console on x.com.
/**
* XActions Algorithm Builder
*
* 24/7 automated account growth engine powered by Puppeteer + LLM.
* Builds up your X/Twitter algorithm and persona by:
* - Searching niche topics and scrolling through results
* - Liking, commenting, and following relevant accounts
* - Creating original posts in your persona's voice
* - Visiting profiles, checking notifications, browsing timeline
* - Maintaining human-like activity patterns with sleep cycles
*
* Everything is driven by a Persona config (see personaEngine.js).
* LLM generates all comments and posts via OpenRouter.
*
* Usage:
* import { startAlgorithmBuilder } from './algorithmBuilder.js';
* await startAlgorithmBuilder({ personaId: 'my_persona', authToken: '...' });
*
* @author nich (@nichxbt) - https://github.com/nirholas
* @license MIT
*/
import {
loadPersona,
savePersona,
buildPersonaSystemPrompt,
buildCommentPrompt,
buildPostPrompt,
shouldBeActive,
getSessionDuration,
getDelayUntilNextSession,
planSession,
} from './personaEngine.js';
import {
createBrowser,
createPage,
loginWithCookie,
} from './scrapers/index.js';
// ============================================================================
// Configuration
// ============================================================================
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
const TIMING = {
// Human-like delays with jitter
BETWEEN_ACTIONS: { min: 2000, max: 5000 },
SCROLL_PAUSE: { min: 1500, max: 4000 },
READ_TIME: { min: 2000, max: 8000 },
TYPE_DELAY: { min: 30, max: 100 }, // per character
BEFORE_CLICK: { min: 300, max: 1000 },
PAGE_LOAD: { min: 3000, max: 6000 },
SEARCH_PAUSE: { min: 2000, max: 5000 },
LONG_BREAK: { min: 30000, max: 120000 }, // occasional longer pauses
};
const SELECTORS = {
// Tweet elements
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
replyButton: '[data-testid="reply"]',
retweetButton: '[data-testid="retweet"]',
bookmarkButton: '[data-testid="bookmark"]',
// User elements
userCell: '[data-testid="UserCell"]',
followButton: '[data-testid$="-follow"]',
userAvatar: '[data-testid="UserAvatar"]',
// Compose
tweetTextarea: '[data-testid="tweetTextarea_0"]',
tweetButton: '[data-testid="tweetButton"]',
replyTextarea: '[data-testid="tweetTextarea_0"]',
// Navigation
searchInput: '[data-testid="SearchBox_Search_Input"]',
searchTab: 'a[href*="/search?"]',
topTab: 'a[role="tab"]:first-child',
latestTab: 'a[role="tab"]:nth-child(2)',
// Profile
profileBio: '[data-testid="UserDescription"]',
followersCount: 'a[href$="/followers"] span span',
followingCount: 'a[href$="/following"] span span',
// Misc
confirmButton: '[data-testid="confirmationSheetConfirm"]',
backButton: '[data-testid="app-bar-back"]',
primaryColumn: '[data-testid="primaryColumn"]',
timeline: 'section[role="region"]',
};
// ============================================================================
// Utility Functions
// ============================================================================
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function randomBetween(min, max) {
return Math.floor(min + Math.random() * (max - min));
}
function randomDelay(timing) {
return sleep(randomBetween(timing.min, timing.max));
}
function log(emoji, message) {
const timestamp = new Date().toLocaleTimeString();
console.log(`${emoji} [${timestamp}] ${message}`);
}
/**
* Human-like mouse movement simulation
*/
async function humanClick(page, element) {
try {
await randomDelay(TIMING.BEFORE_CLICK);
const box = await element.boundingBox();
if (!box) return false;
// Move to element with slight offset (humans don't click exact center)
const x = box.x + box.width * (0.3 + Math.random() * 0.4);
const y = box.y + box.height * (0.3 + Math.random() * 0.4);
await page.mouse.move(x, y, { steps: randomBetween(5, 15) });
await sleep(randomBetween(50, 200));
await page.mouse.click(x, y);
return true;
} catch {
return false;
}
}
/**
* Human-like typing with variable speed
*/
async function humanType(page, selector, text) {
try {
await page.waitForSelector(selector, { timeout: 10000 });
await page.focus(selector);
await sleep(randomBetween(200, 500));
for (const char of text) {
await page.keyboard.type(char, {
delay: randomBetween(TIMING.TYPE_DELAY.min, TIMING.TYPE_DELAY.max),
});
// Occasional longer pause (simulates thinking)
if (Math.random() < 0.05) {
await sleep(randomBetween(300, 800));
}
}
return true;
} catch {
return false;
}
}
/**
* Human-like scrolling with variable speed and pauses
*/
async function humanScroll(page, options = {}) {
const scrolls = options.scrolls || randomBetween(3, 8);
const direction = options.direction || 'down';
for (let i = 0; i < scrolls; i++) {
const distance = randomBetween(200, 600) * (direction === 'up' ? -1 : 1);
await page.evaluate((d) => window.scrollBy(0, d), distance);
await randomDelay(TIMING.SCROLL_PAUSE);
// Sometimes pause longer to "read" content
if (Math.random() < 0.3) {
await randomDelay(TIMING.READ_TIME);
}
}
}
// ============================================================================
// LLM Integration
// ============================================================================
/**
* Call OpenRouter for text generation
*/
async function callLLM(messages, persona) {
const apiKey = persona.llm.apiKey || process.env.OPENROUTER_API_KEY;
if (!apiKey) {
log('⚠️', 'No OpenRouter API key — skipping LLM generation');
return null;
}
const model = persona.llm.models.comment;
try {
const response = await fetch(OPENROUTER_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://xactions.app',
'X-Title': 'XActions Algorithm Builder',
},
body: JSON.stringify({
model,
messages,
temperature: persona.llm.temperature,
max_tokens: 300,
}),
});
if (!response.ok) {
const err = await response.text();
log('❌', `LLM error: ${err.slice(0, 100)}`);
return null;
}
const data = await response.json();
return data.choices?.[0]?.message?.content?.trim() || null;
} catch (err) {
log('❌', `LLM request failed: ${err.message}`);
return null;
}
}
/**
* Generate a comment for a tweet using persona's voice
*/
async function generateComment(tweetText, tweetAuthor, persona) {
const systemPrompt = buildPersonaSystemPrompt(persona);
const userPrompt = buildCommentPrompt(persona, tweetText, tweetAuthor);
const content = await callLLM([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
], persona);
if (!content) return null;
// Clean the response — remove quotes, labels
return content
.replace(/^["']|["']$/g, '')
.replace(/^(reply|comment|response):\s*/i, '')
.trim();
}
/**
* Generate an original post using persona's voice
*/
async function generatePost(persona, context = {}) {
const systemPrompt = buildPersonaSystemPrompt(persona);
const userPrompt = buildPostPrompt(persona, context);
const content = await callLLM([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
], persona);
if (!content) return null;
return content
.replace(/^["']|["']$/g, '')
.trim();
}
// ============================================================================
// Page Actions — Individual browser automation tasks
// ============================================================================
/**
* Extract visible tweets from the current page
*/
async function extractVisibleTweets(page) {
return page.evaluate((sel) => {
const tweets = document.querySelectorAll(sel.tweet);
return Array.from(tweets).slice(0, 20).map(tweet => {
const textEl = tweet.querySelector(sel.tweetText);
const text = textEl?.textContent || '';
// Extract author from the tweet
const links = tweet.querySelectorAll('a[role="link"]');
let author = '';
for (const link of links) {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && !href.includes('/status/') && href.split('/').length === 2) {
author = href.slice(1);
break;
}
}
// Check if already liked
const isLiked = !!tweet.querySelector(sel.unlikeButton);
// Get metrics
const likeBtn = tweet.querySelector(sel.likeButton) || tweet.querySelector(sel.unlikeButton);
const likes = parseInt(likeBtn?.getAttribute('aria-label')?.match(/\d+/)?.[0] || '0');
return { text, author, isLiked, likes, index: Array.from(tweets).indexOf(tweet) };
}).filter(t => t.text.length > 10);
}, SELECTORS);
}
/**
* Extract visible user cells from the current page
*/
async function extractVisibleUsers(page) {
return page.evaluate((sel) => {
const cells = document.querySelectorAll(sel.userCell);
return Array.from(cells).slice(0, 15).map(cell => {
const links = cell.querySelectorAll('a[role="link"]');
let username = '';
for (const link of links) {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && href.split('/').length === 2) {
username = href.slice(1);
break;
}
}
const bio = cell.querySelector('[data-testid="UserDescription"]')?.textContent || '';
const hasFollowButton = !!cell.querySelector('[data-testid$="-follow"]');
return { username, bio, hasFollowButton };
}).filter(u => u.username);
}, SELECTORS);
}
/**
* Search for a term on X
*/
async function doSearch(page, term, tab = 'top') {
log('🔍', `Searching: "${term}" (${tab})`);
try {
await page.goto(`https://x.com/search?q=${encodeURIComponent(term)}&src=typed_query&f=${tab === 'latest' ? 'live' : 'top'}`, {
waitUntil: 'networkidle2',
timeout: 30000,
});
await randomDelay(TIMING.PAGE_LOAD);
await humanScroll(page, { scrolls: randomBetween(2, 5) });
return true;
} catch (err) {
log('⚠️', `Search failed: ${err.message}`);
return false;
}
}
/**
* Browse home timeline
*/
async function browseHome(page) {
log('🏠', 'Browsing home timeline');
try {
await page.goto('https://x.com/home', { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
await humanScroll(page, { scrolls: randomBetween(3, 8) });
return true;
} catch (err) {
log('⚠️', `Home browse failed: ${err.message}`);
return false;
}
}
/**
* Like a tweet on the current page
*/
async function likeTweet(page, tweetIndex = null) {
try {
const likeButtons = await page.$$(SELECTORS.likeButton);
if (likeButtons.length === 0) return false;
const idx = tweetIndex ?? randomBetween(0, Math.min(likeButtons.length - 1, 5));
const btn = likeButtons[idx];
if (!btn) return false;
// Get tweet info for logging
const tweets = await extractVisibleTweets(page);
const tweet = tweets[idx];
const clicked = await humanClick(page, btn);
if (clicked) {
log('❤️', `Liked tweet by @${tweet?.author || 'unknown'}: "${(tweet?.text || '').slice(0, 60)}..."`);
}
await randomDelay(TIMING.BETWEEN_ACTIONS);
return clicked;
} catch {
return false;
}
}
/**
* Follow a user on the current page
*/
async function followUser(page, persona, username = null) {
try {
if (username) {
// Navigate to specific user's profile
await page.goto(`https://x.com/${username}`, { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
}
const followButtons = await page.$$(SELECTORS.followButton);
if (followButtons.length === 0) return null;
const btn = followButtons[0];
const clicked = await humanClick(page, btn);
if (clicked) {
const followed = username || 'user';
log('➕', `Followed @${followed}`);
// Track in persona state
persona.state.followedUsers[followed] = {
followedAt: new Date().toISOString(),
unfollowed: false,
};
}
await randomDelay(TIMING.BETWEEN_ACTIONS);
return clicked ? username : null;
} catch {
return null;
}
}
/**
* Comment on a tweet using LLM-generated text
*/
async function commentOnTweet(page, persona, tweet) {
if (!tweet?.text || !tweet?.author) return false;
// Generate a comment
const comment = await generateComment(tweet.text, tweet.author, persona);
if (!comment) {
log('⚠️', 'Could not generate comment, skipping');
return false;
}
log('💬', `Commenting on @${tweet.author}: "${comment.slice(0, 80)}..."`);
try {
// Find and click reply button on the tweet
const tweets = await page.$$(SELECTORS.tweet);
if (!tweets[tweet.index]) return false;
const replyBtn = await tweets[tweet.index].$(SELECTORS.replyButton);
if (!replyBtn) return false;
await humanClick(page, replyBtn);
await sleep(randomBetween(1500, 3000));
// Type the comment
const typed = await humanType(page, SELECTORS.replyTextarea, comment);
if (!typed) return false;
await sleep(randomBetween(500, 1500));
// Click the reply/post button
const postBtn = await page.$(SELECTORS.tweetButton);
if (postBtn) {
await humanClick(page, postBtn);
log('✅', 'Comment posted');
await randomDelay(TIMING.BETWEEN_ACTIONS);
return true;
}
return false;
} catch (err) {
log('⚠️', `Comment failed: ${err.message}`);
return false;
}
}
/**
* Create an original post using LLM
*/
async function createPost(page, persona) {
const postText = await generatePost(persona);
if (!postText) {
log('⚠️', 'Could not generate post, skipping');
return false;
}
log('✍️', `Creating post: "${postText.slice(0, 80)}..."`);
try {
// Navigate to compose
await page.goto('https://x.com/compose/post', { waitUntil: 'networkidle2', timeout: 30000 });
await sleep(randomBetween(1500, 3000));
// Type the post with human-like speed
const typed = await humanType(page, SELECTORS.tweetTextarea, postText);
if (!typed) return false;
// Pause like you're re-reading what you wrote
await sleep(randomBetween(2000, 5000));
// Click post button
const postBtn = await page.$(SELECTORS.tweetButton);
if (postBtn) {
await humanClick(page, postBtn);
log('🚀', 'Post published!');
persona.state.lastPostAt = new Date().toISOString();
await randomDelay(TIMING.BETWEEN_ACTIONS);
return true;
}
return false;
} catch (err) {
log('⚠️', `Post failed: ${err.message}`);
return false;
}
}
/**
* Visit a user's profile and browse it
*/
async function visitProfile(page, username) {
log('👤', `Visiting @${username}'s profile`);
try {
await page.goto(`https://x.com/${username}`, { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
// Scroll through their tweets like a human would
await humanScroll(page, { scrolls: randomBetween(2, 6) });
// Sometimes read the bio
if (Math.random() < 0.5) {
await sleep(randomBetween(1000, 3000));
}
return true;
} catch {
return false;
}
}
/**
* Check own profile (natural behavior)
*/
async function checkOwnProfile(page) {
log('🪞', 'Checking own profile');
try {
// Click profile link in sidebar or navigate
await page.goto('https://x.com/home', { waitUntil: 'networkidle2' });
await sleep(1000);
// Look for profile link in nav
const profileLink = await page.$('a[data-testid="AppTabBar_Profile_Link"]');
if (profileLink) {
await humanClick(page, profileLink);
await randomDelay(TIMING.PAGE_LOAD);
await humanScroll(page, { scrolls: randomBetween(1, 3) });
}
return true;
} catch {
return false;
}
}
/**
* Check notifications
*/
async function checkNotifications(page) {
log('🔔', 'Checking notifications');
try {
await page.goto('https://x.com/notifications', { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
await humanScroll(page, { scrolls: randomBetween(1, 4) });
return true;
} catch {
return false;
}
}
/**
* Smart unfollow — unfollow users who didn't follow back after the grace period
*/
async function smartUnfollow(page, persona, maxUnfollows = 5) {
const graceDays = persona.strategy.unfollowAfterDays || 5;
const now = Date.now();
const graceMs = graceDays * 24 * 60 * 60 * 1000;
let unfollowed = 0;
const candidates = Object.entries(persona.state.followedUsers || {})
.filter(([, info]) => {
if (info.unfollowed) return false;
const followedAt = new Date(info.followedAt).getTime();
return (now - followedAt) > graceMs;
})
.slice(0, maxUnfollows);
if (candidates.length === 0) return 0;
log('🧹', `Checking ${candidates.length} users for unfollow (${graceDays}-day grace period)`);
for (const [username] of candidates) {
try {
await page.goto(`https://x.com/${username}`, { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
// Check if they follow us back (look for "Follows you" indicator)
const followsBack = await page.evaluate(() => {
const indicator = document.querySelector('[data-testid="userFollowIndicator"]');
return !!indicator;
});
if (followsBack) {
log('✅', `@${username} follows back — keeping`);
persona.state.followedUsers[username].followsBack = true;
continue;
}
// Find and click unfollow button (shows as "Following" button)
const unfollowBtn = await page.$('[data-testid$="-unfollow"]');
if (unfollowBtn) {
await humanClick(page, unfollowBtn);
await sleep(randomBetween(1000, 2000));
// Confirm the unfollow dialog
const confirmBtn = await page.$(SELECTORS.confirmButton);
if (confirmBtn) {
await humanClick(page, confirmBtn);
}
persona.state.followedUsers[username].unfollowed = true;
persona.state.followedUsers[username].unfollowedAt = new Date().toISOString();
persona.state.totalUnfollows = (persona.state.totalUnfollows || 0) + 1;
unfollowed++;
log('➖', `Unfollowed @${username} (no follow-back after ${graceDays} days)`);
await randomDelay(TIMING.BETWEEN_ACTIONS);
}
} catch (err) {
log('⚠️', `Unfollow error for @${username}: ${err.message}`);
}
}
if (unfollowed > 0) {
log('🧹', `Unfollowed ${unfollowed} non-followers`);
}
return unfollowed;
}
// ============================================================================
// Session Runner — Executes planned activities
// ============================================================================
/**
* Run a single session with the given activity plan
*/
async function runSession(page, persona, plan) {
const startTime = Date.now();
const maxDuration = plan.duration * 60 * 1000;
const stats = {
searches: 0,
likes: 0,
follows: 0,
comments: 0,
posts: 0,
profileVisits: 0,
errors: 0,
};
log('🎬', `Starting session — ${plan.activities.length} activities planned, ~${plan.duration}min`);
// Collect tweets and users as we browse
let collectedTweets = [];
let collectedUsers = [];
let tweetCursor = 0;
let userCursor = 0;
for (const activity of plan.activities) {
// Check time limit
if (Date.now() - startTime > maxDuration) {
log('⏰', 'Session time limit reached');
break;
}
try {
switch (activity.type) {
case 'search': {
const searched = await doSearch(page, activity.term, activity.tab);
if (searched) {
stats.searches++;
// Collect tweets and users from search results
const newTweets = await extractVisibleTweets(page);
const newUsers = await extractVisibleUsers(page);
collectedTweets.push(...newTweets.filter(t => !persona.state.engagedPosts.has(t.text.slice(0, 50))));
collectedUsers.push(...newUsers);
}
break;
}
case 'browse_home': {
await browseHome(page);
const newTweets = await extractVisibleTweets(page);
collectedTweets.push(...newTweets.filter(t => !persona.state.engagedPosts.has(t.text.slice(0, 50))));
break;
}
case 'like': {
// First make sure we have tweets to like
if (collectedTweets.length === 0) {
await browseHome(page);
collectedTweets = await extractVisibleTweets(page);
}
// Like a tweet on the current page
const liked = await likeTweet(page);
if (liked) {
stats.likes++;
// Mark as engaged
if (collectedTweets[tweetCursor]) {
persona.state.engagedPosts.add(collectedTweets[tweetCursor].text.slice(0, 50));
tweetCursor++;
}
}
break;
}
case 'follow': {
// Follow from collected users or target accounts
let followed = null;
if (collectedUsers.length > userCursor && collectedUsers[userCursor]?.hasFollowButton) {
const user = collectedUsers[userCursor];
followed = await followUser(page, persona, user.username);
userCursor++;
} else if (persona.niche.targetAccounts.length > 0) {
// Visit a target account's followers and follow some
const target = persona.niche.targetAccounts[Math.floor(Math.random() * persona.niche.targetAccounts.length)];
await page.goto(`https://x.com/${target}/followers`, { waitUntil: 'networkidle2', timeout: 30000 });
await randomDelay(TIMING.PAGE_LOAD);
await humanScroll(page, { scrolls: randomBetween(1, 3) });
const users = await extractVisibleUsers(page);
if (users.length > 0) {
const user = users[Math.floor(Math.random() * Math.min(users.length, 5))];
followed = await followUser(page, persona, user.username);
}
}
if (followed) stats.follows++;
break;
}
case 'comment': {
// Find a good tweet to comment on
if (collectedTweets.length === 0) {
await browseHome(page);
collectedTweets = await extractVisibleTweets(page);
}
// Pick a tweet with decent engagement to comment on
const goodTweets = collectedTweets.filter(t => t.likes > 5 && !t.isLiked);
const target = goodTweets.length > 0
? goodTweets[Math.floor(Math.random() * Math.min(goodTweets.length, 5))]
: collectedTweets[0];
if (target) {
// Navigate back to search or home to find the tweet
const searchTerm = persona.niche.searchTerms[Math.floor(Math.random() * persona.niche.searchTerms.length)];
await doSearch(page, searchTerm, 'top');
const freshTweets = await extractVisibleTweets(page);
if (freshTweets.length > 0) {
const tweetToComment = freshTweets[Math.floor(Math.random() * Math.min(freshTweets.length, 3))];
const commented = await commentOnTweet(page, persona, tweetToComment);
if (commented) {
stats.comments++;
persona.state.engagedPosts.add(tweetToComment.text.slice(0, 50));
}
}
}
break;
}
case 'create_post': {
const posted = await createPost(page, persona);
if (posted) stats.posts++;
break;
}
case 'profile_visit': {
// Visit a target account or discovered user
let username;
if (collectedUsers.length > 0 && Math.random() > 0.3) {
username = collectedUsers[Math.floor(Math.random() * collectedUsers.length)].username;
} else if (persona.niche.targetAccounts.length > 0) {
username = persona.niche.targetAccounts[Math.floor(Math.random() * persona.niche.targetAccounts.length)];
}
if (username) {
await visitProfile(page, username);
stats.profileVisits++;
}
break;
}
case 'check_own_profile': {
await checkOwnProfile(page);
break;
}
case 'check_notifications': {
await checkNotifications(page);
break;
}
case 'smart_unfollow': {
const unfollowed = await smartUnfollow(page, persona, activity.count || 5);
if (unfollowed > 0) stats.unfollows = (stats.unfollows || 0) + unfollowed;
break;
}
}
// Random: take a longer break between some actions (10% chance)
if (Math.random() < 0.1) {
const breakTime = randomBetween(TIMING.LONG_BREAK.min, TIMING.LONG_BREAK.max);
log('☕', `Taking a ${Math.round(breakTime / 1000)}s break`);
await sleep(breakTime);
}
} catch (err) {
stats.errors++;
log('❌', `Activity "${activity.type}" error: ${err.message}`);
// Don't crash the session — continue with next activity
}
}
// Update persona state
const duration = Math.round((Date.now() - startTime) / 60000);
log('📊', `Session complete in ${duration}min — ${stats.likes} likes, ${stats.follows} follows, ${stats.comments} comments, ${stats.posts} posts, ${stats.searches} searches`);
persona.state.totalSessions++;
persona.state.totalLikes += stats.likes;
persona.state.totalFollows += stats.follows;
persona.state.totalComments += stats.comments;
persona.state.totalPosts += stats.posts;
persona.state.totalSearches += stats.searches;
persona.state.totalProfileVisits += stats.profileVisits;
persona.state.lastSessionAt = new Date().toISOString();
return stats;
}
// ============================================================================
// Main Loop — 24/7 Operation
// ============================================================================
/**
* Start the algorithm builder — runs continuously with human-like patterns.
*
* @param {Object} options
* @param {string} options.personaId - Persona ID to load
* @param {string} options.authToken - X/Twitter auth_token cookie value
* @param {boolean} [options.headless=true] - Run browser in headless mode
* @param {boolean} [options.dryRun=false] - Log actions without executing
* @param {number} [options.maxSessions=0] - 0 = infinite, or stop after N sessions
* @param {Function} [options.onSessionComplete] - Callback after each session
* @param {AbortSignal} [options.signal] - AbortController signal to stop
*/
async function startAlgorithmBuilder(options = {}) {
const {
personaId,
authToken,
headless = true,
dryRun = false,
maxSessions = 0,
onSessionComplete,
signal,
} = options;
if (!personaId) throw new Error('personaId is required');
if (!authToken && !process.env.XACTIONS_SESSION_COOKIE) {
throw new Error('authToken or XACTIONS_SESSION_COOKIE env var required');
}
const cookie = authToken || process.env.XACTIONS_SESSION_COOKIE;
let persona = loadPersona(personaId);
log('🤖', `Algorithm Builder starting for persona: ${persona.name} (${persona.preset})`);
log('📋', `Strategy: ${persona.strategy.preset} | Activity: ${persona.activityPattern.preset}`);
log('🎯', `Topics: ${persona.niche.topics.join(', ')}`);
log('🧠', `LLM: ${persona.llm.models.comment}`);
if (dryRun) {
log('🧪', 'DRY RUN MODE — no actions will be executed');
}
// Launch browser
let browser, page;
try {
browser = await createBrowser({ headless: headless ? 'new' : false });
page = await createPage(browser);
await loginWithCookie(page, cookie);
log('✅', 'Logged in to X successfully');
} catch (err) {
log('💀', `Failed to launch browser: ${err.message}`);
throw err;
}
let sessionCount = 0;
let running = true;
// Handle abort signal
if (signal) {
signal.addEventListener('abort', () => {
running = false;
log('🛑', 'Abort signal received — stopping after current session');
});
}
// Handle SIGINT/SIGTERM
const gracefulShutdown = () => {
running = false;
log('🛑', 'Shutdown signal — stopping after current session');
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
try {
while (running) {
// Check if we should be active right now
if (!shouldBeActive(persona)) {
const sleepMin = randomBetween(10, 30);
log('😴', `Sleeping (outside active hours) — checking again in ${sleepMin}min`);
await sleep(sleepMin * 60 * 1000);
continue;
}
// Plan and execute a session
const plan = planSession(persona);
if (!dryRun) {
const stats = await runSession(page, persona, plan);
// Save persona state after each session
savePersona(persona);
// Callback
if (onSessionComplete) {
onSessionComplete({ persona, stats, sessionCount: sessionCount + 1 });
}
} else {
log('🧪', `[DRY RUN] Would execute ${plan.activities.length} activities over ~${plan.duration}min`);
plan.activities.forEach(a => log(' 📌', `${a.type}${a.term ? ': ' + a.term : ''}`));
}
sessionCount++;
// Check session limit
if (maxSessions > 0 && sessionCount >= maxSessions) {
log('🏁', `Reached max sessions (${maxSessions})`);
break;
}
// Calculate delay until next session
const delayMin = getDelayUntilNextSession(persona);
log('⏳', `Next session in ~${delayMin}min`);
// Wait with periodic checks for abort signal
const delayMs = delayMin * 60 * 1000;
const checkInterval = 30000; // check every 30s
let waited = 0;
while (waited < delayMs && running) {
await sleep(Math.min(checkInterval, delayMs - waited));
waited += checkInterval;
}
}
} finally {
// Cleanup
log('🧹', 'Cleaning up...');
savePersona(persona);
try { await browser?.close(); } catch { /* ignore */ }
process.removeListener('SIGINT', gracefulShutdown);
process.removeListener('SIGTERM', gracefulShutdown);
log('👋', `Algorithm Builder stopped. ${sessionCount} sessions completed.`);
}
return { sessionCount, persona };
}
/**
* Run a single session (useful for testing)
*/
async function runSingleSession(options = {}) {
return startAlgorithmBuilder({ ...options, maxSessions: 1 });
}
// ============================================================================
// Exports
// ============================================================================
export {
startAlgorithmBuilder,
runSingleSession,
// Individual actions (for use in other scripts)
doSearch,
browseHome,
likeTweet,
followUser,
commentOnTweet,
createPost,
visitProfile,
checkOwnProfile,
checkNotifications,
extractVisibleTweets,
extractVisibleUsers,
// LLM
generateComment,
generatePost,
};
⚡ More XActions Scripts
Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.
Browse All Scripts