$npx -y skills add affaan-m/ECC --skill x-apiX/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
| 1 | # X API |
| 2 | |
| 3 | Programmatic interaction with X (Twitter) for posting, reading, searching, and analytics. |
| 4 | |
| 5 | ## When to Activate |
| 6 | |
| 7 | - User wants to post tweets or threads programmatically |
| 8 | - Reading timeline, mentions, or user data from X |
| 9 | - Searching X for content, trends, or conversations |
| 10 | - Building X integrations or bots |
| 11 | - Analytics and engagement tracking |
| 12 | - User says "post to X", "tweet", "X API", or "Twitter API" |
| 13 | |
| 14 | ## Authentication |
| 15 | |
| 16 | ### OAuth 2.0 Bearer Token (App-Only) |
| 17 | |
| 18 | Best for: read-heavy operations, search, public data. |
| 19 | |
| 20 | ```bash |
| 21 | # Environment setup |
| 22 | export X_BEARER_TOKEN="your-bearer-token" |
| 23 | ``` |
| 24 | |
| 25 | ```python |
| 26 | import os |
| 27 | import requests |
| 28 | |
| 29 | bearer = os.environ["X_BEARER_TOKEN"] |
| 30 | headers = {"Authorization": f"Bearer {bearer}"} |
| 31 | |
| 32 | # Search recent tweets |
| 33 | resp = requests.get( |
| 34 | "https://api.x.com/2/tweets/search/recent", |
| 35 | headers=headers, |
| 36 | params={"query": "claude code", "max_results": 10} |
| 37 | ) |
| 38 | tweets = resp.json() |
| 39 | ``` |
| 40 | |
| 41 | ### OAuth 1.0a (User Context) |
| 42 | |
| 43 | Required for: posting tweets, managing account, DMs, and any write flow. |
| 44 | |
| 45 | ```bash |
| 46 | # Environment setup — source before use |
| 47 | export X_CONSUMER_KEY="your-consumer-key" |
| 48 | export X_CONSUMER_SECRET="your-consumer-secret" |
| 49 | export X_ACCESS_TOKEN="your-access-token" |
| 50 | export X_ACCESS_TOKEN_SECRET="your-access-token-secret" |
| 51 | ``` |
| 52 | |
| 53 | Legacy aliases such as `X_API_KEY`, `X_API_SECRET`, and `X_ACCESS_SECRET` may exist in older setups. Prefer the `X_CONSUMER_*` and `X_ACCESS_TOKEN_SECRET` names when documenting or wiring new flows. |
| 54 | |
| 55 | ```python |
| 56 | import os |
| 57 | from requests_oauthlib import OAuth1Session |
| 58 | |
| 59 | oauth = OAuth1Session( |
| 60 | os.environ["X_CONSUMER_KEY"], |
| 61 | client_secret=os.environ["X_CONSUMER_SECRET"], |
| 62 | resource_owner_key=os.environ["X_ACCESS_TOKEN"], |
| 63 | resource_owner_secret=os.environ["X_ACCESS_TOKEN_SECRET"], |
| 64 | ) |
| 65 | ``` |
| 66 | |
| 67 | ## Core Operations |
| 68 | |
| 69 | ### Post a Tweet |
| 70 | |
| 71 | ```python |
| 72 | resp = oauth.post( |
| 73 | "https://api.x.com/2/tweets", |
| 74 | json={"text": "Hello from Claude Code"} |
| 75 | ) |
| 76 | resp.raise_for_status() |
| 77 | tweet_id = resp.json()["data"]["id"] |
| 78 | ``` |
| 79 | |
| 80 | ### Post a Thread |
| 81 | |
| 82 | ```python |
| 83 | def post_thread(oauth, tweets: list[str]) -> list[str]: |
| 84 | ids = [] |
| 85 | reply_to = None |
| 86 | for text in tweets: |
| 87 | payload = {"text": text} |
| 88 | if reply_to: |
| 89 | payload["reply"] = {"in_reply_to_tweet_id": reply_to} |
| 90 | resp = oauth.post("https://api.x.com/2/tweets", json=payload) |
| 91 | tweet_id = resp.json()["data"]["id"] |
| 92 | ids.append(tweet_id) |
| 93 | reply_to = tweet_id |
| 94 | return ids |
| 95 | ``` |
| 96 | |
| 97 | ### Read User Timeline |
| 98 | |
| 99 | ```python |
| 100 | resp = requests.get( |
| 101 | f"https://api.x.com/2/users/{user_id}/tweets", |
| 102 | headers=headers, |
| 103 | params={ |
| 104 | "max_results": 10, |
| 105 | "tweet.fields": "created_at,public_metrics", |
| 106 | } |
| 107 | ) |
| 108 | ``` |
| 109 | |
| 110 | ### Search Tweets |
| 111 | |
| 112 | ```python |
| 113 | resp = requests.get( |
| 114 | "https://api.x.com/2/tweets/search/recent", |
| 115 | headers=headers, |
| 116 | params={ |
| 117 | "query": "from:affaanmustafa -is:retweet", |
| 118 | "max_results": 10, |
| 119 | "tweet.fields": "public_metrics,created_at", |
| 120 | } |
| 121 | ) |
| 122 | ``` |
| 123 | |
| 124 | ### Pull Recent Original Posts for Voice Modeling |
| 125 | |
| 126 | ```python |
| 127 | resp = requests.get( |
| 128 | "https://api.x.com/2/tweets/search/recent", |
| 129 | headers=headers, |
| 130 | params={ |
| 131 | "query": "from:affaanmustafa -is:retweet -is:reply", |
| 132 | "max_results": 25, |
| 133 | "tweet.fields": "created_at,public_metrics", |
| 134 | } |
| 135 | ) |
| 136 | voice_samples = resp.json() |
| 137 | ``` |
| 138 | |
| 139 | ### Get User by Username |
| 140 | |
| 141 | ```python |
| 142 | resp = requests.get( |
| 143 | "https://api.x.com/2/users/by/username/affaanmustafa", |
| 144 | headers=headers, |
| 145 | params={"user.fields": "public_metrics,description,created_at"} |
| 146 | ) |
| 147 | ``` |
| 148 | |
| 149 | ### Upload Media and Post |
| 150 | |
| 151 | ```python |
| 152 | # Media upload uses v1.1 endpoint |
| 153 | |
| 154 | # Step 1: Upload media |
| 155 | media_resp = oauth.post( |
| 156 | "https://upload.twitter.com/1.1/media/upload.json", |
| 157 | files={"media": open("image.png", "rb")} |
| 158 | ) |
| 159 | media_id = media_resp.json()["media_id_string"] |
| 160 | |
| 161 | # Step 2: Post with media |
| 162 | resp = oauth.post( |
| 163 | "https://api.x.com/2/tweets", |
| 164 | json={"text": "Check this out", "media": {"media_ids": [media_id]}} |
| 165 | ) |
| 166 | ``` |
| 167 | |
| 168 | ## Rate Limits |
| 169 | |
| 170 | X API rate limits vary by endpoint, auth method, and account tier, and they change over time. Always: |
| 171 | - Check the current X developer docs before hardcoding assumptions |
| 172 | - Read `x-rate-limit-remaining` and `x-rate-limit-reset` headers at runtime |
| 173 | - Back off automatically instead of relying on static tables in code |
| 174 | |
| 175 | ```python |
| 176 | import time |
| 177 | |
| 178 | remaining = int(resp.headers.get("x-rate-limit-remaining", 0)) |
| 179 | if remaining < 5: |
| 180 | reset = int(resp.headers.get("x-rate-limit-reset", 0)) |
| 181 | wait = max(0, reset - int(time.time())) |
| 182 | print(f"Rate limit approaching. Resets in {wait}s") |
| 183 | ``` |
| 184 | |
| 185 | ## Error Handling |
| 186 | |
| 187 | ```python |
| 188 | resp = oauth.post("https://api.x.com/2/tweets", json={"text": content}) |
| 189 | if resp.status_code == 201: |
| 190 | return resp.json()["data"]["id"] |
| 191 | elif resp.status_code == 429: |
| 192 | reset = |