Skip to content

Instantly share code, notes, and snippets.

@SebConejo
Created March 18, 2026 02:11
Show Gist options
  • Select an option

  • Save SebConejo/94a779aba89090d6bf81896df169c938 to your computer and use it in GitHub Desktop.

Select an option

Save SebConejo/94a779aba89090d6bf81896df169c938 to your computer and use it in GitHub Desktop.
OpenClaw scripts for autonomous social media monitoring and posting
#!/usr/bin/env node
/**
* reddit-comment.mjs
* Posts a comment on a Reddit thread using Puppeteer with your existing Chrome session.
*
* Usage:
* node reddit-comment.mjs --url "https://www.reddit.com/r/openclaw/comments/..." --comment "Your comment here"
*
* Requirements:
* npm install puppeteer-core
*
* This script connects to your existing Chrome profile (already logged into Reddit)
* so no API keys or OAuth needed.
*/
import puppeteer from 'puppeteer-core';
import { execSync } from 'child_process';
import { parseArgs } from 'node:util';
// Parse command line arguments
const { values } = parseArgs({
options: {
url: { type: 'string' },
comment: { type: 'string' },
'chrome-path': { type: 'string', default: '' },
'dry-run': { type: 'boolean', default: false },
}
});
if (!values.url || !values.comment) {
console.error('ERROR: Missing required arguments');
console.error('Usage: node reddit-comment.mjs --url "https://reddit.com/r/.../comments/..." --comment "Your comment"');
process.exit(1);
}
const POST_URL = values.url;
const COMMENT_TEXT = values.comment;
const DRY_RUN = values['dry-run'];
// Find Chrome on macOS
function findChrome() {
if (values['chrome-path']) return values['chrome-path'];
const paths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
];
for (const p of paths) {
try {
execSync(`test -f "${p}"`);
return p;
} catch { continue; }
}
console.error('ERROR: Chrome not found. Use --chrome-path to specify location.');
process.exit(1);
}
// Get Chrome user data directory
function getChromeUserDataDir() {
return `${process.env.HOME}/Library/Application Support/Google/Chrome`;
}
async function main() {
const chromePath = findChrome();
const userDataDir = getChromeUserDataDir();
console.log(`[1/6] Launching Chrome with existing profile...`);
const browser = await puppeteer.launch({
executablePath: chromePath,
userDataDir: userDataDir,
headless: false, // Reddit detects headless
args: [
'--no-first-run',
'--disable-blink-features=AutomationControlled',
'--disable-infobars',
],
defaultViewport: { width: 1280, height: 900 },
});
const page = await browser.newPage();
// Set a realistic user agent
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36');
try {
// Step 2: Navigate to the post
console.log(`[2/6] Navigating to post: ${POST_URL}`);
await page.goto(POST_URL, { waitUntil: 'networkidle2', timeout: 30000 });
await sleep(2000);
// Step 3: Verify we're logged in
console.log(`[3/6] Checking login status...`);
const loginCheck = await page.evaluate(() => {
// Check for logged-in indicators
const userMenu = document.querySelector('[id*="USER"]') ||
document.querySelector('button[aria-label*="profile"]') ||
document.querySelector('a[href*="/user/"]');
return !!userMenu;
});
if (!loginCheck) {
console.error('ERROR: Not logged into Reddit. Please log in manually on Chrome first.');
await browser.close();
process.exit(2);
}
console.log(`[3/6] ✓ Logged in`);
// Step 4: Find the comment box
console.log(`[4/6] Finding comment box...`);
// Reddit's new UI uses a contenteditable div or textarea
// Try multiple selectors for compatibility
const commentBoxSelector = await page.evaluate(() => {
// New Reddit (shreddit)
const shredditBox = document.querySelector('shreddit-composer') ||
document.querySelector('[name="body"]') ||
document.querySelector('div[contenteditable="true"][data-lexical-editor]') ||
document.querySelector('div[role="textbox"]') ||
document.querySelector('.public-DraftEditor-content') ||
document.querySelector('textarea[placeholder*="comment"]') ||
document.querySelector('textarea[placeholder*="thought"]');
if (shredditBox) return 'found';
return null;
});
if (!commentBoxSelector) {
console.error('ERROR: Could not find comment box. The post might not allow comments.');
await browser.close();
process.exit(3);
}
// Click on the comment area to activate it
const commentArea = await page.$('div[contenteditable="true"]') ||
await page.$('div[role="textbox"]') ||
await page.$('textarea[placeholder*="comment"]') ||
await page.$('textarea[placeholder*="thought"]') ||
await page.$('.public-DraftEditor-content');
if (!commentArea) {
// Try clicking on the comment prompt text to activate the editor
const promptTexts = await page.$('div[data-click-id="text"]');
if (promptTexts) {
await promptTexts.click();
await sleep(1000);
}
} else {
await commentArea.click();
await sleep(500);
}
// Step 5: Type the comment
console.log(`[5/6] Typing comment (${COMMENT_TEXT.length} chars)...`);
if (DRY_RUN) {
console.log(`[DRY RUN] Would post: "${COMMENT_TEXT.substring(0, 100)}..."`);
console.log(`[DRY RUN] Skipping actual submission.`);
await browser.close();
process.exit(0);
}
// Type with realistic delays
const activeElement = await page.$('div[contenteditable="true"]:focus') ||
await page.$('div[role="textbox"]') ||
await page.$('textarea:focus');
if (activeElement) {
await activeElement.type(COMMENT_TEXT, { delay: 30 });
} else {
// Fallback: use keyboard directly
await page.keyboard.type(COMMENT_TEXT, { delay: 30 });
}
await sleep(1000);
// Step 6: Click the submit button
console.log(`[6/6] Submitting comment...`);
// Find and click the Comment/Reply button
const submitButton = await page.evaluateHandle(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const commentBtn = buttons.find(b => {
const text = b.textContent.trim().toLowerCase();
return (text === 'comment' || text === 'reply') && !b.disabled;
});
return commentBtn;
});
if (!submitButton || !(await submitButton.asElement())) {
console.error('ERROR: Could not find the Comment/Reply button. It might be disabled.');
// Take a screenshot for debugging
await page.screenshot({ path: '/tmp/reddit-comment-debug.png' });
console.error('DEBUG: Screenshot saved to /tmp/reddit-comment-debug.png');
await browser.close();
process.exit(4);
}
await submitButton.asElement().click();
await sleep(3000);
// Verify the comment was posted
console.log(`[VERIFY] Checking if comment appears on page...`);
const commentPosted = await page.evaluate((text) => {
const bodyText = document.body.innerText;
// Check if our comment text appears on the page
return bodyText.includes(text.substring(0, 50));
}, COMMENT_TEXT);
if (commentPosted) {
// Get the current URL (it might have changed to include the comment)
const currentUrl = page.url();
console.log(`\nSUCCESS: Comment posted!`);
console.log(`URL: ${currentUrl}`);
console.log(`PROOF: Comment text found on page after submission.`);
console.log(`COMMENT: "${COMMENT_TEXT.substring(0, 100)}${COMMENT_TEXT.length > 100 ? '...' : ''}"`);
} else {
console.error(`\nWARNING: Comment may not have been posted. Text not found on page after submission.`);
await page.screenshot({ path: '/tmp/reddit-comment-result.png' });
console.error('DEBUG: Screenshot saved to /tmp/reddit-comment-result.png');
}
await browser.close();
} catch (error) {
console.error(`\nFATAL ERROR: ${error.message}`);
try {
await page.screenshot({ path: '/tmp/reddit-comment-error.png' });
console.error('DEBUG: Error screenshot saved to /tmp/reddit-comment-error.png');
} catch {}
await browser.close();
process.exit(5);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
main();
#!/usr/bin/env node
/**
* reddit-search.mjs
* Searches Reddit for high-potential posts to comment on.
* Uses Reddit's public JSON API (no auth needed for reading).
*
* Usage:
* node reddit-search.mjs --subreddit "openclaw" --sort "hot" --limit 10
* node reddit-search.mjs --subreddit "openclaw,LocalLLM,ClawdBot" --sort "new" --min-score 5
* node reddit-search.mjs --search "LLM cost optimization" --sort "relevance"
*
* Output: JSON array of posts with metadata for the agent to evaluate.
*/
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
subreddit: { type: 'string', default: 'openclaw' },
search: { type: 'string', default: '' },
sort: { type: 'string', default: 'hot' }, // hot, new, top, rising
limit: { type: 'string', default: '15' },
'min-score': { type: 'string', default: '0' },
'max-comments': { type: 'string', default: '999' }, // posts with fewer comments = more opportunity
'max-age-hours': { type: 'string', default: '48' },
}
});
const SUBREDDITS = values.subreddit.split(',').map(s => s.trim());
const SEARCH_QUERY = values.search;
const SORT = values.sort;
const LIMIT = parseInt(values.limit);
const MIN_SCORE = parseInt(values['min-score']);
const MAX_COMMENTS = parseInt(values['max-comments']);
const MAX_AGE_HOURS = parseInt(values['max-age-hours']);
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (compatible; script/1.0)',
'Accept': 'application/json',
};
async function fetchSubreddit(subreddit, sort, limit) {
const url = `https://www.reddit.com/r/${subreddit}/${sort}.json?limit=${limit}&raw_json=1`;
const response = await fetch(url, { headers: HEADERS });
if (!response.ok) {
console.error(`ERROR: Failed to fetch r/${subreddit}: ${response.status}`);
return [];
}
const data = await response.json();
return data.data.children.map(child => child.data);
}
async function searchReddit(query, sort, limit) {
const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(query)}&sort=${sort}&limit=${limit}&raw_json=1`;
const response = await fetch(url, { headers: HEADERS });
if (!response.ok) {
console.error(`ERROR: Search failed: ${response.status}`);
return [];
}
const data = await response.json();
return data.data.children.map(child => child.data);
}
function filterPosts(posts) {
const now = Date.now() / 1000;
const maxAge = MAX_AGE_HOURS * 3600;
return posts.filter(post => {
const age = now - post.created_utc;
if (age > maxAge) return false;
if (post.score < MIN_SCORE) return false;
if (post.num_comments > MAX_COMMENTS) return false;
if (post.locked || post.archived) return false;
if (post.over_18) return false;
return true;
});
}
function scorePosts(posts) {
return posts.map(post => {
const ageHours = (Date.now() / 1000 - post.created_utc) / 3600;
// Scoring: higher = better opportunity
let opportunityScore = 0;
// High upvotes = trending
if (post.score > 100) opportunityScore += 3;
else if (post.score > 30) opportunityScore += 2;
else if (post.score > 5) opportunityScore += 1;
// Few comments = room to be seen
if (post.num_comments < 5) opportunityScore += 3;
else if (post.num_comments < 15) opportunityScore += 2;
else if (post.num_comments < 30) opportunityScore += 1;
// Fresh = better
if (ageHours < 4) opportunityScore += 3;
else if (ageHours < 12) opportunityScore += 2;
else if (ageHours < 24) opportunityScore += 1;
// Upvote velocity (score per hour)
const velocity = ageHours > 0 ? post.score / ageHours : 0;
if (velocity > 10) opportunityScore += 2;
else if (velocity > 3) opportunityScore += 1;
// Topic relevance keywords
const titleLower = (post.title + ' ' + (post.selftext || '')).toLowerCase();
const costKeywords = ['cost', 'expensive', 'bill', 'spend', 'token', 'pricing', 'budget', 'save', 'cheap'];
const routingKeywords = ['routing', 'model', 'haiku', 'opus', 'sonnet', 'switch', 'fallback'];
const setupKeywords = ['setup', 'install', 'configure', 'beginner', 'started', 'first'];
if (costKeywords.some(k => titleLower.includes(k))) opportunityScore += 2;
if (routingKeywords.some(k => titleLower.includes(k))) opportunityScore += 2;
if (setupKeywords.some(k => titleLower.includes(k))) opportunityScore += 1;
return {
...post,
opportunityScore,
ageHours: Math.round(ageHours * 10) / 10,
velocity: Math.round(velocity * 10) / 10,
};
}).sort((a, b) => b.opportunityScore - a.opportunityScore);
}
async function main() {
let allPosts = [];
if (SEARCH_QUERY) {
console.error(`Searching Reddit for: "${SEARCH_QUERY}"...`);
const posts = await searchReddit(SEARCH_QUERY, SORT, LIMIT);
allPosts = allPosts.concat(posts);
} else {
for (const sub of SUBREDDITS) {
console.error(`Fetching r/${sub} (${SORT})...`);
const posts = await fetchSubreddit(sub, SORT, LIMIT);
allPosts = allPosts.concat(posts);
// Rate limit: wait between requests
await new Promise(r => setTimeout(r, 1000));
}
}
console.error(`Fetched ${allPosts.length} posts total.`);
const filtered = filterPosts(allPosts);
console.error(`${filtered.length} posts after filtering.`);
const scored = scorePosts(filtered);
// Output clean JSON for the agent
const output = scored.slice(0, LIMIT).map(post => ({
title: post.title,
subreddit: post.subreddit,
url: `https://www.reddit.com${post.permalink}`,
score: post.score,
comments: post.num_comments,
ageHours: post.ageHours,
velocity: post.velocity,
opportunityScore: post.opportunityScore,
flair: post.link_flair_text || null,
author: post.author,
preview: (post.selftext || '').substring(0, 200),
}));
// Output to stdout (for the agent to parse)
console.log(JSON.stringify(output, null, 2));
}
main().catch(err => {
console.error(`FATAL: ${err.message}`);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment