X
Read and post on X (Twitter) through 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), re…
Oliver Drobnik
@odrobnik
Install
$ openclaw skills install @odrobnik/x-api-v2X (Twitter) API v2
Look 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.
When to use
- The user asks to look up an X (Twitter) account or post, read someone's recent posts or a thread, search X, see their own bookmarks or likes, or publish a post on X.
- Not for other social networks, direct messages, or downloading media.
user,timeline,thread,search,tweet,tweets,bookmarksandlikesonly read.postpublishes 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).
Setup
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.
Commands
# 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 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 output (--json)
Add --json before or after the subcommand (x.py --json user NAME = x.py user NAME --json) to get machine-readable output:
- stdout carries exactly one JSON document, also for errors. Human-readable text, emoji and warnings go to stderr (discard with
2>/dev/null). - Pretty-printed (indent 2) when stdout is a terminal, compact one-line JSON when piped. UTF-8, not ASCII-escaped. Posts and users are passed through as X sends them, plus a
url. - Errors:
{"error": {"message": "...", "code": "..."}}on stdout, non-zero exit (1 = failure, 2 = usage, including bad option values).codeis X's error type (e.g.usage-capped,resource-not-found,invalid_request,rate_limited), ornot_signed_in,missing_credentials,missing_oauth_client,unknown_account,invalid_tweet,invalid_username,not_found,network_error,filesystem_error,usage;errorfor 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 |
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 '.[]'
How to use it
- Posting needs a clear yes. Run
postwithout--yesfirst: 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. - Bookmarks and likes are private. Show them to the user in the conversation. Don't post them, send them anywhere else, or save them to files unless the user asks for that.
searchandthreadonly reach back 7 days (X's recent search). Older posts need X's full-archive search, which this skill doesn't use.threadreturns the author's own posts in the conversation (a self-thread), oldest first, not other people's replies.- Every request costs money on X's pay-per-use plan. Use the smallest
--maxthat answers the question, and look up many posts with onetweetscall rather than manytweetcalls. - Signing in needs the user. In cloud, container or other headless sessions always use
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 runauth --code "<that address>". On the user's own computer, plainauthopens the browser and finishes by itself. - If a command reports that the sign-in expired or was revoked, run
authagain.
Security and permissions
- Network: only X. https://api.x.com for the API and for signing in and out; the sign-in page itself opens at https://x.com in the browser.
- Files: reads config.json from the config folder (default ~/.openclaw/x); reads and keeps tokens.json there and, during sign-in, pending-auth.json, both owner-only (mode 600). Setups from version 1.0 keep working: the two settings files 1.0 kept in that folder are still read. Nothing else on disk.
- Local:
authopens 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. - Sign-in: asks X for tweet.read, users.read, like.read, bookmark.read, tweet.write and offline.access (to stay signed in). It is renewed automatically;
logoutrevokes it at X and deletes tokens.json. - Declared scope: the
permissionslist in the frontmatter states this same scope for reviewers and scanners. Hosts don't enforce it; it describes what the script does.
Costs and rate limits
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 operators
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 |
Limits
- Search and thread reach back 7 days only.
- X has kept likes private since 2024, so expect
likesto work for the signed-in account only. - Posts are limited to 280 characters for most accounts. X counts each link as 23 characters and some characters (emoji, CJK) as 2; the preview shows the plain length.
- No media uploads, replies, reposts, quote posts, deleting posts, or direct messages.
Top skills in this category
Multi Search Engine
@gpyangyoujunMulti search engine integration with 16 engines (7 CN + 9 Global). Supports advanced search operators, time filters, site search, privacy engines, and Wolfra...
Searxng
@abk234Privacy-respecting metasearch using your local SearXNG instance. Search the web, images, news, and more without external API dependencies.
Browse, search, post, and moderate Reddit. Read-only works without auth; posting/moderation requires OAuth setup.
Google Search Console
@hith3shGoogle Search Console API integration with managed OAuth. Query search performance analytics, inspect URL indexing status, review sitemaps, and manage verifi...
google-ads
@jdrhyneQuery, audit, and optimize Google Ads campaigns. Use an attached browser or the Google Ads API for campaign, keyword, budget, conversion-tracking, and wasted-spend analysis, or when the user explicitly requests a reviewed account change. Defaults to read-only reporting; account mutations require a bounded preview and action-time approval.