Skill: hundevmode/twitter-x-apify-actors (version 1.0.3)

--- name: twitter-x-apify-actors description: Use this skill when the user needs Twitter/X audience collection through Apify actors (followers/following/both) with optional email enrichment, username extraction from links, normalized row output, or webhook-ready payload building. required_env_vars: - APIFY_TOKEN required-env-vars: - APIFY_TOKEN primary_credential: APIFY_TOKEN primary-credential: APIFY_TOKEN metadata: short-description: Run Apify Twitter/X follower and email actors openclaw: requires: env: - APIFY_TOKEN primaryCredential: APIFY_TOKEN --- # Twitter/X Apify Actors ## Overview This skill runs a reliable actor-based pipeline for Twitter/X lead collection using Apify. It extracts a username from an X/Twitter link, runs a follower/following actor, optionally runs an email actor, and returns normalized rows for outreach workflows. Use this skill when a user asks to: - collect followers/following from X via Apify actors - enrich collected usernames with emails - convert profile links to actor-ready usernames - build JSON/webhook payloads for n8n or API endpoints Default actor IDs in this skill: - Followers actor: `bIYXeMcKISYGnHhBG` - Email actor: `mSaHt2tt3Z7Fcwf0o` ## Quick Workflow 1. Parse input target (`https://x.com/...`, `https://twitter.com/...`, or `@username`). 2. Build follower actor payload using `collectType` and `limit`. 3. Run follower actor and normalize usernames. 4. If enrichment is enabled, run email actor and merge results. 5. Return final rows + summary metrics. ## Execution Rules - Prefer script execution for reliability: use `scripts/apify_twitter_actors.py`. - Keep actor IDs configurable, but default to the IDs above. - Always validate `collectType` (`followers`, `following`, `both`) and positive limit. - If email enrichment is disabled, skip email actor entirely. - Never hardcode the Apify token in outputs. Use env `APIFY_TOKEN` or explicit CLI argument. ## Authentication (Apify token) Users can provide the Apify API token in two supported ways. ### Option A: Environment variable (recommended) ```bash export APIFY_TOKEN='apify_api_xxx' python3 scripts/apify_twitter_actors.py run-pipeline \ --target 'https://x.com/elonmusk' \ --collect-type followers \ --limit 1000 \ --include-emails ``` ### Option B: CLI argument ```bash python3 scripts/apify_twitter_actors.py run-pipeline \ --apify-token 'apify_api_xxx' \ --target 'https://x.com/elonmusk' \ --collect-type followers \ --limit 1000 \ --include-emails ``` If both are provided, `--apify-token` is used. If neither is provided, the script returns an explicit authentication error. ## Script Usage Run with Python 3.10+. ```bash python3 scripts/apify_twitter_actors.py parse-username --target 'https://x.com/elonmusk' ``` ```bash APIFY_TOKEN='apify_api_xxx' \ python3 scripts/apify_twitter_actors.py run-followers \ --target 'https://x.com/elonmusk' \ --collect-type followers \ --limit 1000 ``` ```bash APIFY_TOKEN='apify_api_xxx' \ python3 scripts/apify_twitter_actors.py run-pipeline \ --target 'https://x.com/elonmusk' \ --collect-type followers \ --limit 1000 \ --include-emails ``` Quick auth check: ```bash APIFY_TOKEN='apify_api_xxx' \ python3 scripts/apify_twitter_actors.py run-followers \ --target 'https://x.com/elonmusk' \ --collect-type followers \ --limit 10 ``` For contracts and payload details, read: - `references/actor-contracts.md` - `references/troubleshooting.md` ## Output Contract The pipeline returns JSON with: - `targetUsername` - `collectType` - `totalCollected` - `emailsFound` - `rows[]` with `username`, `name`, `email`, `sourceType`, `collectedAt` Use this output directly in n8n Code/HTTP nodes or export to CSV/Google Sheets.

path: README.md
content: # Twitter/X Scraper Skill for OpenClaw (Apify Followers + Optional Email Enrichment)

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
[![Python](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/)
[![Repo stars](https://img.shields.io/github/stars/hundevmode/twitter-x-apify-actors-openclaw-skill?style=social)](https://github.com/hundevmode/twitter-x-apify-actors-openclaw-skill/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/hundevmode/twitter-x-apify-actors-openclaw-skill)](https://github.com/hundevmode/twitter-x-apify-actors-openclaw-skill/commits/main)

A production-focused OpenClaw **Twitter/X scraper skill** to run lead collection with Apify actors: collect followers/following and optionally enrich with emails.

This skill is built for teams that need repeatable **Twitter scraper automation** and **Twitter lead generation** without manually wiring actor payloads each time.

## Table of Contents

- [Actor Links](#actor-links)
- [What This Skill Does](#what-this-skill-does)
- [Who This Skill Is For](#who-this-skill-is-for)
- [Repository Structure](#repository-structure)
- [Why This Beats Manual Scraping](#why-this-beats-manual-scraping)
- [Twitter Scraper API Workflow](#twitter-scraper-api-workflow)
- [Use-Case Snippets](#use-case-snippets)
- [Requirements](#requirements)
- [Authentication (Apify)](#authentication-apify)
- [Quick Start](#quick-start)
- [Output Format](#output-format)
- [Install as OpenClaw Skill](#install-as-openclaw-skill)
- [ClawHub Publishing Notes](#clawhub-publishing-notes)
- [SEO Keywords](#seo-keywords)
- [License](#license)

## Actor Links

- Followers / following actor: [https://console.apify.com/actors/bIYXeMcKISYGnHhBG](https://console.apify.com/actors/bIYXeMcKISYGnHhBG)
- Email enrichment actor: [https://console.apify.com/actors/mSaHt2tt3Z7Fcwf0o](https://console.apify.com/actors/mSaHt2tt3Z7Fcwf0o)

## What This Skill Does

- Extracts username from `x.com`, `twitter.com`, or `@handle`
- Builds actor-ready payloads for follower/following collection
- Runs follower actor and normalizes output rows
- Optionally runs email actor and merges email/name data
- Returns JSON with metrics and outreach-ready rows

## Who This Skill Is For

- Growth teams building X audience lead lists
- Founders running outbound campaigns from Twitter/X
- Agencies that need repeatable social graph collection
- Operators who want Apify actor automation without custom glue code

## Repository Structure

| Path | Purpose |
|---|---|
| `SKILL.md` | Trigger rules, workflow, execution guidance |
| `agents/openai.yaml` | OpenClaw UI metadata |
| `scripts/apify_twitter_actors.py` | CLI runner for followers + optional email enrichment |
| `references/actor-contracts.md` | Actor input/output contracts |
| `references/troubleshooting.md` | Common failures and fixes |

## Why This Beats Manual Scraping

Manual Twitter/X prospecting usually fails at scale because extraction, filtering, enrichment, and formatting happen in separate steps. That creates inconsistency and slows outbound teams.

This skill improves the process by keeping the pipeline deterministic:

- one input target format (`x.com`, `twitter.com`, or `@handle`)
- one runner for followers/following/both
- one optional enrichment branch (email actor)
- one normalized output contract for downstream systems

For growth teams and agencies, this means faster campaign setup, fewer formatting errors, and better reuse across client niches.

## Twitter Scraper API Workflow

Teams often look for a Twitter scraper API workflow that is simple to run and easy to maintain. This skill follows a clear execution pattern: parse profile target, run followers actor, optionally run email enrichment, and return normalized output. The flow is stable enough for repeated campaign usage while staying flexible through input flags like `collect-type`, `limit`, and `include-emails`.

From an operations perspective, the value is not only in collection speed. The bigger win is predictable structure. Because the output schema stays consistent across runs, it is straightforward to pass results into n8n, Google Sheets, CSV exports, or CRM staging steps without rewriting transformation logic every time.

## Use-Case Snippets

### 1) Founder outbound list from a competitor audience
Collect followers of a target founder account, enrich with emails only when needed, and push final rows to your CRM staging sheet.

### 2) Agency lead sprint for Twitter/X niches
Run multiple profile targets with the same script contract, standardize output schema, and hand off to SDRs without manual cleanup.

### 3) Low-cost discovery mode before enrichment
Run follower-only collection first (`--include-emails` off), evaluate quality, then rerun top segments with enrichment enabled.

## Requirements

- Python 3.10+
- `requests`
- Apify API token

Install dependencies:

```bash
pip install -r requirements.txt
```

## Authentication (Apify)

Two supported ways:

### Option A: Environment variable (recommended)

```bash
export APIFY_TOKEN='apify_api_xxx'
```

### Option B: CLI argument

```bash
--apify-token 'apify_api_xxx'
```

If both are provided, `--apify-token` wins.

## Quick Start

### 1) Parse username

```bash
python3 scripts/apify_twitter_actors.py parse-username --target 'https://x.com/elonmusk'
```

### 2) Collect followers/following only

```bash
APIFY_TOKEN='apify_api_xxx' \
python3 scripts/apify_twitter_actors.py run-followers \
  --target 'https://x.com/elonmusk' \
  --collect-type followers \
  --limit 1000
```

### 3) Full pipeline with optional email enrichment

```bash
APIFY_TOKEN='apify_api_xxx' \
python3 scripts/apify_twitter_actors.py run-pipeline \
  --target 'https://x.com/elonmusk' \
  --collect-type followers \
  --limit 1000 \
  --include-emails
```

## Commands

- `parse-username`
- `run-followers`
- `run-pipeline`

Supported collect types:

- `followers`
- `following`
- `both`

## Output Format

`run-pipeline` returns:

- `targetUsername`
- `collectType`
- `includeEmails`
- `totalCollected`
- `emailsFound`
- `rows[]` with `username`, `name`, `email`, `sourceType`, `collectedAt`

This is ready for n8n, CSV export, Google Sheets, or CRM staging.

## Install as OpenClaw Skill

If your OpenClaw/skills runtime supports GitHub install:

```bash
npx skills add hundevmode/twitter-x-apify-actors-openclaw-skill --skill twitter-x-apify-actors
```

List skills in repo:

```bash
npx skills add hundevmode/twitter-x-apify-actors-openclaw-skill --list
```

## ClawHub Publishing Notes

Before publishing to ClawHub:

1. Confirm `SKILL.md` frontmatter is correct
2. Ensure `agents/openai.yaml` metadata is present
3. Run a smoke test with valid `APIFY_TOKEN`
4. Keep actor IDs documented and configurable

## SEO Keywords

Twitter scraper, Twitter/X scraper, X followers scraper, social media scraper, Apify actors, OpenClaw skill, email enrichment, Twitter lead generation, outbound automation, growth automation.

## License

MIT (free for commercial and private use).


path: agents/openai.yaml
content: interface:
  display_name: "Twitter/X Apify Actors"
  short_description: "Run Twitter followers and email actors via Apify"
  default_prompt: "Use $twitter-x-apify-actors to run follower and email actors from a Twitter/X link."


path: references/actor-contracts.md
content: # Actor Contracts: Twitter/X Followers + Email

## Followers Actor

Default actor id: `bIYXeMcKISYGnHhBG`

### Input

```json
{
  "userNameList": ["elonmusk"],
  "userIdList": [],
  "maxFollowers": 1000,
  "maxFollowing": 1,
  "getFollowers": true,
  "getFollowing": false,
  "outputMode": "usernames"
}
```

### Expected output shape (examples)

Rows often contain one of these keys for username extraction:
- `username`
- `screenname`
- `userName`
- `handle`
- `value`

## Email Actor

Default actor id: `mSaHt2tt3Z7Fcwf0o`

### Input

```json
{
  "usernames": "user1\nuser2\nuser3",
  "max_results": 1000
}
```

### Expected output shape (examples)

- `screenname` or `username`
- `name`
- `email`

## Final normalized row

```json
{
  "targetUsername": "elonmusk",
  "username": "someuser",
  "sourceType": "followers",
  "collectedAt": "2026-02-28T11:00:00Z",
  "name": "Optional Name",
  "email": "[REDACTED_SECRET]"
}
```


path: references/troubleshooting.md
content: # Troubleshooting

## Common failures

### 401 / 403 from Apify
- Token is missing or invalid.
- Pass `--apify-token` or set `APIFY_TOKEN` in environment.

### Actor returns empty array
- Username is invalid or actor did not find records.
- Verify target with `parse-username` command first.

### Email merge returns zero emails
- Enrichment actor may have no matches.
- Run pipeline with `--include-emails` only when needed.

### Bad collectType or limit
- Supported collect types: `followers`, `following`, `both`.
- `limit` must be a positive integer.

## Safe defaults
- Start with `collect-type=followers`
- Start with `limit=300` for initial tests
- Keep `--include-emails` disabled for fast low-cost checks

## Minimal smoke test

```bash
APIFY_TOKEN='apify_api_xxx' \
python3 scripts/apify_twitter_actors.py run-pipeline \
  --target 'https://x.com/elonmusk' \
  --collect-type followers \
  --limit 100 \
  --include-emails
```


path: requirements.txt
content: requests>=2.31.0


path: scripts/apify_twitter_actors.py
content: #!/usr/bin/env python3
"""Apify actor runner for Twitter/X followers + optional email enrichment."""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import re
import sys
from dataclasses import dataclass
from typing import Any

try:
    import requests
except ModuleNotFoundError:
    requests = None

FOLLOWER_ACTOR_DEFAULT = "bIYXeMcKISYGnHhBG"
EMAIL_ACTOR_DEFAULT = "mSaHt2tt3Z7Fcwf0o"
APIFY_BASE = "https://api.apify.com/v2/acts"


class SkillError(Exception):
    pass


@dataclass
class Config:
    token: str
    follower_actor_id: str = FOLLOWER_ACTOR_DEFAULT
    email_actor_id: str = EMAIL_ACTOR_DEFAULT


def utc_now_iso() -> str:
    return dt.datetime.now(dt.UTC).isoformat()


def parse_username(target: str) -> str:
    s = (target or "").strip()
    if not s:
        raise SkillError("Target is empty")

    if "twitter.com" not in s and "x.com" not in s:
        return s.replace("@", "").strip()

    m = re.search(r"(?:x|twitter)\.com/([A-Za-z0-9_]+)", s, flags=re.IGNORECASE)
    if not m:
        raise SkillError(f"Could not parse username from target: {target}")
    return m.group(1)


def require_token(explicit: str | None) -> str:
    [REDACTED_SECRET] or os.getenv("APIFY_TOKEN", "")
    if not token:
        raise SkillError("Apify token missing. Pass --apify-token or set APIFY_TOKEN.")
    return token


def run_actor_sync(actor_id: str, token: str, payload: dict[str, Any], timeout_sec: int = 600) -> list[dict[str, Any]]:
    if requests is None:
        raise SkillError("Missing dependency: requests. Install with `pip install requests`.")

    url = f"{APIFY_BASE}/{actor_id}/run-sync-get-dataset-items"
    params = {"token": token, "format": "json", "clean": "true"}
    resp = requests.post(url, params=params, json=payload, timeout=timeout_sec)

    if resp.status_code >= 400:
        raise SkillError(f"Actor {actor_id} failed: HTTP {resp.status_code} - {resp.text[:500]}")

    data = resp.json()
    if isinstance(data, list):
        return data
    if isinstance(data, dict):
        if isinstance(data.get("data"), list):
            return data["data"]
        return [data]
    return []


def build_follower_input(username: str, collect_type: str, limit: int) -> dict[str, Any]:
    collect_type = collect_type.lower().strip()
    if collect_type not in {"followers", "following", "both"}:
        raise SkillError("collectType must be one of: followers, following, both")
    if limit < 1:
        raise SkillError("limit must be >= 1")

    get_followers = collect_type in {"followers", "both"}
    get_following = collect_type in {"following", "both"}

    return {
        "userNameList": [username],
        "userIdList": [],
        "maxFollowers": limit if get_followers else 1,
        "maxFollowing": limit if get_following else 1,
        "getFollowers": get_followers,
        "getFollowing": get_following,
        "outputMode": "usernames",
    }


def normalize_usernames(raw_rows: list[dict[str, Any]], target_username: str, collect_type: str, limit: int) -> list[dict[str, Any]]:
    seen: set[str] = set()
    out: list[dict[str, Any]] = []

    for row in raw_rows:
        username = str(
            row.get("username")
            or row.get("screenname")
            or row.get("userName")
            or row.get("handle")
            or row.get("value")
            or ""
        ).replace("@", "").strip()

        if not username:
            continue

        key = username.lower()
        if key in seen:
            continue
        seen.add(key)

        out.append(
            {
                "targetUsername": target_username,
                "username": username,
                "sourceType": collect_type,
                "collectedAt": utc_now_iso(),
            }
        )

        if len(out) >= limit:
            break

    return out


def build_email_input(rows: list[dict[str, Any]], limit: int) -> dict[str, Any]:
    usernames = [r["username"] for r in rows if r.get("username")]
    return {
        "usernames": "\n".join(usernames),
        "max_results": limit,
    }


def merge_email_rows(base_rows: list[dict[str, Any]], email_rows_raw: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], int]:
    by_username: dict[str, dict[str, str]] = {}
    for row in email_rows_raw:
        key = str(row.get("screenname") or row.get("username") or "").replace("@", "").strip().lower()
        if not key:
            continue
        by_username[key] = {
            "name": str(row.get("name") or ""),
            "email": str(row.get("email") or ""),
        }

    merged: list[dict[str, Any]] = []
    found = 0
    for row in base_rows:
        hit = by_username.get(row["username"].lower(), {"name": "", "email": ""})
        item = {**row, "name": hit["name"], "email": hit["email"]}
        if item["email"]:
            found += 1
        merged.append(item)

    return merged, found


def run_followers(cfg: Config, target: str, collect_type: str, limit: int) -> dict[str, Any]:
    target_username = parse_username(target)
    follower_input = build_follower_input(target_username, collect_type, limit)
    raw = run_actor_sync(cfg.follower_actor_id, cfg.token, follower_input)
    rows = normalize_usernames(raw, target_username, collect_type, limit)

    return {
        "ok": True,
        "targetUsername": target_username,
        "collectType": collect_type,
        "limit": limit,
        "rows": rows,
        "recordsCount": len(rows),
    }


def run_pipeline(cfg: Config, target: str, collect_type: str, limit: int, include_emails: bool) -> dict[str, Any]:
    base = run_followers(cfg, target, collect_type, limit)
    rows = base["rows"]

    if not include_emails:
        final_rows = [{**r, "name": "", "email": ""} for r in rows]
        return {
            "ok": True,
            "targetUsername": base["targetUsername"],
            "collectType": collect_type,
            "includeEmails": False,
            "totalCollected": len(final_rows),
            "emailsFound": 0,
            "rows": final_rows,
        }

    email_input = build_email_input(rows, limit)
    email_raw = run_actor_sync(cfg.email_actor_id, cfg.token, email_input)
    final_rows, emails_found = merge_email_rows(rows, email_raw)

    return {
        "ok": True,
        "targetUsername": base["targetUsername"],
        "collectType": collect_type,
        "includeEmails": True,
        "totalCollected": len(final_rows),
        "emailsFound": emails_found,
        "rows": final_rows,
    }


def cmd_parse_username(args: argparse.Namespace) -> int:
    print(parse_username(args.target))
    return 0


def cmd_run_followers(args: argparse.Namespace) -> int:
    [REDACTED_SECRET])
    cfg = Config(token=token, follower_actor_id=args.follower_actor_id, email_actor_id=args.email_actor_id)
    result = run_followers(cfg, args.target, args.collect_type, args.limit)
    print(json.dumps(result, ensure_ascii=True, indent=2))
    return 0


def cmd_run_pipeline(args: argparse.Namespace) -> int:
    [REDACTED_SECRET])
    cfg = Config(token=token, follower_actor_id=args.follower_actor_id, email_actor_id=args.email_actor_id)
    result = run_pipeline(cfg, args.target, args.collect_type, args.limit, args.include_emails)
    print(json.dumps(result, ensure_ascii=True, indent=2))
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Run Twitter/X Apify actors for followers + optional emails.")
    sub = parser.add_subparsers(dest="command", required=True)

    p_user = sub.add_parser("parse-username", help="Extract username from x.com/twitter.com link or @username")
    p_user.add_argument("--target", required=True)
    p_user.set_defaults(func=cmd_parse_username)

    common = {
        "apify-token": {"help": "Apify API token (fallback: APIFY_TOKEN env)"},
        "target": {"required": True, "help": "Twitter/X profile URL or @username"},
        "collect-type": {"default": "followers", "choices": ["followers", "following", "both"]},
        "limit": {"type": int, "default": 1000},
        "follower-actor-id": {"default": FOLLOWER_ACTOR_DEFAULT},
        "email-actor-id": {"default": EMAIL_ACTOR_DEFAULT},
    }

    p_follow = sub.add_parser("run-followers", help="Run follower/following actor and return normalized usernames")
    p_follow.add_argument("--apify-token", **common["apify-token"])
    p_follow.add_argument("--target", **common["target"])
    p_follow.add_argument("--collect-type", **common["collect-type"])
    p_follow.add_argument("--limit", **common["limit"])
    p_follow.add_argument("--follower-actor-id", **common["follower-actor-id"])
    p_follow.add_argument("--email-actor-id", **common["email-actor-id"])
    p_follow.set_defaults(func=cmd_run_followers)

    p_pipe = sub.add_parser("run-pipeline", help="Run followers actor + optional email enrichment")
    p_pipe.add_argument("--apify-token", **common["apify-token"])
    p_pipe.add_argument("--target", **common["target"])
    p_pipe.add_argument("--collect-type", **common["collect-type"])
    p_pipe.add_argument("--limit", **common["limit"])
    p_pipe.add_argument("--include-emails", action="store_true")
    p_pipe.add_argument("--follower-actor-id", **common["follower-actor-id"])
    p_pipe.add_argument("--email-actor-id", **common["email-actor-id"])
    p_pipe.set_defaults(func=cmd_run_pipeline)

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    try:
        return args.func(args)
    except SkillError as exc:
        print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
        return 2
    except Exception as exc:
        if requests is not None and isinstance(exc, requests.RequestException):
            print(json.dumps({"ok": False, "error": f"Network error: {exc}"}), file=sys.stderr)
            return 3
        raise


if __name__ == "__main__":
    raise SystemExit(main())


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