Skill: vmathur/x-monitor (version 1.0.0)

--- name: x-monitor description: Monitor specific X/Twitter accounts and surface noteworthy tweets on a configurable schedule. Filters for high-value content about technology and trends, excluding political rage bait. Use when user wants to manage their X account list, run a manual check, or update filtering criteria. --- # X Monitor Automated Twitter/X monitoring with intelligent filtering for high-value content. ## Setup ### 1. X API Credentials Save your X API credentials to `~/.openclaw/workspace/x-monitor/credentials.json`: ```json { "bearer_token": "YOUR_BEARER_TOKEN_HERE" } ``` ### 2. Handles to Monitor Configure accounts in `~/.openclaw/workspace/x-monitor/handles.json`: ```json { "handles": [ "naval", "paul_graham", "balajis", "vitalikbuterin" ] } ``` **Limit:** 10-20 handles recommended for API quota management. ### 3. Schedule Configuration Configure check frequency in `~/.openclaw/workspace/skills/x-monitor/config/schedule.json`: ```json { "timezone": "America/Los_Angeles", "check_times": ["08:00", "12:00", "16:00", "20:00"], "enabled": true } ``` **Options:** - `timezone`: IANA timezone string (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") - `check_times`: Array of 24-hour times for daily checks (e.g., ["09:00", "18:00"] for twice daily) - `enabled`: Set to false to pause scheduled checks **Common schedules:** - 4x daily (default): `["08:00", "12:00", "16:00", "20:00"]` - 3x daily: `["09:00", "14:00", "20:00"]` - 2x daily: `["09:00", "18:00"]` - 1x daily: `["09:00"]` ### 4. Setup Cron Jobs After configuring your schedule, ask the agent to set up the cron jobs: - "set up x monitor cron jobs" — creates cron jobs based on your schedule.json - "update x monitor schedule" — updates cron jobs after changing schedule.json ### 5. Noteworthy Criteria Edit `~/.openclaw/workspace/x-monitor/noteworthy-criteria.md` to customize what gets surfaced. ## Scheduled Reports Reports run at the times specified in `config/schedule.json`. Each report includes: 1. **Executive summary** — high-level overview of noteworthy items 2. **Filtered tweets** — chronological list with author, content, metrics ## Commands **Manage handles:** - "add @username to x monitor" — add a new handle - "remove @username from x monitor" — remove a handle - "show x monitor handles" — list current handles **Manual check:** - "check x now" — run immediate report **Schedule management:** - "show x monitor schedule" — display current schedule config - "set x monitor to check at 9am and 6pm" — update check times - "set x monitor timezone to America/New_York" — update timezone - "set up x monitor cron jobs" — create/update cron jobs from schedule.json **Update criteria:** - "show noteworthy criteria" — display current filter rules - "update noteworthy criteria" — help edit the criteria ## What Counts as Noteworthy (Default) **Include:** - Insights on technology trends, AI, crypto, product design - First-person experiences building/shipping - Novel frameworks, metaphors, or mental models - Data-driven observations with concrete examples - Contrarian but well-argued takes **Exclude:** - Political rage bait or partisan content - Generic motivational quotes - Pure engagement farming ## API Details Uses X API v2 `tweets/search/recent` endpoint: - Max results: 10 per handle per check - Fields: `created_at`, `public_metrics`, `author_id`, `lang` - Expansions: `author_id` for full user info - Query pattern: `from:{handle}` ## Storage - **Credentials:** `~/.openclaw/workspace/x-monitor/credentials.json` - **Handles:** `~/.openclaw/workspace/x-monitor/handles.json` - **Schedule:** `~/.openclaw/workspace/skills/x-monitor/config/schedule.json` - **Criteria:** `~/.openclaw/workspace/x-monitor/noteworthy-criteria.md` - **Last check:** `~/.openclaw/workspace/x-monitor/last-check.json` (timestamp tracking) ## Cron Job Setup When setting up cron jobs, the agent will: 1. Read `config/schedule.json` for timezone and times 2. Create isolated cron jobs using `agentTurn` payloads 3. Each job runs: fetch tweets → filter for noteworthy → deliver summary Example cron expression for 8am in America/Los_Angeles: ```json { "schedule": { "kind": "cron", "expr": "0 8 * * *", "tz": "America/Los_Angeles" }, "payload": { "kind": "agentTurn", "message": "Run x-monitor check and report noteworthy tweets" } } ``` ## Error Handling - Rate limits: Backs off and logs warning - Invalid handles: Skips and reports - API errors: Logs and continues with remaining handles

path: scripts/setup-crons.sh
content: #!/bin/bash
# Helper script to display cron setup commands based on schedule.json
# The agent reads this and uses the cron tool to set up jobs

SCHEDULE_FILE="$HOME/.openclaw/workspace/skills/x-monitor/config/schedule.json"

if [ ! -f "$SCHEDULE_FILE" ]; then
    echo "Error: Schedule file not found at $SCHEDULE_FILE"
    exit 1
fi

# Read schedule config
TIMEZONE=$(jq -r '.timezone // "America/Los_Angeles"' "$SCHEDULE_FILE")
ENABLED=$(jq -r '.enabled // true' "$SCHEDULE_FILE")

if [ "$ENABLED" != "true" ]; then
    echo "X Monitor schedule is disabled in config"
    exit 0
fi

echo "X Monitor Schedule Configuration:"
echo "  Timezone: $TIMEZONE"
echo "  Check times:"

# Read check times and output cron expressions
jq -r '.check_times[]' "$SCHEDULE_FILE" | while read TIME; do
    HOUR=$(echo "$TIME" | cut -d: -f1 | sed 's/^0//')
    MINUTE=$(echo "$TIME" | cut -d: -f2)
    echo "    - $TIME → cron: $MINUTE $HOUR * * * (tz: $TIMEZONE)"
done

echo ""
echo "Use the cron tool to create jobs with these schedules."


path: scripts/fetch-tweets.py
content: #!/usr/bin/env python3
"""
Fetch and filter X/Twitter tweets from monitored handles.
"""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
import requests

# Config paths
BASE_DIR = Path.home() / '.openclaw' / 'workspace' / 'x-monitor'
CREDS_FILE = BASE_DIR / 'credentials.json'
HANDLES_FILE = BASE_DIR / 'handles.json'
LAST_CHECK_FILE = BASE_DIR / 'last-check.json'

def load_json(path):
    if not path.exists():
        return {}
    with open(path) as f:
        return json.load(f)

def save_json(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, 'w') as f:
        json.dump(data, f, indent=2)

def fetch_recent_tweets(handle, bearer_token, since_time=None):
    """Fetch recent tweets from a handle using X API v2."""
    url = 'https://api.x.com/2/tweets/search/recent'
    
    query = f'from:{handle}'
    if since_time:
        # Format: YYYY-MM-DDTHH:mm:ssZ
        query += f' -is:retweet'
    
    params = {
        'query': query,
        'max_results': 10,
        'tweet.fields': 'created_at,public_metrics,author_id,lang',
        'expansions': 'author_id',
        'user.fields': 'name,username'
    }
    
    if since_time:
        params['start_time'] = since_time
    
    headers = {
        'Authorization': f'Bearer {bearer_token}'
    }
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching tweets for @{handle}: {e}", file=sys.stderr)
        return None

def format_tweet(tweet, user_map):
    """Format tweet for display."""
    author_id = tweet.get('author_id')
    user = user_map.get(author_id, {})
    username = user.get('username', 'unknown')
    name = user.get('name', username)
    
    text = tweet.get('text', '')
    created = tweet.get('created_at', '')
    metrics = tweet.get('public_metrics', {})
    
    likes = metrics.get('like_count', 0)
    retweets = metrics.get('retweet_count', 0)
    
    # Format timestamp
    if created:
        dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
        time_str = dt.strftime('%b %d, %I:%M %p')
    else:
        time_str = 'unknown time'
    
    return {
        'username': username,
        'name': name,
        'text': text,
        'time': time_str,
        'likes': likes,
        'retweets': retweets,
        'url': f'https://twitter.com/{username}/status/{tweet["id"]}'
    }

def main():
    # Load config
    creds = load_json(CREDS_FILE)
    handles_data = load_json(HANDLES_FILE)
    last_check = load_json(LAST_CHECK_FILE)
    
    bearer_token = creds.get('bearer_token')
    if not bearer_token or bearer_token == 'YOUR_BEARER_TOKEN_HERE':
        print("Error: X API bearer token not configured", file=sys.stderr)
        print("Edit ~/.openclaw/workspace/x-monitor/credentials.json", file=sys.stderr)
        sys.exit(1)
    
    handles = handles_data.get('handles', [])
    if not handles:
        print("No handles configured. Add handles to ~/.openclaw/workspace/x-monitor/handles.json")
        sys.exit(0)
    
    # Get last check time
    last_check_time = last_check.get('last_check_time')
    
    # Fetch tweets
    all_tweets = []
    for handle in handles:
        data = fetch_recent_tweets(handle, bearer_token, since_time=last_check_time)
        if not data or 'data' not in data:
            continue
        
        # Build user map
        user_map = {}
        if 'includes' in data and 'users' in data['includes']:
            for user in data['includes']['users']:
                user_map[user['id']] = user
        
        # Format tweets
        for tweet in data.get('data', []):
            formatted = format_tweet(tweet, user_map)
            all_tweets.append(formatted)
    
    # Sort by time (most recent first)
    # Note: This is simplified - for production you'd parse the full timestamp
    
    # Update last check time
    now = datetime.utcnow().isoformat() + 'Z'
    save_json(LAST_CHECK_FILE, {'last_check_time': now})
    
    # Save all tweets to history file with timestamp
    history_file = BASE_DIR / 'tweet_history.json'
    try:
        if history_file.exists():
            history = load_json(history_file)
        else:
            history = {"checks": []}
        
        # Add this check to history
        check_entry = {
            "timestamp": now,
            "tweet_count": len(all_tweets),
            "tweets": all_tweets
        }
        
        # Add to start of list (most recent first)
        history["checks"].insert(0, check_entry)
        
        # Keep only last 50 checks to avoid file growing too large
        if len(history["checks"]) > 50:
            history["checks"] = history["checks"][:50]
        
        save_json(history_file, history)
    except Exception as e:
        print(f"Warning: Failed to save tweet history: {e}", file=sys.stderr)
    
    # Output as JSON for OpenClaw to process
    print(json.dumps({
        'tweets': all_tweets,
        'count': len(all_tweets)
    }, indent=2))

if __name__ == '__main__':
    main()


path: config/schedule.json
content: {
  "timezone": "America/Los_Angeles",
  "check_times": ["08:00", "12:00", "16:00", "20:00"],
  "enabled": true
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)