Install
openclaw skills install @odrobnik/x-api-v2Read and post on X (Twitter) via the official X API v2. With an app bearer token: look up a profile, read an account's recent posts (optionally without retweets or replies), read a thread its author wrote in the last 7 days, search posts from the last 7 days with X's search operators, and look up posts by URL or ID (up to 100 at once). With a one-time OAuth 2.0 sign-in in the browser: read the signed-in account's home timeline (newest first), list its bookmarks (filtered by text, or just their links), list liked posts, and publish a post, which only shows a preview until run again with --yes. The bearer token and the OAuth client come from env vars or config.json; the sign-in is stored owner-only (by default in ~/.openclaw/x), renewed automatically, and revoked by logout. Every command can also print JSON (--json). X bills API use per request. Use when the user asks to look someone up on X, read recent posts or a thread, search X, see their home timeline, bookmarks or likes, or post on X.
openclaw skills install @odrobnik/x-api-v2Look up profiles, posts and threads on X, search recent posts, read the signed-in account's bookmarks and likes, and publish posts, through the official X API v2.
Entry point: {baseDir}/scripts/x.py (Python 3 standard library only)
{baseDir} stands for this skill's folder, the one that contains this SKILL.md. Replace it with that folder's full path in every command, because commands run in the agent's workspace. In Claude Code the folder is ${CLAUDE_SKILL_DIR}, which Claude Code fills in here; OpenClaw's agent resolves {baseDir} itself; other agents use the folder where they found this file.
user, timeline, thread, search, tweet, tweets, bookmarks and likes only read. post publishes on the user's account: run it only for a post the user asked for, and only after they approved the exact text (see How to use it).See SETUP.md. Profiles, posts and search need an app bearer token from the X Developer Console. Bookmarks, likes and posting need a one-time sign-in with auth, which uses the app's OAuth 2.0 client ID (and secret).
Credentials are read from config.json in the config folder: ~/.openclaw/x by default, ${OPENCLAW_STATE_DIR}/x when OpenClaw's state folder is moved (an existing ~/.openclaw/x stays in use until its files move), or any folder named by X_SKILL_CONFIG_DIR. The env vars X_BEARER_TOKEN, X_CLIENT_ID and X_CLIENT_SECRET override it. The sign-in is stored in tokens.json in the same folder, readable by the owner only. Never print or share these files.
# Read (with the bearer token; without one, with the sign-in)
python3 {baseDir}/scripts/x.py user USERNAME # profile: bio, counts, verification
python3 {baseDir}/scripts/x.py timeline USERNAME [--max 5-100] [--exclude retweets,replies]
python3 {baseDir}/scripts/x.py thread POST_URL_OR_ID # the author's own posts in that conversation
python3 {baseDir}/scripts/x.py search "QUERY" [--max 10-100] # posts from the last 7 days
python3 {baseDir}/scripts/x.py tweet POST_URL_OR_ID
python3 {baseDir}/scripts/x.py tweets POST_URL_OR_ID [more ...] # up to 100 in one request
# The signed-in account (needs auth)
python3 {baseDir}/scripts/x.py home [--max 1-3200] [--exclude retweets,replies] # home timeline, newest first
python3 {baseDir}/scripts/x.py bookmarks [--max 1-100] [--contains "TEXT"] [--urls]
python3 {baseDir}/scripts/x.py likes USERNAME [--max 1-100]
python3 {baseDir}/scripts/x.py post "TEXT" [--yes] # without --yes: preview only, exit 3
# Sign in (one-time) and out
python3 {baseDir}/scripts/x.py auth # opens the browser, waits for the callback on localhost:8080
python3 {baseDir}/scripts/x.py auth --manual # prints the sign-in URL and exits
python3 {baseDir}/scripts/x.py auth --code "REDIRECT_ADDRESS"
python3 {baseDir}/scripts/x.py logout # revokes the sign-in at X, deletes tokens.json
USERNAME takes a name, @name or a profile link. POST_URL_OR_ID takes a post link from x.com or twitter.com, or the numeric ID.
--json)Add --json before or after the subcommand (x.py --json user NAME = x.py user NAME --json) to get machine-readable output:
2>/dev/null).url.{"error": {"message": "...", "code": "..."}} on stdout, non-zero exit (1 = failure, 2 = usage, including bad option values). code is X's error type (e.g. usage-capped, resource-not-found, invalid_request, rate_limited), or not_signed_in, missing_credentials, missing_oauth_client, unknown_account, invalid_tweet, invalid_username, not_found, network_error, filesystem_error, usage; error for anything else.| Command | JSON shape |
|---|---|
user | the user from X (id, name, username, description, created_at, verified, public_metrics) plus url |
timeline, search, likes, bookmarks | [post, ...] in X's order |
home | [post, ...] newest first; each post also has author_username, author_name and full_text (the untruncated text: a long post's note_tweet, or for a repost RT @user: plus the original's full text) |
thread | [post, ...], oldest first |
tweet | one post |
tweets | [post, ...] in the order asked; a post that can't be found is {id, found: false, error} (exit 1) |
bookmarks --urls | the links in the bookmarks, as a list of strings |
post | {ok, posted, id, text, account, url}; without --yes: {text, characters, account, ok: false, needs_confirmation: true, dry_run: true}, exit 3 |
auth --manual | {ok, auth_url, redirect_uri, next}; auth and auth --code: {ok, token_file, account, scopes, has_refresh_token} |
logout | {ok, revoked, tokens_deleted, was_signed_in} |
A post is the post from X (id, text, created_at, public_metrics, author_id and more) plus its url, and author_username when X sent the author along.
python3 {baseDir}/scripts/x.py --json search "from:XDevelopers -is:retweet" --max 10 | jq -r '.[] | .url'
python3 {baseDir}/scripts/x.py bookmarks --urls --json | jq -r '.[]'
python3 {baseDir}/scripts/x.py home --max 20 --json | jq -r '.[] | "@\(.author_username): \(.full_text)"'
post without --yes first: it shows the text, its length and the account, posts nothing, and exits with status 3. Show that preview to the user, and only after a clear yes run the same command with --yes. Never post text the user hasn't seen, and never post on your own initiative.search and thread only reach back 7 days (X's recent search). Older posts need X's full-archive search, which this skill doesn't use.thread returns the author's own posts in the conversation (a self-thread), oldest first, not other people's replies.home --max above 100 pages through in requests of up to 100 posts; X serves at most the latest 3,200 posts or 7 days.--max that answers the question, and look up many posts with one tweets call rather than many tweet calls.auth --manual: send the user the URL and ask for the address their browser shows afterwards. It starts with http://localhost:8080/callback, and that page not loading is expected. Then run auth --code "<that address>". On the user's own computer, plain auth opens the browser and finishes by itself.auth again.auth opens the browser and listens on 127.0.0.1 port 8080 for up to 5 minutes. Apart from that, the script starts no other programs and downloads no code.logout revokes it at X and deletes tokens.json.permissions list in the frontmatter states this same scope for reviewers and scanners. Hosts don't enforce it; it describes what the script does.X bills API use per request on its pay-per-use plan, from credits bought in the X Developer Console. A post or user fetched again within the same UTC day is charged once, and failed requests are free. See references/pricing.md.
Rate limits per 15 minutes:
| Request | App (bearer token) | Signed-in account |
|---|---|---|
| Profile lookup | 900 | 900 |
| An account's posts | about 1,500 | about 900 |
| Search | 450 | 300 |
| Post lookup | 900 | 900 |
| Bookmarks | none | about 180 |
| Likes | none | about 75 |
When a limit is hit, the error (code rate_limited) says when it resets.
search takes X API v2 query syntax, which differs from the search box on the X website:
| Operator | Example | Finds |
|---|---|---|
from: | from:XDevelopers | posts by that account |
to: | to:XDevelopers | replies to that account |
@ | @XDevelopers | posts that mention it |
-is:retweet | OpenClaw -is:retweet | posts that aren't retweets |
is:reply, -is:reply | from:XDevelopers -is:reply | only replies, or no replies |
has:links, has:media | OpenClaw has:links | posts with links or media |
lang: | OpenClaw lang:de | posts in that language |
"..." | "open source" | the exact phrase |
OR, ( ) | (OpenClaw OR ClawHub) -is:retweet | either term |
likes to work for the signed-in account only.