[cron:ac836bad-bf7b-40ae-8ac1-43d80be065c5 X Engagement - Helpful Replies (5/day)] SCHEDULED TASK: Post 1 helpful engagement reply on X/Twitter.

## CRITICAL: Use GHC profile ONLY
- SOCIAL_PROFILE=/home/crogers2287/.social-poster-profile-ghc
- NEVER use /home/crogers2287/.social-poster-profile/ (that's @cannadealsfl)
- NEVER use Maya VM twikit or cannadeals cookies

## CRITICAL: Only Recent Tweets (Last 2 Days)
When searching, ONLY consider tweets from the LAST 2 DAYS.
- Check the tweet timestamp before replying
- If the tweet is older than 48 hours, SKIP IT
- Look for "X hours ago" or "Yesterday" indicators
- NEVER reply to tweets from weeks/months/years ago

## Rules
- Search hashtags: #dealhunter #couponing #savemoney #amazondeal #frugalliving #budgettips #deals #bargain
- Find a REAL person's tweet (not a bot/spam account) that's RECENT (last 48 hours)
- Reply with something genuinely helpful
- NO self-promotion. No links to GottaHaveCoups. Just be helpful.
- NO hashtags on replies
- Log in memory/YYYY-MM-DD.md

## Method
cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster
SOCIAL_PROFILE=/home/crogers2287/.social-poster-profile-ghc xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" node post.mjs twitter-reply "<reply>" --url "<tweet_url>"
Current time: Sunday, February 15th, 2026 — 4:56 AM (UTC)

Return your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.


This is a scheduled cron task to post a helpful engagement reply on X/Twitter. Let me break down what I need to do:

1. Use the GHC profile ONLY (SOCIAL_PROFILE=/home/crogers2287/.social-poster-profile-ghc)
2. Search for recent tweets (last 2 days) using hashtags like #dealhunter #couponing #savemoney #amazondeal #frugalliving #budgettips #deals #bargain
3. Find a real person's tweet (not a bot/spam)
4. Reply with something genuinely helpful - NO self-promotion, NO links to GottaHaveCoups, NO hashtags on replies
5. Log the action in memory/YYYY-MM-DD.md
6. Use the social-poster skill

Current date is Sunday, February 15th, 2026. So I need to look at tweets from February 13-15, 2026.

Let me first read the social-poster skill to understand how to use it properly.

I'll help with this X engagement task. Let me first read the social-poster skill to understand the proper workflow.


ENOENT: no such file or directory, access '/home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/SKILL.md'


The skill file doesn't exist. Let me check what files are actually available in the workspace.

exec

find /home/crogers2287/openclaw-workspaces/cannadeals-marketing -type f -name "*.mjs" 2>/dev/null | head -20


Good, I found the social-poster directory. Let me look at the search script to understand how to search for tweets, and also look at the post.mjs file.

ls -la /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/ | grep -E "\.mjs|post"


#!/usr/bin/env node
/**
 * Search for recent tweets on X using GHC profile
 */

import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { execSync } from 'child_process';
import fs from 'fs';

chromium.use(stealthPlugin());

const PROFILE_DIR = '/home/crogers2287/.social-poster-profile-ghc';
const XVFB_DISPLAY = ':99';

function ensureXvfb() {
  try {
    execSync(`pgrep -f "Xvfb ${XVFB_DISPLAY}"`, { stdio: 'ignore' });
  } catch {
    console.log('[*] Starting Xvfb...');
    execSync(`Xvfb ${XVFB_DISPLAY} -screen 0 1920x1080x24 &`, { stdio: 'ignore' });
    execSync('sleep 1');
  }
  process.env.DISPLAY = XVFB_DISPLAY;
}

async function searchTweets() {
  ensureXvfb();

  const browser = await chromium.launchPersistentContext(PROFILE_DIR, {
    headless: false,
    viewport: { width: 1920, height: 1080 },
    args: [
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
      '--disable-features=IsolateOrigins,site-per-process',
      `--display=${XVFB_DISPLAY}`,
    ],
    ignoreDefaultArgs: ['--enable-automation'],
  });

  const page = browser.pages()[0] || await browser.newPage();

  try {
    // Search for deal-related tweets (last 24-48 hours)
    const searchQuery = '#savemoney OR #budgettips OR #frugalliving OR #deals OR #couponing';
    console.log(`[*] Searching: ${searchQuery}`);

    await page.goto(`https://x.com/search?q=${encodeURIComponent(searchQuery)}&src=typed_query&f=live`, {
      waitUntil: 'networkidle',
      timeout: 60000
    });
    await page.waitForTimeout(3000);

    // Check if logged in
    if (page.url().includes('/login')) {
      throw new Error('Not logged in!');
    }

    // Extract tweets
    const tweets = await page.$$eval('article[data-testid="tweet"]', articles => {
      return articles.slice(0, 10).map(article => {
        const textEl = article.querySelector('[data-testid="tweetText"]');
        const userEl = article.querySelector('[data-testid="User-Name"] a');
        const timeEl = article.querySelector('time');
        const linkEl = article.querySelector('a[href*="/status/"]');

        return {
          text: textEl ? textEl.innerText.substring(0, 200) : '',
          user: userEl ? userEl.href.split('/').pop() : 'unknown',
          time: timeEl ? timeEl.getAttribute('datetime') : '',
          url: linkEl ? `https://x.com${linkEl.getAttribute('href')}` : '',
        };
      });
    });

    // Filter for recent tweets (last 48 hours)
    const cutoff = new Date(Date.now() - 48 * 60 * 60 * 1000);
    const recentTweets = tweets.filter(t => {
      if (!t.time) return false;
      const tweetTime = new Date(t.time);
      return tweetTime > cutoff && t.text.length > 20;
    });

    console.log('\n=== Recent Tweets (Last 48 Hours) ===\n');
    recentTweets.forEach((t, i) => {
      console.log(`${i + 1}. @${t.user}`);
      console.log(`   Text: ${t.text}`);
      console.log(`   URL: ${t.url}`);
      console.log(`   Time: ${t.time}`);
      console.log('');
    });

    // Save results
    fs.writeFileSync('/tmp/recent_tweets_ghc.json', JSON.stringify(recentTweets, null, 2));
    console.log('[+] Results saved to /tmp/recent_tweets_ghc.json');

  } finally {
    await browser.close();
  }
}

searchTweets().catch(e => {
  console.error('Error:', e.message);
  process.exit(1);
});


-rw-rw-r--  1 crogers2287 crogers2287  1406 Feb 12 02:30 _inject_ghc.mjs
-rw-rw-r--  1 crogers2287 crogers2287  1917 Feb 13 20:50 check_login.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3348 Feb 13 20:49 debug_search.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2808 Feb 12 13:06 delete_by_content.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2944 Feb 12 12:44 delete_tweet.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3396 Feb 15 04:13 find-tweets.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2093 Feb 13 20:51 find_tweets.mjs
-rwxrwxr-x  1 crogers2287 crogers2287  2153 Feb 14 00:01 find_tweets_fast.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2242 Feb 12 02:15 inject_ghc_cookies.mjs
-rw-rw-r--  1 crogers2287 crogers2287  1523 Feb 12 02:15 inject_ghc_pinterest.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2463 Feb 15 04:20 manual_login.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3095 Feb 14 01:16 maya_search.mjs
-rw-rw-r--  1 crogers2287 crogers2287  9826 Feb 14 23:43 post.mjs
-rwxrwxr-x  1 crogers2287 crogers2287  2653 Feb 14 00:02 post_fast.mjs
-rw-rw-r--  1 crogers2287 crogers2287  9825 Feb 14 03:28 post_fixed.mjs
-rw-rw-r--  1 crogers2287 crogers2287  9820 Feb 14 08:43 post_long_timeout.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2440 Feb 15 04:26 quick_login.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2935 Feb 12 01:33 reddit-login.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2899 Feb 14 03:26 search_authentic.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2507 Feb 14 03:27 search_authentic_v2.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2506 Feb 14 09:43 search_authentic_v3.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3462 Feb 14 14:58 search_deals.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3463 Feb 14 23:41 search_deals_100.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3492 Feb 15 03:56 search_debug.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3353 Feb 15 00:44 search_grant.mjs
-rw-rw-r--  1 crogers2287 crogers2287  2577 Feb 14 12:53 search_grocery.mjs
-rw-rw-r--  1 crogers2287 crogers2287  1781 Feb 15 01:45 search_temp.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3420 Feb 13 20:49 search_tweets.mjs
-rw-rw-r--  1 crogers2287 crogers2287  1885 Feb 15 01:47 search_user.mjs
-rw-rw-r--  1 crogers2287 crogers2287  3206 Feb 15 04:40 simple_search.mjs


Now let me look at the post.mjs to understand how to post replies. I also want to search for tweets first to find a good candidate to reply to.


#!/usr/bin/env node
/**
 * CannaDeals Social Poster - Stealth Browser Automation
 * Uses Playwright + Stealth Plugin + Persistent Profile
 *
 * Usage:
 *   node post.mjs twitter "Tweet text here" [--image /path/to/img.jpg]
 *   node post.mjs pinterest "Pin description" --title "Pin Title" --link "https://..." [--image /path/to/img.jpg] [--board "Florida Cannabis Deals"]
 *   node post.mjs reddit-comment "Comment text" --url "https://reddit.com/r/.../comments/..."
 *
 * First run: use --login to open browser for manual auth
 *   node post.mjs twitter --login
 */

import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { execSync } from 'child_process';
import path from 'path';
import fs from 'fs';

// Stealth
chromium.use(stealthPlugin());

// Config
const PROFILE_DIR = process.env.SOCIAL_PROFILE || '/home/crogers2287/.social-poster-profile';
const XVFB_DISPLAY = ':100';

// Human-like typing with occasional pauses
async function humanType(locator, text, opts = {}) {
  const baseDelay = opts.delay || 80;
  for (let i = 0; i < text.length; i++) {
    await locator.pressSequentially(text[i], { delay: baseDelay + Math.random() * 60 });
    // Occasional longer pause (thinking)
    if (Math.random() < 0.05) {
      await locator.page().waitForTimeout(300 + Math.random() * 500);
    }
  }
}

// Ensure Xvfb is running
function ensureXvfb() {
  try {
    execSync(`pgrep -f "Xvfb ${XVFB_DISPLAY}"`, { stdio: 'ignore' });
  } catch {
    console.log('[*] Starting Xvfb...');
    execSync(`Xvfb ${XVFB_DISPLAY} -screen 0 1920x1080x24 &`, { stdio: 'ignore' });
    execSync('sleep 1');
  }
  process.env.DISPLAY = XVFB_DISPLAY;
}

async function launchBrowser(headless = false) {
  ensureXvfb();

  const browser = await chromium.launchPersistentContext(PROFILE_DIR, {
    headless: false, // Must be false for stealth — Xvfb provides the display
    channel: undefined, // Use Playwright's bundled Chromium
    viewport: { width: 1920, height: 1080 },
    args: [
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
      '--disable-features=IsolateOrigins,site-per-process',
      `--display=${XVFB_DISPLAY}`,
    ],
    ignoreDefaultArgs: ['--enable-automation'],
  });

  return browser;
}

// ==================== TWITTER ====================
async function postTwitter(text, imagePath) {
  const browser = await launchBrowser();
  const page = browser.pages()[0] || await browser.newPage();

  try {
    await page.goto('https://x.com/compose/post', { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.waitForTimeout(2000);

    // Check login
    const url = page.url();
    if (url.includes('/login') || url.includes('/i/flow/login')) {
      throw new Error('Not logged in! Run with --login first.');
    }

    // Upload image first if provided
    if (imagePath && fs.existsSync(imagePath)) {
      console.log(`[*] Uploading image: ${imagePath}`);
      const fileInput = page.locator('input[type="file"][data-testid="fileInput"]').first();
      await fileInput.setInputFiles(imagePath);
      await page.waitForTimeout(3000);
    }

    // Type tweet — use the primary column one to avoid strict mode violation
    const textBox = page.locator('[data-testid="tweetTextarea_0"]').first();
    await textBox.click();
    await page.waitForTimeout(500);
    await humanType(textBox, text);

    await page.waitForTimeout(1500);

[196 more lines in file. Use offset=101 to continue.]


Good, now I understand the structure. Let me search for tweets using the hashtags and find a good candidate for a helpful reply. I'll use the search_tweets.mjs script to find recent tweets.

Let me search for recent tweets to find a good candidate for a helpful reply.

cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster && node search_tweets.mjs 2>&1


Command still running (session swift-daisy, pid 2279176). Use process (list/poll/log/write/kill/clear/remove) for follow-up.

[*] Searching: #savemoney OR #budgettips OR #frugalliving OR #deals OR #couponing


The command is still running. Let me check on it using the process tool.


[*] Searching: #savemoney OR #budgettips OR #frugalliving OR #deals OR #couponing

Process still running.


Still running. Let me wait a bit more and then poll again.


(no new output)

Process still running.


Let me wait a bit longer or check the log.


[*] Searching: #savemoney OR #budgettips OR #frugalliving OR #deals OR #couponing


It's still running. Let me wait a bit more.


Still running. Let me give it more time or kill it if it's stuck. Actually, let me check the log again.


It seems stuck. Let me try a simpler approach - use the simple_search.mjs script instead, or I'll kill this process and try a different method.


Killed session swift-daisy.


Let me try a different approach. I'll read the simple_search.mjs file and use that instead.


#!/usr/bin/env node
/**
 * Simple X search for engagement - GHC profile
 */

import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { execSync } from 'child_process';

chromium.use(stealthPlugin());

const PROFILE = '/home/crogers2287/.social-poster-profile-ghc';

// Start Xvfb
try { execSync('pgrep -f "Xvfb :99"', { stdio: 'ignore' }); } catch {
  execSync('Xvfb :99 -screen 0 1920x1080x24 &', { stdio: 'ignore' });
  execSync('sleep 1');
}
process.env.DISPLAY = ':99';

const browser = await chromium.launchPersistentContext(PROFILE, {
  headless: false,
  viewport: { width: 1920, height: 1080 },
  args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--display=:99'],
  ignoreDefaultArgs: ['--enable-automation'],
});

const page = browser.pages()[0] || await browser.newPage();

// Go to X home to verify login
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);

const url = page.url();
const loggedIn = !url.includes('/login') && !url.includes('/i/flow');
console.log('Logged in:', loggedIn);

if (!loggedIn) {
  console.log('ERROR: Not logged in! Current URL:', url);
  await browser.close();
  process.exit(1);
}

// Search for recent tweets
console.log('[+] Searching for recent tweets...');
const searchQueries = [
  '#savemoney',
  '#couponing',
  '#frugalliving',
  '#deals',
  '#budgettips'
];

const cutoff = new Date(Date.now() - 48 * 60 * 60 * 1000); // 48 hours ago
const allTweets = [];

for (const query of searchQueries) {
  try {
    await page.goto(`https://x.com/search?q=${encodeURIComponent(query)}&src=typed_query&f=live`, {
      waitUntil: 'domcontentloaded',
      timeout: 30000
    });
    await page.waitForTimeout(4000);

    const tweets = await page.$$eval('article[data-testid="tweet"]', (articles, cutoffStr) => {
      const cutoff = new Date(cutoffStr);
      return articles.map(article => {
        const textEl = article.querySelector('[data-testid="tweetText"]');
        const timeEl = article.querySelector('time');
        const linkEl = article.querySelector('a[href*="/status/"]');
        const userEl = article.querySelector('[data-testid="User-Name"]');

        const text = textEl ? textEl.innerText : '';
        const time = timeEl ? timeEl.getAttribute('datetime') : '';
        const url = linkEl ? 'https://x.com' + linkEl.getAttribute('href') : '';
        const user = userEl ? userEl.innerText.split('\n')[0] : '';

        // Check if recent
        const tweetTime = time ? new Date(time) : null;
        const isRecent = tweetTime && tweetTime > cutoff;

        return { text, time, url, user, isRecent };
      }).filter(t => t.isRecent && t.text.length > 20);
    }, cutoff.toISOString());

    allTweets.push(...tweets);
  } catch (e) {
    console.log(`Search error for ${query}:`, e.message);
  }
}

// Remove duplicates
const unique = [];
const seen = new Set();
for (const t of allTweets) {
  if (!seen.has(t.url)) {
    seen.add(t.url);
    unique.push(t);
  }
}

console.log(JSON.stringify(unique.slice(0, 10), null, 2));
await browser.close();


Good, let me try this simpler search script.

cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster && node simple_search.mjs 2>&1


Command still running (session mellow-comet, pid 2284136). Use process (list/poll/log/write/kill/clear/remove) for follow-up.

Logged in: true
[+] Searching for recent tweets...


Another one is running. Let me check on it.


Logged in: true
[+] Searching for recent tweets...


Good, it's logged in and searching. Let me wait a bit more for results.


Logged in: true
[+] Searching for recent tweets...

Process still running.


It seems like it's taking a while to search. Let me poll again.


Let me try killing this and try a different approach - maybe use the browser tool to search directly.


Killed session mellow-comet.


Let me try using the browser tool directly to search for tweets. This might be more efficient.


I need to provide a target URL. Let me try searching Twitter directly via browser.


Can't reach the OpenClaw browser control service (timed out after 15000ms). Start (or restart) the OpenClaw gateway (OpenClaw.app menubar, or `openclaw gateway`) and try again.


The browser control service is not running. Let me try a different approach - I'll manually create a script to search for tweets using the social-poster profile directly and output results.

Actually, let me try to run a quick search using a different method. Let me check if there's a cached recent tweets file I can use.

ls -la /tmp/recent_tweets* 2>/dev/null || echo "No cached files found"


-rw-rw-r-- 1 crogers2287 crogers2287 2 Feb 13 20:49 /tmp/recent_tweets_ghc.json


The file is too small (2 bytes) - it's empty. Let me try a more direct approach using curl and the twikit library if it's available, or create a new search script that is simpler and faster.

Let me check what's in the profile directory to see if there's a cookies file or something I can use.

ls -la /home/crogers2287/.social-poster-profile-ghc/ 2>/dev/null | head -20 || echo "Profile not found"


total 5780
drwx------  39 crogers2287 crogers2287   4096 Feb 15 04:57 .
drwxr-xr-x 305 crogers2287 crogers2287  28672 Feb 15 01:39 ..
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:36 .org.chromium.Chromium.1qF1zb
-rw-------   1 crogers2287 crogers2287 185426 Feb 14 22:39 .org.chromium.Chromium.2852iZ
-rw-------   1 crogers2287 crogers2287 185430 Feb 14 23:39 .org.chromium.Chromium.5BuErQ
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:36 .org.chromium.Chromium.5R1nJj
-rw-------   1 crogers2287 crogers2287 185430 Feb 14 23:40 .org.chromium.Chromium.67S40H
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:36 .org.chromium.Chromium.6dVpqt
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:35 .org.chromium.Chromium.7Ooj0X
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:37 .org.chromium.Chromium.8Ouyr0
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:37 .org.chromium.Chromium.AuX9bu
-rw-------   1 crogers2287 crogers2287 185426 Feb 15 00:43 .org.chromium.Chromium.Dd8Wfl
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:37 .org.chromium.Chromium.EZuQii
-rw-------   1 crogers2287 crogers2287 185462 Feb 14 22:36 .org.chromium.Chromium.G4Hw8p
-rw-------   1 crogers2287 crogers2287 185426 Feb 15 00:44 .org.chromium.Chromium.ROBvxP
-rw-------   1 crogers2287 crogers2287 185426 Feb 15 01:45 .org.chromium.Chromium.SqwZL3
-rw-------   1 crogers2287 crogers2287 185430 Feb 14 23:40 .org.chromium.Chromium.TuFY2M
-rw-------   1 crogers2287 crogers2287 185461 Feb 14 21:37 .org.chromium.Chromium.VPSklN
-rw-------   1 crogers2287 crogers2287 185426 Feb 15 04:24 .org.chromium.Chromium.VYTBaZ


Good, the profile exists. Let me try to create a simple script that just searches one hashtag at a time to be faster. Or better yet, let me try to use the find-tweets.mjs script which might be simpler.


#!/usr/bin/env node
/**
 * Find tweets on X for engagement
 */

import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { execSync } from 'child_process';
import path from 'path';
import fs from 'fs';

// Stealth
chromium.use(stealthPlugin());

// Config
const PROFILE_DIR = process.env.SOCIAL_PROFILE || '/home/crogers2287/.social-poster-profile-ghc';
const XVFB_DISPLAY = ':100';

// Ensure Xvfb is running
function ensureXvfb() {
  try {
    execSync(`pgrep -f "Xvfb ${XVFB_DISPLAY}"`, { stdio: 'ignore' });
  } catch {
    console.log('[*] Starting Xvfb...');
    execSync(`Xvfb ${XVFB_DISPLAY} -screen 0 1920x1080x24 &`, { stdio: 'ignore' });
    execSync('sleep 1');
  }
  process.env.DISPLAY = XVFB_DISPLAY;
}

async function launchBrowser(headless = false) {
  ensureXvfb();

  const browser = await chromium.launchPersistentContext(PROFILE_DIR, {
    headless: false,
    channel: undefined,
    viewport: { width: 1920, height: 1080 },
    args: [
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled',
      '--disable-features=IsolateOrigins,site-per-process',
      `--display=${XVFB_DISPLAY}`,
    ],
    ignoreDefaultArgs: ['--enable-automation'],
  });

  return browser;
}

async function searchTweets() {
  const browser = await launchBrowser();
  const page = browser.pages()[0] || await browser.newPage();

  try {
    // Go to X search for recent #savemoney tweets
    await page.goto('https://x.com/search?q=%23savemoney%20OR%20%23couponing%20OR%20%23frugalliving%20OR%20%23deals&src=typed_query&f=live', {
      waitUntil: 'domcontentloaded',
      timeout: 60000
    });
    await page.waitForTimeout(3000);

    // Check if we're logged in
    const url = page.url();
    if (url.includes('/login') || url.includes('/i/flow/login')) {
      console.log('ERROR: Not logged in!');
      await browser.close();
      process.exit(1);
    }

    console.log('[+] Logged in, searching for tweets...');

    // Wait for tweets to load
    await page.waitForSelector('article[data-testid="tweet"]', { timeout: 30000 });

    // Extract tweet info
    const tweets = await page.evaluate(() => {
      const articles = document.querySelectorAll('article[data-testid="tweet"]');
      const results = [];

      articles.forEach((article, i) => {
        if (i >= 5) return; // Get first 5 tweets

        // Get tweet link
        const linkEl = article.querySelector('a[href*="/status/"]');
        const tweetUrl = linkEl ? `https://x.com${linkEl.getAttribute('href')}` : null;

        // Get tweet text
        const textEl = article.querySelector('[data-testid="tweetText"]');
        const text = textEl ? textEl.textContent : '';

        // Get username
        const userEl = article.querySelector('[data-testid="User-Name"] a');
        const username = userEl ? userEl.textContent : '';

        // Get time
        const timeEl = article.querySelector('time');
        const time = timeEl ? timeEl.getAttribute('datetime') : '';

        results.push({ tweetUrl, text, username, time });
      });

      return results;
    });

    console.log(JSON.stringify(tweets, null, 2));

  } catch (err) {
    console.error('Error:', err.message);
  } finally {
    await browser.close();
  }
}

searchTweets();


cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster && SOCIAL_PROFILE=/home/crogers2287/.social-poster-profile-ghc node find-tweets.mjs 2>&1


node:internal/process/promises:394
    triggerUncaughtException(err, true /* fromPromise */);
    ^

browserType.launchPersistentContext: Target page, context or browser has been closed
Browser logs:

╔════════════════════════════════════════════════════════════════════════════════════════════════╗
║ Looks like you launched a headed browser without having a XServer running.                     ║
║ Set either 'headless: true' or use 'xvfb-run <your-playwright-app>' before running Playwright. ║
║                                                                                ║
║ <3 Playwright Team                                                                             ║
╚════════════════════════════════════════════════════════════════════════════════════════════════╝
Call log:
  - <launching> /home/crogers2287/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-update --no-default-browser-check --disable-dev-shm-usage --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --no-sandbox --no-sandbox --disable-blink-features=AutomationControlled,AutomationControlled --disable-features=IsolateOrigins,site-per-process --display=:100 --user-data-dir=/home/crogers2287/.social-poster-profile-ghc --remote-debugging-pipe about:blank
  - <launched> pid=2294841
  - [pid=2294841][err] [2294841:2294841:0215/045747.780677:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:256] Missing X server or $DISPLAY
  - [pid=2294841][err] [2294841:2294841:0215/045747.780733:ERROR:ui/aura/env.cc:246] The platform failed to initialize.  Exiting.
  - [pid=2294841] <gracefully close start>
  - [pid=2294841] <kill>
  - [pid=2294841] <will force kill>
  - [pid=2294841] <process did exit: exitCode=1, signal=null>
  - [pid=2294841] starting temporary directories cleanup
  - [pid=2294841] finished temporary directories cleanup
  - [pid=2294841] <gracefully close end>

    at async Proxy.launchPersistentContext (/home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/node_modules/playwright-extra/dist/index.cjs.js:703:25)
    at async launchBrowser (/home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/find-tweets.mjs:34:19)
    at async searchTweets (/home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/find-tweets.mjs:51:19)

Node.js v22.20.0

Command exited with code 1


The issue is that Xvfb isn't running properly. Let me start Xvfb manually and try again.

pkill -f "Xvfb :100" 2>/dev/null; sleep 1; Xvfb :100 -screen 0 1920x1080x24 &


Command still running (session brisk-cove, pid 2295669). Use process (list/poll/log/write/kill/clear/remove) for follow-up.


Xvfb is starting. Let me now try running the find-tweets script with xvfb-run.

export DISPLAY=:100 && cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster && xvfb-run --server-num=100 --server-args="-screen 0 1920x1080x24" node find-tweets.mjs 2>&1


[+] Logged in, searching for tweets...
[
  {
    "tweetUrl": "https://x.com/SlickdealsPlus/status/2022897568087203945",
    "text": "29-Gal StorageWorks Steel Frame Recycling Bin w/ Lid & 2-Ct Washable Inner Bags $19.80  *ad\nhttps://slickdeals.net/f/19210279-29-gal-storageworks-steel-frame-recycling-bin-w-lid-2-count-washable-inner-bags-3-colors-19-79-free-shipping-w-prime-or-on-35?utm_source=rss&utm_content=fp&utm_medium=RSS2&utm_source=twitter…\n#deals",
    "username": "Slickdeals",
    "time": "2026-02-15T04:55:38.000Z"
  },
  {
    "tweetUrl": "https://x.com/Austin894742999/status/2022894303060582840",
    "text": "Tostitos Bitesize Rounds 40-Count 1oz $12.60 w/ Subscribe & Save  *ad\nhttps://slickdeals.net/f/19209511-40-count-1-oz-tostitos-corn-tortilla-chips-bitesize-rounds-12-57-w-s-s-free-shipping-w-prime-or-on-orders-over-35?utm_source=rss&utm_content=fp&utm_medium=RSS2&utm_source=twitter… #deals",
    "username": "Austin",
    "time": "2026-02-15T04:42:40.000Z"
  },
  {
    "tweetUrl": "https://x.com/Austin894742999/status/2022894301701607696",
    "text": "Klein Tools 3.75\" Compact Titanium-Coated Utility Shears T2107 $15 Free Store Pickup  *ad\nhttps://slickdeals.net/f/19210312-klein-tools-compact-titanium-coated-utility-shears-7-inch-t2107-14-98-free-pickup-lowe-s?utm_source=rss&utm_content=fp&utm_medium=RSS2&utm_source=twitter… #deals",
    "username": "Austin",
    "time": "2026-02-15T04:42:39.000Z"
  },
  {
    "tweetUrl": "https://x.com/Austin894742999/status/2022894300313366872",
    "text": "Hercules DJ DJControl Inpulse 200 MK2 2-Channel DJ Controller $104  *ad\nhttps://slickdeals.net/f/19210843-hercules-dj-control-inpulse-200-mk2-2-channel-dj-controller-104-free-shipping?utm_source=rss&utm_content=fp&utm_medium=RSS2&utm_source=twitter… #deals",
    "username": "Austin",
    "time": "2026-02-15T04:42:39.000Z"
  },
  {
    "tweetUrl": "https://x.com/voicanflav/status/2022894212069625928",
    "text": "Don't miss this deal!\n\n Popular Choice\n\nCheck it out:\nhttps://dolphin.360businesstour.com/pro/products/fitstill-silicone-sleeve-case-for-go-pro-hero12-hero11-hero.html…\n\n#Deals #Shopping #AmazonFinds #DolphinAI #360VirtualTours #AI #SEO #Marketing",
    "username": "Flavia",
    "time": "2026-02-15T04:42:18.000Z"
  }
]


These are all bot/spam accounts posting deals. Let me search for more authentic content - perhaps looking for people talking about couponing and saving money in a more personal way. Let me try searching for different hashtags.

These are mostly deal bots. Let me search for more authentic, personal tweets from real users.

write

#!/usr/bin/env node
/**
 * Find authentic tweets for engagement - GHC profile
 */

import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { execSync } from 'child_process';
import fs from 'fs';

chromium.use(stealthPlugin());

const PROFILE = '/home/crogers2287/.social-poster-profile-ghc';

// Start Xvfb
process.env.DISPLAY = ':100';

const browser = await chromium.launchPersistentContext(PROFILE, {
  headless: false,
  viewport: { width: 1920, height: 1080 },
  args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--display=:100'],
  ignoreDefaultArgs: ['--enable-automation'],
});

const page = browser.pages()[0] || await browser.newPage();

// Search for personal finance/frugal tweets
const searchQueries = [
  '#savemoney tips',
  'couponing help',
  'frugal living advice',
  'budget tips',
  'saving money advice'
];

const cutoff = new Date(Date.now() - 48 * 60 * 60 * 1000);
const allTweets = [];

for (const query of searchQueries) {
  try {
    console.log(`[+] Searching: ${query}`);
    await page.goto(`https://x.com/search?q=${encodeURIComponent(query)}&src=typed_query&f=live`, {
      waitUntil: 'domcontentloaded',
      timeout: 30000
    });
    await page.waitForTimeout(3000);

    const tweets = await page.$$eval('article[data-testid="tweet"]', (articles, cutoffStr) => {
      const cutoff = new Date(cutoffStr);
      return articles.map(article => {
        const textEl = article.querySelector('[data-testid="tweetText"]');
        const timeEl = article.querySelector('time');
        const linkEl = article.querySelector('a[href*="/status/"]');
        const userEl = article.querySelector('[data-testid="User-Name"]');

        const text = textEl ? textEl.innerText : '';
        const time = timeEl ? timeEl.getAttribute('datetime') : '';
        const url = linkEl ? 'https://x.com' + linkEl.getAttribute('href') : '';
        const userHtml = userEl ? userEl.innerText : '';
        const user = userHtml.split('\n')[0];
        const handle = userHtml.split('\n')[1] || '';

        // Check if recent
        const tweetTime = time ? new Date(time) : null;
        const isRecent = tweetTime && tweetTime > cutoff;

        // Skip likely bots/spam
        const isLikelyBot = text.includes('*ad') ||
                           text.includes('Check it out:') ||
                           text.includes('utm_source=') ||
                           text.startsWith('http') ||
                           user.toLowerCase().includes('deal');

        return { text: text.substring(0, 300), time, url, user, handle, isRecent, isLikelyBot };
      }).filter(t => t.isRecent && t.text.length > 20 && !t.isLikelyBot);
    }, cutoff.toISOString());

    allTweets.push(...tweets);
  } catch (e) {
    console.log(`Search error for ${query}:`, e.message);
  }
}

// Remove duplicates
const unique = [];
const seen = new Set();
for (const t of allTweets) {
  if (!seen.has(t.url) && t.url) {
    seen.add(t.url);
    unique.push(t);
  }
}

console.log('\n=== Authentic Tweets Found ===\n');
console.log(JSON.stringify(unique.slice(0, 10), null, 2));

fs.writeFileSync('/tmp/authentic_tweets.json', JSON.stringify(unique.slice(0, 10), null, 2));
console.log('\n[+] Saved to /tmp/authentic_tweets.json');

await browser.close();


Successfully wrote 3381 bytes to /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster/search_authentic_personal.mjs


exec

cd /home/crogers2287/openclaw-workspaces/cannadeals-marketing/skills/social-poster && xvfb-run --server-num=100 --server-args="-screen 0 1920x1080x24" node search_authentic_personal.mjs 2>&1


Command still running (session tidy-crustacean, pid 2302214). Use process (list/poll/log/write/kill/clear/remove) for follow-up.

[+] Searching: #savemoney tips
[+] Searching: couponing help


[+] Searching: #savemoney tips
[+] Searching: couponing help
[+] Searching: frugal living advice


[+] Searching: #savemoney tips
[+] Searching: couponing help
[+] Searching: frugal living advice
[+] Searching: budget tips

Process still running.


[+] Searching: #savemoney tips
[+] Searching: couponing help
[+] Searching: frugal living advice
[+] Searching: budget tips


[+] Searching: saving money advice

Process still running.

[+] Searching: #savemoney tips
[+] Searching: couponing help
[+] Searching: frugal living advice
[+] Searching: budget tips
[+] Searching: saving money advice


It's on the last query. Let me wait a bit more.


(no new output)

Process still running.


[+] Searching: #savemoney tips
[+] Searching: couponing help
[+] Searching: frugal living advice
[+] Searching: budget tips
[+] Searching: saving money advice

=== Authentic Tweets Found ===

[
  {
    "text": "Happy Valentine’s Day!\n\nShow the planet — and your wallet — a little extra love this month by following these water-saving tips!\n\n#SaveWater #SaveMoney #WaterSavingTips #Valentine #ValentinesDay #Valentines",
    "time": "2026-02-14T16:01:11.000Z",
    "url": "https://x.com/hiltonheadpsd/status/2022702670717366456",
    "user": "Hilton Head PSD",
    "handle": "@hiltonheadpsd",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "Live Minimally & Save Money Easily With These Amazing Frugal Living Tips! \nhttps://\nsavebly.com/best-frugal-li\nving-tips/\n… #savemoney #frugalliving #minimalism",
    "time": "2026-02-14T00:00:05.000Z",
    "url": "https://x.com/savebly/status/2022460800787505440",
    "user": "Michael",
    "handle": "@savebly",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": " Wondering if Zomato coupons & offers are worth it in 2026?\n\nWe break down the best deals, real savings & smart tips to help you order food for less \n\n Read now: \nhttps://\nnfcihospitality.com/zomato-coupons\n-offers-worth-it-2026/\n…\n\n#ZomatoDeals #FoodDelivery #SaveMoney #2026Offers #FoodieTips",
    "time": "2026-02-13T11:53:56.000Z",
    "url": "https://x.com/nfcihm/status/2022278060398297136",
    "user": "NFCIHM",
    "handle": "@nfcihm",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "I definitely think th easiest way is looking at some customizable vtuber models on vgen in your budget! Watching tiktoks on tips and tricks on how to use obs and vtube studios! If you have questions on things I would be more than happy to try and help!",
    "time": "2026-02-15T04:04:38.000Z",
    "url": "https://x.com/XxSaturnxXVT/status/2022884733760548891",
    "user": "XxSaturnxXVT ",
    "handle": "@XxSaturnxXVT",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "Discover 10 Affordable Sustainable Fashion Brands that don’t break the bank. Learn how to build an ethical wardrobe on a budget with expert tips and real-world brand guides.\n\nhttps://\ndl.imporinfo.com/2026/02/10-aff\nordable-sustainable-fashion.html\n…",
    "time": "2026-02-15T03:02:28.000Z",
    "url": "https://x.com/tv67126016/status/2022869088159285254",
    "user": "온브레인tv",
    "handle": "@tv67126016",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "That's a killer idea! Grok as the built-in financial AI for X Money would be absolutely epic \nLove that you made a concept — it looks super clean! What features did you prioritize? Budget tracking, investment tips, real-time market analysis?\nTag \n@grok\n\n and \n@xAI\n\n, let's make this",
    "time": "2026-02-15T01:48:36.000Z",
    "url": "https://x.com/taha_saleh_taiz/status/2022850500996272155",
    "user": "طه صالح",
    "handle": "@taha_saleh_taiz",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "I'm offering custom Japan itineraries for $15\n\nWhat you get:\n→ Exact times & routes (9am, 2pm, etc)\n→ Google Maps links you can click\n→ Full budget breakdown\n→ Local insider tips\n→ Hidden spots tourists miss\n\n24-hour delivery guaranteed\n\nOrder here: \nhttps://\nfiverr.com/s/P2ApeXw",
    "time": "2026-02-15T01:13:19.000Z",
    "url": "https://x.com/SoheiJtrip/status/2022841619326865705",
    "user": "Sohei@Japan_trip",
    "handle": "@SoheiJtrip",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "Let's show our support for AshDres\nDaming nominations and YES di tayo pwede ma zero!\n\nUse FREE VOTES CONSISTENTLY every 8hours (use both VPCA app and web)\nFor those who have extra budget PREMIUM VOTES are available. \nTips: drop premium every Friday (discount & boost)",
    "time": "2026-02-14T23:41:35.000Z",
    "url": "https://x.com/crimmymatcha/status/2022818534447894604",
    "user": "harra:)",
    "handle": "@crimmymatcha",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "Maximizing a $50 grocery budget requires a solid game plan, but mastering Trader Joe's with our tips ensures you find the value to last all week.",
    "time": "2026-02-14T22:38:42.000Z",
    "url": "https://x.com/FoodRepublic/status/2022802711238914051",
    "user": "Food Republic",
    "handle": "@FoodRepublic",
    "isRecent": true,
    "isLikelyBot": false
  },
  {
    "text": "Awesome work on your OpenClaw guide! Great to see accessible setups integrating xAI and Grok APIs on a budget Mac Mini. Thanks for the shoutout—excited for more normies to dive in. If you need API tips, hit me up! ",
    "time": "2026-02-14T22:36:54.000Z",
    "url": "https://x.com/grok/status/2022802256840605963",
    "user": "Grok",
    "handle": "@grok",
    "isRecent": true,
    "isLikelyBot": false
  }
]

[+] Saved to /tmp/authentic_tweets.json