XActions for agents

August 28, 2026 ยท View on GitHub

X/Twitter automation with no X API key: a 56-command CLI, a Node library, 152 MCP tools, 50 agent skills, 95 browser console scripts, and a web dashboard. Apache-2.0, by nichxbt.

This file is for the agent, not the human. It answers one question first, because getting it wrong costs the whole session: do you shell out to the CLI, or do you load the MCP server?


Two lanes into the same engine

XActions ships both lanes from one install, and they call the same code. The difference is what each costs you before you have read a single tweet.

CLI laneMCP lane
How you call itone Bash call, xactions <cmd> --compacta tool call, after the client connects the server
What loads into context up frontnothingthe whole tool list
What comes backthe rows you asked for, one per linea JSON result object
Writesreads, plus engage and bulkevery write tool, with an approval gate
Usable without client configurationyes, it is just a processno, the client must be configured first

The tool list is not small. The server advertises 152 tools, and the tools/list payload it serves is about 60 KB of JSON before you have done any work at all. Measure it yourself:

node -e "import('./src/mcp/server.js').then(m => console.log(JSON.stringify(m.TOOLS).length, 'bytes,', m.TOOLS.length, 'tools'))"

That cost is worth paying when you are going to make many calls in a session, keep state between them, or write. It is not worth paying to answer "how many followers does this account have."

The rule

Reading a handful of things? Shell out. Working a long session, or writing? Load the server.

Concretely, prefer the CLI when:

  • You need one or two facts and then you are done.
  • You are inside a larger task where X is a detail, not the subject.
  • You want to pipe, filter, or count the result with jq, grep, sort, wc, or feed it to another command in the same Bash call.
  • The client has no MCP configuration and you are not going to add one.

Prefer the MCP server when:

  • You are posting, replying, following, muting, or deleting, and you want the draft-approval gate to hold each write for a human.
  • The session will make many calls and the per-call schema cost amortises.
  • You want the structured error objects and the session state the server keeps across calls.

Nothing stops you from using both. xactions mcp-config writes the client config, and the CLI keeps working next to it.


The CLI lane

The output contract

Two flags, and they are global, so every read command takes them:

  • --compact prints one record per line as tab-separated key=value pairs, essential fields only, no colours and no spinners.
  • --fields <list> narrows that to exactly the columns you name, in the order you name them.

--json is per command and prints the full structured object.

--compact wins when both are passed. They are alternatives, not a pair: pick --compact when you are going to read the answer yourself, and --json when you are going to pipe it into jq.

Field names are shared across commands, so --fields likes means the same thing everywhere. The defaults per record kind:

Record kindDefault columns
tweetid username date likes retweets replies views text
profileid username name followers following tweets verified bio
userid username name followers verified bio
mediatype url tweetUrl
reportusername followers following postsPerDay engagementRate medianEngagement mediaShare bestHourUTC bestWeekday

Anything else the record carried can still be named in --fields.

Recipes

Read a profile:

xactions profile NASA --compact
xactions profile NASA --fields username,followers,verified --compact

Read someone's posts:

xactions tweets NASA --limit 50 --compact
xactions tweets NASA --limit 200 --fields id,date,likes,text --compact
xactions tweets NASA --limit 50 --json | jq '[.[] | select(.likes > 1000)] | length'

Search:

xactions search "agent skills" --limit 40 --compact
xactions search "mcp server" --filter top --limit 25 --fields username,likes,text --compact

--filter takes latest (the default), top, people, photos, videos.

Followers, and who does not follow back:

xactions followers nichxbt --limit 500 --compact
xactions following nichxbt --limit 500 --fields username,followers --compact
xactions non-followers nichxbt --limit 500 --compact

Account report, and comparisons:

xactions analyze NASA --compact
xactions analyze NASA SpaceX --fields username,followers,engagementRate,bestHourUTC --compact

analyze takes several usernames and samples --limit posts from each (default 50, clamped to the 5 to 200 range).

The rest of the read surface, same flags:

xactions hashtag ai --limit 50 --compact
xactions thread https://x.com/NASA/status/1234567890 --compact
xactions media NASA --limit 30 --compact

One Bash call, whole answer

The point of the lane is that the shell does the second half of the work, so nothing intermediate has to pass through the context window:

# Top five posts by likes, text only
xactions tweets NASA --limit 200 --json | jq -r 'sort_by(-.likes) | .[:5] | .[] | "\(.likes)\t\(.text)"'

# How many of the accounts I follow do not follow back
xactions non-followers nichxbt --limit 1000 --compact | wc -l

# Accounts above 100k followers, from a follower list
xactions followers nichxbt --limit 1000 --json | jq -r '.[] | select(.followers > 100000) | .username'

What needs a login

Public reads work with no account: profile, tweets, thread, media, analyze, hashtag. Search, followers, following, non-followers, likes, bookmarks and DMs need a session.

xactions doctor    # what works right now, and what each failure needs
xactions connect   # log in through a real browser and save the session
xactions login     # or paste an auth_token cookie

If the machine already has a logged-in browser, do not make the user copy a cookie out of DevTools:

xactions login --from-browser chrome        # or chromium, brave, edge, arc, firefox
xactions login --cookies-file cookies.txt   # Netscape cookies.txt
xactions login --cookies-file cookies.json  # Cookie-Editor / EditThisCookie export
xactions login --cookies-file state.json    # Playwright / Puppeteer storageState

--cookies-file also takes a raw auth_token=...; ct0=... string in a file. Prefer a full cookie jar over a bare auth_token: it carries ct0, which every write needs.

Long jobs, and the accounts that survive them

One X session is worth roughly 50 GraphQL calls per operation per 15 minutes, so a follower scrape of any real size needs more than one:

  • Account pool. Sessions live in a SQLite database with their cookies, optional proxy, lock state and a per-operation rate-limit window read from X's own x-rate-limit-* response headers. The pooled client looks like a single client and rotates on its own: a 429 or a spent window moves the call to the next account, a 401 or 403 locks the account. src/scrapers/twitter/http/accountPool.js.
  • Resumable scrapes. A checkpoint JSON file is written after every page. A --limit 50000 scrape that dies at page 400 restarts from the saved cursor with the collected count already subtracted, so re-running the same command finishes the job. src/scrapers/twitter/http/checkpoint.js.

Neither needs configuring for a small read. Reach for them when a job is large enough that a rate-limit wall is a matter of when, not if.

xactions doctor is the first command to run when something returns nothing: it reports the guest tier, the saved session, the query-ID cache and the installed skills, with the fix next to each failure.


The MCP lane

xactions mcp-config                     # print the config for Claude Desktop
xactions mcp-config --client cursor     # or cursor, windsurf, vscode
xactions mcp-config --write             # write it into Claude Desktop's config file
node src/mcp/server.js                  # run it directly over stdio
node src/mcp/server.js --http --port 3000   # Streamable HTTP on /mcp instead

stdio is the default and is what a local client wants. --http (or MCP_TRANSPORT=http) serves the Streamable HTTP transport on /mcp for a remote or hosted client; set XACTIONS_MCP_TOKEN and send Authorization: Bearer <token> to require auth on it.

Do not load all 152 tools

The tool list is filterable by group, and a filtered tool is neither advertised nor callable, so the schema cost drops with it. Groups: read, write, dm, lists, spaces, analytics, ai, grok, automation, monitoring, workflows, persona, graph, data, x402, drafts, auth.

XACTIONS_MCP_TOOLS=read,analytics node src/mcp/server.js
XACTIONS_MCP_EXCLUDE=write,dm node src/mcp/server.js
node src/mcp/server.js --tools read,analytics
node src/mcp/server.js --exclude write,dm

If a session is only ever going to read, XACTIONS_MCP_TOOLS=read is the closest the MCP lane gets to the CLI lane's context cost.

Hold writes for a human

XACTIONS_MCP_REQUIRE_APPROVAL=1 node src/mcp/server.js

Every tool that posts, deletes, follows, mutes or sends is then saved as a draft instead of running. Release or bin them with the x_list_drafts, x_approve_draft and x_discard_draft tools, or from the shell:

xactions drafts list
xactions drafts show <id>
xactions drafts approve <id>
xactions drafts discard <id>

Daily action caps

Separate from approval, and always on. Every write tool call is charged against a rolling 24 hour per-account budget for its action class, and a call that would go over is refused before anything reaches X. The ledger is a JSON file under XACTIONS_HOME (default ~/.xactions), so the budget survives a restart, a crash, and a fresh npx xactions-mcp. Defaults follow X's own published limits. src/mcp/action-caps.js.

Environment

VariableEffect
XACTIONS_SESSION_COOKIEThe auth_token cookie. Without it the server runs the guest tier
XACTIONS_MODElocal (default, free, Puppeteer) or remote
XACTIONS_MCP_TOOLSAllowlist of tool names or groups
XACTIONS_MCP_EXCLUDEDenylist, applied after the allowlist
XACTIONS_MCP_REQUIRE_APPROVALHold every write tool as a draft
XACTIONS_MCP_TOKENBearer token required by the --http transport
XACTIONS_HOMEWhere sessions, checkpoints, the action ledger and the query-ID cache live (default ~/.xactions)
XACTIONS_WEBHOOK_SECRETSigning key for outbound webhook deliveries

Installing without a config file

.mcpb is a bundle the user drags onto Claude Desktop > Settings > Extensions. It carries the server and its dependencies, and its manifest prompts for the session cookie and the tool groups at install time, so nothing is typed into a config file. Built with node scripts/build-mcpb.mjs, attached to each release by .github/workflows/release-mcpb.yml.


Straight to the file

The requests that come up most, and what answers them. Every path is real; if one is not, that is a bug in this table.

RequestAnswer
Unfollow everyonesrc/unfollowEveryone.js
Unfollow people who do not follow backsrc/unfollowback.js, or xactions non-followers
Who unfollowed mesrc/detectUnfollowers.js
Download a videoscripts/videoDownloader.js
Like, repost and reply to every post on a profilescripts/engageProfile.js (browser panel) or xactions engage <user> --like --repost --comment --prompt "...". See docs/engage.md
Act on every result of a searchscripts/searchSweep.js. See docs/search-sweep.md
Train the algorithm for a nichesrc/automation/algorithmBuilder.js, or xactions persona create
Grow an account over timeskills/algorithm-cultivation/SKILL.md
Run a long-lived LLM-driven agentsrc/algorithmBuilder.js with src/personaEngine.js, via xactions persona run <id>
Read an account without logging inxactions profile <user>, xactions tweets <user>
Import an official X archivexactions archive <path-to-zip>
Hold agent writes for human approvalset XACTIONS_MCP_REQUIRE_APPROVAL=1, then xactions drafts
Log in from a browser already signed in to x.comxactions login --from-browser, or --cookies-file for an export
Watch an account in real timexactions stream start tweet <user>, or src/streaming/livePipeline.js for x.com's own event pipeline
Send an event to another servicesrc/notifications/webhook.js, signed with XACTIONS_WEBHOOK_SECRET
Keep a big scrape alive across rate limitssrc/scrapers/twitter/http/accountPool.js and checkpoint.js

Skills

50 skills live in skills/, one directory each, following the Agent Skills specification: a SKILL.md with name and description frontmatter, plus references/ where a skill needs more than one file. Read the one that matches the request before you start; each names the exact script, page and arguments for its job, and the mistakes to avoid.

xactions skills list                    # every skill, and where each is installed
xactions skills show follower-monitoring
xactions skills install --all --global  # ~/.claude/skills/<id>/
xactions skills install --all --target cursor

Any third-party installer that follows the spec resolves the same tree:

npx skills add nirholas/XActions

The catalogue and what each skill covers: docs/skills.md. The machine-readable index: skills/index.json.


Where things live

src/cli/          The xactions command. index.js registers most commands,
                  commands/ holds the ones with their own modules
src/mcp/          MCP server, tool groups, draft-approval gate
src/scrapers/     HTTP and browser scrapers
src/client/       The low-level HTTP Twitter client
src/automation/   Browser console scripts (paste core.js first)
src/a2a/          Agent-to-Agent protocol server
skills/           50 Agent Skills, one directory each
api/              Express backend (routes/, services/, middleware/)
dashboard/        Static frontend
scripts/          The 95 browser console scripts, plus build and maintenance
                  scripts (twitter/ holds standalone console variants)
docs/             Documentation
extension/        Browser extension
prisma/           Database schema

The three runtime contexts, because code that is correct in one is broken in another:

ContextRuns inEntry pointHard constraint
Browser scriptsthe DevTools console on x.coman IIFE you pasteno Node APIs; DOM and sessionStorage only
Library, CLI, MCPyour machinesrc/cli/index.js, src/mcp/server.jsNode >= 20, ESM throughout
API serveran Express processapi/server.jsPostgreSQL via Prisma, Redis for the queue

Stack: Node >= 20 ESM (CI runs 20, 22 and 24), Express with Helmet and rate limiting, Prisma, Bull on Redis, Puppeteer with the stealth plugin, Vitest, @modelcontextprotocol/sdk, Socket.io.

Deeper map: docs/, starting with docs/mcp-setup.md and docs/skills.md.


Things that will bite you

  • Browser scripts run in the DevTools console on x.com, not in Node. Paste src/automation/core.js first; it provides the config, selectors, utilities and rate limiting the others assume.
  • X's DOM changes often. Prefer data-testid selectors. Current ones: docs/agents/selectors.md.
  • Rate limits are real. Every automation path keeps delays between actions. Do not remove them to go faster; the account pays for it.
  • An empty result is an error, not a zero. The read commands throw rather than reporting "0 results", because a silent zero is indistinguishable from an account that genuinely has nothing.
  • GraphQL query IDs are refreshed from x.com's own bundles, not pinned. If every read breaks at once, xactions doctor reports the cache age.
  • Requests are signed. Every GraphQL call carries an x-client-transaction-id header computed the way x.com's own client computes it (src/scrapers/twitter/http/transactionId.js). A hand-rolled request that omits it gets a different, worse answer from X than the client does.
  • The live pipeline is not a WebSocket. x.com answers an Upgrade request with the same HTTP/2 response it gives a plain GET, never a 101. It is a streaming GET whose body is newline-delimited JSON read line by line. Do not "fix" it into a WebSocket client.

House style

  • ES modules, const over let, async/await.
  • Errors handled at the boundaries: network, user input, and the CLI surface.
  • No mocks, no stubs, no placeholder data anywhere in committed work.
  • Every file carries @author nich (@nichxbt) and the Apache-2.0 notice.
  • Third-party code is attributed in THIRD-PARTY-NOTICES.md before it is merged. Read the licence table there before adapting anything from another project.