Search for tweets from specific locations and automatically
How to Use
- Go to x.com and make sure you're logged in
- Open browser console (F12 -> Console)
- Configure the CONFIG object below
- Paste and run the script
Configuration Options
| Option | Default | Description |
|---|---|---|
from | https: | www.latlong.net/ |
geocode | null | e.g., '40.7128,-74.0060,25mi' |
Default Configuration
const CONFIG = {
// Location to search for (city, country, or place)
location: 'New York',
// Optional: geocode for precise location (lat,long,radius)
// Get coordinates from: https://www.latlong.net/
geocode: null, // e.g., '40.7128,-74.0060,25mi'
// Search query to combine with location (optional)
searchQuery: '',
// Comments to randomly pick from
comments: [
'Love seeing posts from this area! 🌍',
'Great content from a great place! 🔥',
'Thanks for sharing! 💯',
'Awesome post! 🙌',
'This is amazing! ✨'
],
// Maximum number of comments to post
maxComments: 10,
// Delay between actions (milliseconds)
minDelay: 3000,
maxDelay: 7000,
// Skip tweets from these usernames
skipUsernames: [],
// Only comment on recent tweets (hours)
maxTweetAge: 24,
// Skip retweets
skipRetweets: true
};
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.
/**
* =============================================================================
* XActions - Comment By Location
* =============================================================================
*
* @name Comment By Location
* @description Search for tweets from specific locations and automatically
* comment on them with customizable messages
* @author nichxbt
* @version 1.0.0
* @date 2026-01-26
* @website https://xactions.app
*
* Usage:
* 1. Go to x.com and make sure you're logged in
* 2. Open browser console (F12 -> Console)
* 3. Configure the CONFIG object below
* 4. Paste and run the script
*
* =============================================================================
*/
(async function commentByLocation() {
'use strict';
// ===========================================================================
// CONFIGURATION - Customize these settings
// ===========================================================================
const CONFIG = {
// Location to search for (city, country, or place)
location: 'New York',
// Optional: geocode for precise location (lat,long,radius)
// Get coordinates from: https://www.latlong.net/
geocode: null, // e.g., '40.7128,-74.0060,25mi'
// Search query to combine with location (optional)
searchQuery: '',
// Comments to randomly pick from
comments: [
'Love seeing posts from this area! 🌍',
'Great content from a great place! 🔥',
'Thanks for sharing! 💯',
'Awesome post! 🙌',
'This is amazing! ✨'
],
// Maximum number of comments to post
maxComments: 10,
// Delay between actions (milliseconds)
minDelay: 3000,
maxDelay: 7000,
// Skip tweets from these usernames
skipUsernames: [],
// Only comment on recent tweets (hours)
maxTweetAge: 24,
// Skip retweets
skipRetweets: true
};
// ===========================================================================
// SELECTORS
// ===========================================================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
replyButton: '[data-testid="reply"]',
tweetTextarea: '[data-testid="tweetTextarea_0"]',
tweetButton: '[data-testid="tweetButton"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
retweet: '[data-testid="socialContext"]',
timestamp: 'time'
};
// ===========================================================================
// HELPERS
// ===========================================================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
return sleep(delay);
};
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const log = (msg, type = 'info') => {
const styles = {
info: 'color: #1DA1F2; font-weight: bold;',
success: 'color: #17BF63; font-weight: bold;',
error: 'color: #E0245E; font-weight: bold;',
warn: 'color: #FFAD1F; font-weight: bold;'
};
console.log(`%c[XActions] ${msg}`, styles[type] || styles.info);
};
const getProcessedTweets = () => {
try {
return JSON.parse(sessionStorage.getItem('xactions_location_commented') || '[]');
} catch {
return [];
}
};
const markTweetProcessed = (tweetId) => {
const tweets = getProcessedTweets();
if (!tweets.includes(tweetId)) {
tweets.push(tweetId);
sessionStorage.setItem('xactions_location_commented', JSON.stringify(tweets));
}
};
const getTweetId = (tweet) => {
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const timeEl = tweet.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweet.querySelector('a[href*="/status/"]');
if (link) {
const match = link.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
}
return null;
};
const isTweetRecent = (tweet) => {
const timeEl = tweet.querySelector(SELECTORS.timestamp);
if (!timeEl) return true;
const tweetTime = new Date(timeEl.getAttribute('datetime'));
const hoursAgo = (Date.now() - tweetTime.getTime()) / (1000 * 60 * 60);
return hoursAgo <= CONFIG.maxTweetAge;
};
const isRetweet = (tweet) => {
// Retweets render socialContext as a link to the reposter's profile;
// pinned posts render it as a plain element. Structural check works on
// any UI language.
const socialContext = tweet.querySelector(SELECTORS.retweet);
return !!socialContext && socialContext.closest('a') !== null;
};
// ===========================================================================
// MAIN FUNCTIONS
// ===========================================================================
const buildSearchUrl = () => {
let query = CONFIG.searchQuery;
if (CONFIG.geocode) {
query += ` geocode:${CONFIG.geocode}`;
} else if (CONFIG.location) {
query += ` near:"${CONFIG.location}"`;
}
const encodedQuery = encodeURIComponent(query.trim());
return `https://x.com/search?q=${encodedQuery}&src=typed_query&f=live`;
};
const navigateToSearch = async () => {
// Assigning location.href reloads the page and kills this script, so only
// navigate when not already on the matching search results, and tell the
// user to re-run after the reload (processed IDs persist in sessionStorage)
const query = CONFIG.geocode ? `geocode:${CONFIG.geocode}` : (CONFIG.location ? `near:"${CONFIG.location}"` : '');
const onSearch = window.location.pathname === '/search' &&
decodeURIComponent(window.location.search).toLowerCase().includes(query.toLowerCase());
if (onSearch) {
log(`🔍 On search results for "${CONFIG.location || CONFIG.geocode}"`);
return true;
}
log(`🔍 Navigating to the location search. The page will reload; paste and run this script again to start commenting.`, 'warn');
const searchUrl = buildSearchUrl();
window.location.href = searchUrl;
await sleep(4000);
return false;
};
const postComment = async (tweet, comment) => {
try {
// Click reply button
const replyBtn = tweet.querySelector(SELECTORS.replyButton);
if (!replyBtn) {
log('Reply button not found', 'warn');
return false;
}
replyBtn.click();
await sleep(1500);
// Find and fill textarea
const textarea = document.querySelector(SELECTORS.tweetTextarea);
if (!textarea) {
log('Tweet textarea not found', 'warn');
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
// Focus and type
textarea.focus();
await sleep(300);
document.execCommand('insertText', false, comment);
await sleep(500);
// Click reply/tweet button
const tweetBtn = document.querySelector(SELECTORS.tweetButton);
if (!tweetBtn || tweetBtn.disabled) {
log('Tweet button not found or disabled', 'warn');
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
tweetBtn.click();
await sleep(2000);
return true;
} catch (err) {
log(`Error posting comment: ${err.message}`, 'error');
return false;
}
};
const processTweets = async (stats) => {
// A Set that we also update locally on every mark, not just a one-time
// snapshot: tweets stay in the DOM across scrolls, so a stale snapshot
// would cause the same tweet to be re-processed/re-commented on every
// outer iteration instead of being skipped as already handled
const processedTweets = new Set(getProcessedTweets());
let scrollAttempts = 0;
const maxScrollAttempts = 30;
let noNewTweetsCount = 0;
while (stats.commented < CONFIG.maxComments && scrollAttempts < maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNew = false;
for (const tweet of tweets) {
if (stats.commented >= CONFIG.maxComments) break;
const tweetId = getTweetId(tweet);
if (!tweetId || processedTweets.has(tweetId)) continue;
foundNew = true;
// Skip retweets if configured
if (CONFIG.skipRetweets && isRetweet(tweet)) {
log(`⏭️ Skipping retweet`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
// Check tweet age
if (!isTweetRecent(tweet)) {
log(`⏭️ Skipping old tweet`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
// Skip certain usernames
const usernameEl = tweet.querySelector('[data-testid="User-Name"] a');
const username = usernameEl?.href?.split('/').pop();
if (CONFIG.skipUsernames.includes(username)) {
log(`⏭️ Skipping @${username}`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
stats.processed++;
// Scroll tweet into view
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(1000);
// Pick random comment
const comment = randomItem(CONFIG.comments);
log(`💬 Commenting on tweet from ${username || 'user'}...`);
const success = await postComment(tweet, comment);
if (success) {
stats.commented++;
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
log(`✅ Comment ${stats.commented}/${CONFIG.maxComments} posted!`, 'success');
} else {
stats.failed++;
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
}
await randomDelay();
}
if (!foundNew) {
noNewTweetsCount++;
if (noNewTweetsCount >= 3) {
log('No more new tweets found', 'warn');
break;
}
} else {
noNewTweetsCount = 0;
}
// Scroll for more tweets
window.scrollBy(0, 800);
await sleep(1500);
scrollAttempts++;
}
};
// ===========================================================================
// EXECUTION
// ===========================================================================
console.clear();
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ 📍 XACTIONS - COMMENT BY LOCATION ║
║ ║
║ Automatically comment on tweets from specific locations ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🚀 Starting Comment By Location...', 'info');
log(`📍 Location: ${CONFIG.location}`);
log(`💬 Max comments: ${CONFIG.maxComments}`);
if (CONFIG.geocode) {
log(`🌐 Using geocode: ${CONFIG.geocode}`);
}
const stats = {
processed: 0,
commented: 0,
failed: 0,
startTime: Date.now()
};
try {
if (await navigateToSearch()) {
await processTweets(stats);
}
} catch (err) {
log(`Fatal error: ${err.message}`, 'error');
console.error(err);
}
// ===========================================================================
// SUMMARY
// ===========================================================================
const duration = ((Date.now() - stats.startTime) / 1000 / 60).toFixed(1);
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ 📊 COMPLETION SUMMARY ║
╠═══════════════════════════════════════════════════════════════╣
║ 📍 Location: ${String(CONFIG.location).padEnd(38)}║
║ ✅ Comments posted: ${String(stats.commented).padEnd(38)}║
║ ❌ Failed: ${String(stats.failed).padEnd(38)}║
║ 📝 Tweets processed: ${String(stats.processed).padEnd(38)}║
║ ⏱️ Duration: ${String(duration + ' minutes').padEnd(38)}║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🎉 Comment By Location completed!', 'success');
})();
⚡ More XActions Scripts
Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.
Browse All Scripts