[cron:541ac66e-23e1-40db-8995-866d12d10a0e X Account Scraper - Run Every 30 Min] Run X scraper to check monitored accounts for new deals:

cd /home/crogers2287/openclaw-workspaces/ghc-marketing && python3 scripts/x_deal_scraper.py

Log results to group chat. If new deals found, mention how many were imported.
Current time: Monday, February 16th, 2026 — 1:11 PM (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 cron job that wants me to run the X account scraper script. I need to:
1. Run the script from the correct directory
2. Check for new deals
3. Log the results
4. Report back how many deals were imported (if any)

Let me execute the script and see what happens.

exec

cd /home/crogers2287/openclaw-workspaces/ghc-marketing && python3 scripts/x_deal_scraper.py


Command still running (session marine-sable, pid 2167782). Use process (list/poll/log/write/kill/clear/remove) for follow-up.


The script is running in the background. I need to poll it to get the results.


(no new output)

Process still running.


Still running. Let me wait a bit more and check again.


Let me check the log to see if there's any output yet.


(no output yet)


No output yet. Let me wait a bit longer and check again.


It's still running. This script typically takes a while to fetch data from X and process it. Let me wait a bit more.


Still running. Let me check the log again to see if there's any output now.


Still no output. Let me continue polling.


The script is still running. Let me check the log one more time.


Still no output. Let me continue checking.


The script is taking a while. Let me wait a bit more.


Still no output. Let me continue polling to see when it finishes.


Still running. Let me check the log one more time.


Still running with no output. Let me continue polling.


scp: Connection closed


Interesting - there's an error message "scp: Connection closed". This suggests the script might be trying to do an scp operation and it failed. Let me continue polling to see if the script finishes or provides more output.


scp: Connection closed

Process still running.


The script is still running despite the scp error. Let me wait and check again to see if it completes or provides more information.


(no new output)

Process still running.


Let me check the log to see the full output.


Let me continue polling to see if the script finishes.


Let me check the log to see if there's any new output.


Still just that scp error. Let me continue polling.


Still running with just the scp error. Let me continue polling.


The script has been running for a while and only showing an scp error. Let me check the actual script to understand what it's trying to do.


#!/usr/bin/env python3
"""
X (Twitter) Deal Scraper for GottaHaveCoups
============================================

Scrapes specified X accounts for deal posts and imports them into the GHC system.

Usage:
    python3 x_deal_scraper.py                    # Run scrape on all accounts
    python3 x_deal_scraper.py --add @account      # Add account to scrape list
    python3 x_deal_scraper.py --remove @account   # Remove account from list
    python3 x_deal_scraper.py --list              # List all monitored accounts
    python3 x_deal_scraper.py --dashboard         # Launch web dashboard
"""

import json
import os
import sys
import re
import argparse
import subprocess
from datetime import datetime, timezone
from urllib.request import Request, urlopen
from typing import List, Dict, Optional

# Config paths
CONFIG_DIR = "/home/crogers2287/openclaw-workspaces/ghc-marketing/config"
CONFIG_FILE = os.path.join(CONFIG_DIR, "x_scraper_accounts.json")
SCRAPED_DEALS_FILE = os.path.join(CONFIG_DIR, "x_scraped_deals.json")

# GHC API endpoints
GHC_API_BASE = "https://www.gottahavecoups.com/api/v1"
GHC_DEALS_API = f"{GHC_API_BASE}/deals"
GHC_IMPORT_API = f"{GHC_API_BASE}/admin/import-deal"  # Adjust based on actual API

# Maya VM connection for twikit
MAYA_HOST = "maya@maya"

def load_config() -> Dict:
    """Load scraper configuration."""
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, 'r') as f:
            return json.load(f)
    return {
        "version": 1,
        "accounts": [],
        "last_scrape": None,
        "scrape_interval_minutes": 30,
        "auto_import": True,
        "priority_boost": True,
        "min_deal_score": 30
    }

def save_config(config: Dict):
    """Save scraper configuration."""
    os.makedirs(CONFIG_DIR, exist_ok=True)
    with open(CONFIG_FILE, 'w') as f:
        json.dump(config, f, indent=2)

def load_scraped_deals() -> List[Dict]:
    """Load previously scraped deals to avoid duplicates."""
    if os.path.exists(SCRAPED_DEALS_FILE):
        with open(SCRAPED_DEALS_FILE, 'r') as f:
            return json.load(f)
    return []

def save_scraped_deals(deals: List[Dict]):
    """Save scraped deals history."""
    with open(SCRAPED_DEALS_FILE, 'w') as f:
        json.dump(deals, f, indent=2)

def add_account(username: str, category: str = "general", notes: str = ""):
    """Add an X account to the scrape list."""
    config = load_config()
    username = username.lstrip('@').lower()

    # Check if already exists
    for acc in config["accounts"]:
        if acc["username"].lower() == username:
            print(f"⚠  @{username} is already in the scrape list")
            return False

    config["accounts"].append({
        "username": username,
        "category": category,
        "notes": notes,
        "added_at": datetime.now(timezone.utc).isoformat(),
        "last_scraped": None,
        "active": True,
        "deals_found": 0
    })

    save_config(config)
    print(f"✅ Added @{username} to scrape list")
    return True

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


I can see the script is trying to connect to Maya VM. Let me continue reading to see what it's doing with scp.


def remove_account(username: str):
    """Remove an X account from the scrape list."""
    config = load_config()
    username = username.lstrip('@').lower()

    original_count = len(config["accounts"])
    config["accounts"] = [a for a in config["accounts"] if a["username"].lower() != username]

    if len(config["accounts"]) < original_count:
        save_config(config)
        print(f"✅ Removed @{username} from scrape list")
        return True
    else:
        print(f"⚠  @{username} not found in scrape list")
        return False

def list_accounts():
    """List all accounts being scraped."""
    config = load_config()

    if not config["accounts"]:
        print("\n📭 No X accounts configured for scraping")
        print("\nAdd accounts with: python3 x_deal_scraper.py --add @username")
        return

    print(f"\n📱 X Accounts to Scrape ({len(config['accounts'])} total)\n")
    print(f"{'Username':<20} {'Category':<12} {'Status':<8} {'Deals':<6} {'Last Scraped'}")
    print("-" * 70)

    for acc in config["accounts"]:
        status = "🟢" if acc.get("active", True) else "🔴"
        last_scraped = acc.get("last_scraped", "Never")[:16] if acc.get("last_scraped") else "Never"
        deals = acc.get("deals_found", 0)
        print(f"@{acc['username']:<19} {acc.get('category', 'general'):<12} {status:<8} {deals:<6} {last_scraped}")

    print("\n" + "-" * 70)
    print(f"Auto-import: {'✅ ON' if config.get('auto_import') else '❌ OFF'}")
    print(f"Priority boost: {'✅ ON' if config.get('priority_boost') else '❌ OFF'}")
    print(f"Min deal score: {config.get('min_deal_score', 30)}")

def extract_deal_info(tweet_text: str, tweet_media: List[str] = None) -> Optional[Dict]:
    """
    Extract deal information from a tweet text.
    Returns deal dict or None if no deal detected.
    """
    deal = {
        "title": None,
        "price": None,
        "original_price": None,
        "discount_percentage": None,
        "store": None,
        "product_url": None,
        "image_url": None,
        "category": None,
        "source_tweet": None
    }

    # Price patterns
    price_patterns = [
        r'\$([\d,]+\.?\d*)',  # $XX.XX
        r'([\d,]+)\s*dollars',  # XX dollars
    ]

    # Look for prices
    prices = []
    for pattern in price_patterns:
        matches = re.findall(pattern, tweet_text, re.IGNORECASE)
        prices.extend([float(p.replace(',', '')) for p in matches])

    if len(prices) >= 1:
        deal["price"] = prices[0]
    if len(prices) >= 2:
        # Assume higher price is original
        deal["original_price"] = max(prices)
        deal["price"] = min(prices)
        if deal["original_price"] > 0:
            deal["discount_percentage"] = round(
                (deal["original_price"] - deal["price"]) / deal["original_price"] * 100
            )

    # Store detection
    store_patterns = {
        "Amazon": [r'amazon\.com', r'amzn\.to', r'@amazon'],
        "Walmart": [r'walmart\.com', r'w-mt\.co', r'@walmart'],
        "Target": [r'target\.com', r'@target'],
        "Best Buy": [r'bestbuy\.com', r'@bestbuy'],
        "Costco": [r'costco\.com', r'@costco'],
        "Home Depot": [r'homedepot\.com', r'@homedepot'],
    }

    for store, patterns in store_patterns.items():
        for pattern in patterns:
            if re.search(pattern, tweet_text, re.IGNORECASE):
                deal["store"] = store
                break
        if deal["store"]:

[572 more lines in file. Use offset=201 to continue.]


Let me continue reading to find where the scp command is being used.


            break

    # URL extraction
    url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
    urls = re.findall(url_pattern, tweet_text)
    if urls:
        deal["product_url"] = urls[0]

    # Title extraction - look for product mentions
    title_patterns = [
        r'(?:Get|Grab|Score| snag )\s*([^\n.]+?)(?:\s+for\s+|\s+at\s+|\s+-\s+|\s+–\s+)',
        r'([^\n.]{20,80}?)(?:\s+is\s+\$|\s+only\s+\$|\s+just\s+\$)',
    ]

    for pattern in title_patterns:
        match = re.search(pattern, tweet_text, re.IGNORECASE)
        if match:
            deal["title"] = match.group(1).strip()[:150]
            break

    # If no title found, use first line
    if not deal["title"]:
        first_line = tweet_text.split('\n')[0][:150]
        deal["title"] = first_line

    # Category detection
    category_keywords = {
        1: ["tech", "electronics", "laptop", "phone", "headphones", "gaming", "tv", "computer"],
        2: ["home", "kitchen", "furniture", "decor", "appliance", "tool"],
        3: ["fashion", "clothing", "shoes", "apparel", "jewelry", "watch"],
        4: ["toy", "game", "lego", "nintendo", "playstation", "xbox"],
        5: ["sports", "outdoor", "fitness", "gym", "camping", "exercise"],
        8: ["book", "kindle", "novel", "audiobook"],
        9: ["food", "grocery", "snack", "coffee", "tea"],
        10: ["pet", "dog", "cat", "animal"],
    }

    text_lower = tweet_text.lower()
    for cat_id, keywords in category_keywords.items():
        for kw in keywords:
            if kw in text_lower:
                deal["category"] = cat_id
                break
        if deal["category"]:
            break

    # Check if this looks like a deal
    if deal["price"] and (deal["store"] or deal["discount_percentage"]):
        return deal

    return None

def scrape_account_tweets(username: str, limit: int = 20) -> List[Dict]:
    """
    Scrape recent tweets from an X account using Maya VM Chrome/Playwright.
    Returns list of tweet dicts.
    """
    print(f"🔍 Scraping @{username}...")

    try:
        # Create a scraping script to run on Maya VM
        scrape_script = f'''
import asyncio
from playwright.async_api import async_playwright
import json

async def scrape():
    tweets = []
    async with async_playwright() as p:
        try:
            browser = await p.chromium.connect_over_cdp("http://localhost:9222")
            context = browser.contexts[0]
            page = await context.new_page()

            # Go to user's profile
            await page.goto(f"https://x.com/{username}", wait_until="networkidle", timeout=30000)
            await asyncio.sleep(3)

            # Extract tweets
            tweet_elements = await page.locator('article[data-testid="tweet"]').all()

            for tweet in tweet_elements[:{limit}]:
                try:
                    # Get tweet text
                    text_elem = tweet.locator('[data-testid="tweetText"]')
                    text = await text_elem.inner_text() if await text_elem.count() > 0 else ""

                    # Get timestamp
                    time_elem = tweet.locator('time')
                    timestamp = await time_elem.get_attribute('datetime') if await time_elem.count() > 0 else ""

                    if text:
                        tweets.append({{
                            "text": text,
                            "created_at": timestamp,
                            "media": []
                        }})
                except Exception as e:
                    pass

[472 more lines in file. Use offset=301 to continue.]


Let me continue reading to see where the scp command is being called.


            await page.close()
        except Exception as e:
            print(f"Browser error: {{e}}")

    print(json.dumps(tweets, default=str))

asyncio.run(scrape())
'''

        # Save script to Maya VM
        script_path = f"/tmp/scrape_{username}.py"
        with open(script_path, 'w') as f:
            f.write(scrape_script)

        # Copy to Maya
        subprocess.run(['scp', '-q', script_path, f"{MAYA_HOST}:/tmp/scrape_{username}.py"], check=True)

        # Run the scraper on Maya
        result = subprocess.run(
            ['ssh', MAYA_HOST, f'python3 /tmp/scrape_{username}.py'],
            capture_output=True,
            text=True,
            timeout=60
        )

        if result.returncode != 0:
            print(f"⚠  Error scraping @{username}: {result.stderr}")
            return []

        # Parse output
        output = result.stdout.strip()
        if output:
            try:
                tweets = json.loads(output)
                print(f"✅ Found {len(tweets)} tweets from @{username}")
                return tweets
            except json.JSONDecodeError:
                print(f"⚠  Invalid JSON from @{username}")
                return []

        return []

    except subprocess.TimeoutExpired:
        print(f"⏱  Timeout scraping @{username}")
        return []
    except Exception as e:
        print(f"⚠  Error scraping @{username}: {e}")
        return []

def import_deal_to_ghc(deal: Dict, source_account: str) -> bool:
    """
    Import a scraped deal into the GottaHaveCoups system.
    Returns True if successful.
    """
    # For now, we'll create a draft deal that can be reviewed
    # In production, this would POST to the GHC API

    draft_deal = {
        "title": deal["title"],
        "description": f"Scraped from X @{source_account}",
        "price": deal["price"],
        "original_price": deal.get("original_price"),
        "discount_percentage": deal.get("discount_percentage"),
        "store_name": deal.get("store", "Unknown"),
        "category_id": deal.get("category", 1),
        "source": f"X:@{source_account}",
        "deal_url": deal.get("product_url", ""),
        "image_url": deal.get("image_url", ""),
        "quality_score": 50,  # Boost priority
        "is_hot": True,
        "is_featured": True,
        "scraped_at": datetime.now(timezone.utc).isoformat(),
        "needs_review": True  # Flag for manual review
    }

    # Save to scraped deals file
    scraped = load_scraped_deals()

    # Check for duplicates (by title + price)
    for existing in scraped:
        if (existing.get("title") == draft_deal["title"] and
            existing.get("price") == draft_deal["price"]):
            print(f"   ⚠  Duplicate deal skipped: {draft_deal['title'][:50]}...")
            return False

    scraped.append(draft_deal)
    save_scraped_deals(scraped)

    print(f"   ✅ Imported: {draft_deal['title'][:60]}...")
    return True

def run_scraper():
    """Main scraper routine."""
    config = load_config()

    if not config["accounts"]:
        print("📭 No accounts configured. Add accounts with --add @username")

[372 more lines in file. Use offset=401 to continue.]


Now I see it! The script is trying to use scp to copy a Python script to the Maya VM at line 317:

```python
subprocess.run(['scp', '-q', script_path, f"{MAYA_HOST}:/tmp/scrape_{username}.py"], check=True)
```

The error "scp: Connection closed" means it can't connect to Maya VM. This could be because:
1. Maya VM is not running
2. SSH keys are not configured properly
3. The Maya VM host is not reachable

Let me wait for the process to complete to see what the final result is.


Let me wait a bit more for the process to complete.


scp: Connection closed


Let me continue polling to wait for the process to complete.


(no new output)

Process still running.


This is taking too long. Let me check if there's a way to manually test the Maya connection. Actually, let me wait a bit more to see if the script completes or throws an error.


The script has been running for a while with only the scp error. Let me see if I can get more information by checking the config to see what accounts are being scraped.


X (Twitter) Accounts to Scrape for Deals


So there are 3 accounts being scraped. The script is trying to connect to Maya VM but the scp connection is failing. Let me check if the script finally completes.


Let me wait a bit longer to see if it completes.


This is taking too long. Let me try one more poll and if it's still running, I'll assume there's an issue with the Maya VM connection.


Let me try to kill the process and check Maya VM connectivity separately.


Killed session marine-sable.


Now let me test the Maya VM connectivity to see if it's reachable.

ssh -o ConnectTimeout=5 maya@maya 'echo "Maya VM is online"'


ssh: connect to host maya port 22: Connection timed out

Command exited with code 255


The Maya VM is offline or unreachable. The X scraper requires Maya VM to be online because it uses the browser on Maya for scraping. Let me provide a report on this issue.

🔴 **X Scraper Failed - Maya VM Offline**

The X deal scraper couldn't run because Maya VM is offline or unreachable.

**Issue:**
- Connection to `maya@maya` timed out (SSH failed)
- Script tried to copy scraping tools to Maya and run browser automation there
- Script hung on `scp: Connection closed` error

**Configured Accounts (3):**
- @dealsfinderio
- @glitcheddeals
- @fatkiddeals

**Last successful scrape:** 2026-02-16 03:11 AM UTC

**Fix needed:**
Maya VM needs to be brought online for X scraping to work. The scraper depends on Maya's Chrome/Playwright setup for account scraping.

**Alternative:** The scraper could be refactored to run locally on Fred if Maya stays offline, but that would require Chrome/Playwright setup here.