Two-part system for Desearch AI's X/Twitter engagement workflow:
analyze.py— Reads x-monitor's sliding tweet window, scores posts, runs GPT-4o-mini deep-dive on top performers, generates @desearch_ai content ideas, and posts a digest to Discord #x-alerts.execute_actions.py— Readspending_actions.json, finds items with explicit Mission Control approval, and executesretweet/quoteactions via Playwright browser automation on x.com.post_tweet.py— Posts original tweets with the same approval contract.
Live execution requires explicit Mission Control per-post approval. The system no longer accepts implied, batch, or chat-based approval for publishing.
For any live action (retweet, quote, or original post), the item MUST include:
| Field | Required | Description |
|---|---|---|
approval_status |
✅ Yes | Must be exactly "approved" (case-insensitive) |
approval_url |
Conditional | URL to the MC approval message/link (required when approved_at is not present) |
approved_by |
✅ Yes | Who approved (operator audit trail) |
approved_at |
Conditional | Required when approval_url is not present |
Item enters queue → Check status='approved' + approval_status='approved' + approved_by + (approval_url or approved_at)
↓
❌ REJECTED ✅ PROCEED
(status = ↓
approval_rejected) Execute action
Items without valid approval are:
- Logged with rejection reason
- Marked as
approval_rejectedin the queue - Never attempted for live execution
This ensures auditability — every live action can be traced back to an explicit human approval.
- Analysis runs automatically every 4 hours via OpenClaw (re-enable around
bash run-engage.sh analyze). - It reads
tweets_window.jsonfrom x-monitor (24h sliding window, ~100 tweets). - Scores every tweet:
score = likes×3 + rts×5 + replies×2 + views×0.01 + quotes×4 + bookmarks×2 - Picks the top 10. Runs GPT-4o-mini on top 3 only (cost-efficient).
- Generates 3 content ideas for @desearch_ai based on the patterns.
- Posts a 6-message digest to Discord
#x-alerts(channel1477727527618347340). - Writes
pending_actions.jsonwith the top 3 tweets for RT/Quote approval, using an exclusive queue lock and atomic replace semantics. - Live execution remains a separate step, behind explicit per-post approval from Mission Control.
After merging → analysis reports appear in Discord automatically. Live X account actions still require explicit MC approval first.
📊 Engagement Report | Top 10 posts • 2026-04-02 10:00 UTC
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#1 @steipete · score 6400 · ❤️1.2K 🔄55 👁️143.6K
> I never use plan mode...
#2 @openclaw · score 2606 · ❤️386 🔄61 👁️49.8K
> OpenClaw 2026.4.1 🦞...
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 Top 3 Deep Dive
[Detailed card per post: tweet text + engagement breakdown + LLM analysis]
[Actions: 🔄 RT as @cosmic_desearch | 💬 Quote | ⏭️ Skip]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 Content Ideas for @desearch_ai
[3 ideas based on top-performer patterns]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
cd ~/projects/openclaw/x-engage
cp .env.example .env
# Fill in OPENAI_API_KEY and DISCORD_BOT_TOKEN in .env
uv sync
# Install Playwright's Chromium browser (required for execute_actions.py and post_tweet.py)
uv run playwright install chromiumx-engage now resolves its normal runtime behavior from the active social_runtime_configs row managed by Social OS.
- Accounts / lanes come from
lane_routing - Browser profile mapping comes from
session_mappings - Default post send-window comes from
send_window - Execution cadence metadata comes from
check_interval_seconds
Supported runtime sources, in order:
X_ENGAGE_RUNTIME_PATH=/path/to/runtime.json— explicit local override for tests / emergency recoverySOCIAL_OS_SUPABASE_URL+SOCIAL_OS_SUPABASE_KEY(orSOCIAL_OS_SUPABASE_ANON_KEY)SUPABASE_URL+SUPABASE_ANON_KEY/SUPABASE_SERVICE_ROLE_KEYVITE_SUPABASE_URL+VITE_SUPABASE_ANON_KEYconfig.jsonfallback only when no managed runtime source is available
session_mappings may contain either:
- an absolute /
~/...profile path, or - a logical session id like
brand-session, which resolves underX_ENGAGE_BROWSER_PROFILE_ROOT(default:~/.x-engage-browser)
execute_actions.py and post_tweet.py use per-account persistent browser profiles from the managed runtime contract first, then fall back to config.json only if the managed source is unavailable.
On the very first run for each account the browser will open to x.com. Log in to the correct account manually — the session is saved for all future runs.
Recommended cadence:
- every 4h:
bash run-engage.sh analyze - operator review window after each digest
- explicit approval via Mission Control SocialPage
- optional/manual validation:
bash run-engage.sh execute-dry-run - live execution only when explicitly approved:
X_ENGAGE_ENABLE_LIVE_EXECUTION=1 bash run-engage.sh execute-live
Do not bundle analysis and live execution into one unattended cron.
python3 analyze.py --dry-run # Dry run: prints JSON, no Discord post
python3 analyze.py # Live run: posts to Discord #x-alertsOr via the safe shell wrapper:
bash run-engage.sh analyze
bash run-engage.sh analyze-dry-run
bash run-engage.sh execute-dry-run
X_ENGAGE_ENABLE_LIVE_EXECUTION=1 bash run-engage.sh execute-livex-engage owns the Social OS review-queue generation contract. Social OS should call
this endpoint/command instead of inserting placeholder social_posts rows itself. The
contract is review-queue-only: it loads the x-monitor signal window, applies the
normal account filter/route stage, writes pending_actions.json + source-rich Social
OS review rows generated by x-engage-analyzer, emits runtime telemetry, and never
executes live X actions.
One-shot CLI mode:
printf '%s' '{"source":"social-os-ui","mode":"review_queue_only","allow_live_actions":false,"requested_by":"social-os","requested_at":"2026-05-04T07:49:00Z"}' \
| bash run-engage.sh queue-refillHTTP mode for SOCIAL_QUEUE_REFILL_URL deployments:
bash run-engage.sh queue-refill-server --host 127.0.0.1 --port 8788
# POST http://127.0.0.1:8788/queue-refillRequest body:
| Field | Required | Description |
|---|---|---|
source |
✅ | Expected caller, usually social-os-ui. |
mode |
✅ | Must be review_queue_only. |
allow_live_actions |
✅ | Must be false; requests with true are rejected before analysis. |
requested_by |
✅ | Operator or service initiating the refill. |
requested_at |
✅ | ISO timestamp used for telemetry and next_run_at calculation. |
trigger |
Optional | manual (default) or scheduled. |
run_id |
Optional | Stable caller-provided run id; otherwise x-engage generates one. |
skip_llm |
Optional | Defaults to true for safe/local queue refill. |
Normalized response includes ok, status, message, created, refreshed,
skipped, run_id, and either next_run_at or schedule_not_configured. Counts
come from x-engage-created Social OS review rows; Social OS placeholder rows do not
count as success.
# Dry run — see what would be executed without opening any browser
uv run python execute_actions.py --dry-run
# Live run — requires explicit MC approval for each item
uv run python execute_actions.py# Dry run
uv run python post_tweet.py --account personal --text "Hello world" --dry-run
# Live — requires explicit approval via CLI args or item metadata
uv run python post_tweet.py --account personal --text "Hello world" \
--approval-status approved \
--approval-url "https://discord.com/channels/.../1234567890" \
--approved-by "Giga"| File | Purpose |
|---|---|
analyze.py |
Engagement analyzer: score, analyze, post digest to Discord |
execute_actions.py |
Action executor: RT/Quote approved tweets via Playwright |
post_tweet.py |
Post original tweets with explicit MC approval |
run_validation_wave.py |
Validation batch executor (requires approval in batch file) |
run-engage.sh |
Shell wrapper for cron (loads .env, calls python3 analyze.py) |
config.json |
Local repo fallback for paths, model, scoring weights, and emergency account metadata |
runtime_loader.py |
Shared Social OS runtime contract loader + fallback merger |
.env |
API keys (not in git) |
.env.example |
Template for keys |
pending_actions.json |
Tweet queue managed by both scripts |
score = likes×3 + retweets×5 + replies×2 + views×0.01 + quotes×4 + bookmarks×2
Example: likes=10, rts=5, replies=2, views=500, quotes=1 → 68
GPT-4o-mini is called only for the top-3 posts (not all 10), keeping cost minimal.
Each entry represents one tweet × account pair. The same tweet appears once per account.
[{
"tweet_id": "123",
"tweet_url": "https://x.com/user/status/123",
"tweet_text": "...",
"author": "username",
"score": 650.0,
"action": "pending | retweet | quote",
"quote_text": "(required for action=quote)",
"status": "pending | approved | done | skipped | failed | approval_rejected",
"account_id": "personal",
"account_handle": "cosmic_desearch",
"lane": "founder | brand",
"action_types": ["retweet", "quote"],
"source": "x-engage-analyzer",
"category": "ai",
"timestamp": "2026-...",
"approval_status": "approved", // REQUIRED for live execution
"approval_url": "https://discord.com/...", // REQUIRED unless approved_at is present
"approved_by": "Giga", // REQUIRED for audit
"approved_at": "2026-..." // REQUIRED unless approval_url is present
}]Deduplication key is (tweet_id, account_id) — re-running analyze.py never adds duplicates.
Set action=retweet or action=quote + status=approved + approval_status=approved + approved_by=<operator> + (approval_url=<MC approval URL> or approved_at=<timestamp>) to queue for execution.
After execute_actions.py runs, status becomes done (or failed with an error field).
config.json is now fallback-only for runtime account/browser settings. The normal operator control path is the managed Social OS runtime contract.
Keep these fields in config.json for repo-local behavior and recovery:
{
"x_monitor_window_path": "/path/to/x-monitor/tweets_window.json",
"discord_channel_id": "1477727527618347340",
"openai_model": "gpt-4o-mini",
"top_n": 10,
"top_deep_dive": 3,
"trigger_interval_hours": 4,
"pending_actions_path": "pending_actions.json",
"score_weights": { "likes": 3, "retweets": 5, "replies": 2, "views": 0.01, "quotes": 4, "bookmarks": 2 },
"x_accounts": [
{
"id": "personal",
"label": "@cosmic_desearch (founder)",
"handle": "cosmic_desearch",
"lane": "founder",
"browser_profile": "~/.x-engage-browser/personal",
"min_confidence": 0.7,
"action_types": ["retweet", "quote"]
}
]
}When managed runtime is available, x-engage will:
- replace
x_accountsfromlane_routing+session_mappings - use the managed
send_windowforpost_tweet.pywhen--send-windowis omitted - keep local-only fields like
x_monitor_window_path, scoring weights, and Discord channel id
Use X_ENGAGE_RUNTIME_PATH only as an explicit emergency override or in tests. Day-to-day edits should happen in Social OS, not in this repo.
analyze.py generates one pending_actions.json entry per tweet × account. All accounts in the managed runtime lane_routing projection are processed — there is no active_account toggle.
execute_actions.py groups approved actions by account_id and opens a separate Chromium browser context per account (each with its own runtime-resolved browser profile), so sessions never cross-contaminate. It now claims the shared queue lock before execution and validates explicit MC approval before any live action.
To add a new account in the normal path: update lane_routing + session_mappings in the Social OS runtime panel. Keep config.json account entries only for fallback metadata / recovery.
x-monitor (every 2h) x-engage (every 4h)
───────────────────── ────────────────────
monitor.py → analyze.py
↓ fetches tweets ↓ reads tweets_window.json
↓ deduplicates ↓ scores + ranks top 10
↓ writes tweets_window.json ↓ GPT-4o-mini analyzes top 3
↓ posts raw tweets to ↓ generates content ideas
Discord #x-alerts ↓ posts digest to Discord #x-alerts
↓ writes pending_actions.json
↓
execute_actions.py (manual trigger, requires MC approval)
↓ reads items with status='approved' and approval_status='approved'
↓ RT/Quote via Playwright
↓ posts confirmations to Discord
Both services post to Discord #x-alerts (1477727527618347340):
- x-monitor: real-time tweet alerts (raw, unanalyzed)
- x-engage: engagement analysis digest with LLM insights + action executor
Repository: https://github.com/Desearch-ai/x-engage