How to search X (Twitter) posts by keyword and export them
Use X post search to collect the posts for a query a page at a time, then X post detail to re-read single posts by ID or link. The worked example is 8 calls and costs $0.0109; a failed call is not charged.
Updated
The job: you want what people posted on X about a product, a brand or a topic, as rows you can sort and share. Search with the same query you would type into the X search box, page through the results, and keep the posts that matter. A day later, read the standouts again to see whether they kept spreading. You end with a JSON Lines file of posts, each with its text, author, time, link and counts for views, likes, replies, reposts and bookmarks.
No X account, developer app or cookie is involved. Each call is an HTTPS request with your key.
Step by step
Step 1: search
Call X post search with query. The query goes to X's own search as written, so the search box's operators work: "exact phrase", OR, -word, from:handle, lang:en, since:2026-11-05, until:2026-11-12, min_faves:50, -filter:replies. The operators answer lists the ones people use most. sort takes latest (the default, X's Latest tab) or top (the Top tab).
One call returns about 20 posts for $0.002, whatever operators you use. Each post has id, url, text, lang, published_at, author (with username, name, followers and verified) and metrics: views, likes, comments (replies), reposts and favorites (bookmarks). metrics.shares is always null, because X does not show it.
curl -G "https://api.okdata.online/v1/x/posts/search" \
-H "Authorization: Bearer $OKDATA_API_KEY" \
--data-urlencode 'query="standing desk" lang:en since:2026-11-05 -filter:replies' \
--data-urlencode 'sort=latest' \
--data-urlencode 'fields=id,url,text,published_at,author.username,metrics'
Use --data-urlencode for the query: quotes, colons and spaces must be encoded. For the next page, send page.next_cursor back as cursor, unchanged, and stop when page.has_more is false. view=full adds ext.quotes, ext.is_reply and ext.conversation_id to each post, at the same price.
Step 2: re-read single posts
X post detail takes a post's id or its link as url and returns the same post object for $0.0003. Two uses: posts someone sent you as links, and posts you already have whose counts you want to refresh. Views keep climbing for a day or two after a post goes up, so a second read tells you which posts still travel.
curl -G "https://api.okdata.online/v1/x/posts/get" \
-H "Authorization: Bearer $OKDATA_API_KEY" \
-d id=1988412365027741953 \
-d fields=id,metrics.views,metrics.likes,metrics.reposts
A deleted post, or one from an account that has gone protected, returns not_found and costs nothing.
Step 3: put it together in Python
The script collects up to MAX_PAGES pages into posts.jsonl and saves the IDs of the three most viewed posts. Run it again with recheck a day later to read those three again.
import json
import os
import sys
import requests
API = "https://api.okdata.online"
HEADERS = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}
QUERY = '"standing desk" lang:en since:2026-11-05 -filter:replies'
MAX_PAGES = 5 # each page is one call
def get(path, **params):
body = requests.get(API + path, headers=HEADERS, params=params, timeout=60).json()
if body["code"] != "ok":
raise RuntimeError(f"{body['code']}: {body['message']} (charged: {body['charged']})")
return body
def collect():
seen, cursor = {}, None
for _ in range(MAX_PAGES):
body = get("/v1/x/posts/search", query=QUERY, sort="latest", **({"cursor": cursor} if cursor else {}))
for p in body["data"]["items"]:
seen[p["id"]] = p
if not body["page"]["has_more"]:
break
cursor = body["page"]["next_cursor"]
with open("posts.jsonl", "w", encoding="utf-8") as f:
for p in seen.values():
f.write(json.dumps(p, ensure_ascii=False) + "\n")
top = sorted(seen.values(), key=lambda p: p["metrics"]["views"] or 0, reverse=True)[:3]
json.dump([p["id"] for p in top], open("watch.json", "w"))
print(len(seen), "posts saved")
def recheck():
for post_id in json.load(open("watch.json")):
item = get("/v1/x/posts/get", id=post_id)["data"]["item"]
print(post_id, item["metrics"]["views"], item["metrics"]["likes"], item["url"])
recheck() if sys.argv[1:] == ["recheck"] else collect()
The dictionary keyed by id drops duplicates, which matters when you run overlapping date windows.
Good to know
- Public posts only. Posts from protected accounts never come back, from search or by ID.
- Long periods. For a month or more, run one query per week or per day with
since:anduntil:andsort=latest. Short slices make it easy to see that every day was covered. - The official X API. X sells its own API pay-per-use. Its pricing page lists post reads at half a US cent per post returned, which is $5 per 1,000 posts, with reads capped at 3 million posts per monthly billing cycle; a post you already paid for is not charged again within the same UTC day. X's search documentation offers recent search over the last 7 days to all developers and full-archive search back to 2006 on pay-per-use and Enterprise. If you also post, reply or need a contract with X, use theirs.
- Retries without paying twice. Send an
Idempotency-Keyheader with each call. The same request with the same key within 24 hours returns the stored answer withmeta.replayed: trueand no new charge.
For a cost comparison with X's own API, see what the X API costs in 2026. To have an assistant write and run these queries, connect the MCP server.
What the example costs
Five pages of the latest English posts about standing desks from the past week, then the three most viewed posts read again a day later
| Endpoint | Calls | Price | Subtotal |
|---|---|---|---|
| X post search | 5 | $0.002 | $0.01 |
| X post detail | 3 | $0.0003 | $0.0009 |
| Total | 8 | $0.0109 |
Prices in force when this page was built. A failed call costs nothing.
In your AI (MCP)
Connect the MCP server once, then say what you want. The agent finds these endpoints, reads their prices and calls them on your balance.
SaySearch X for "standing desk" in English from the last 7 days, without replies, latest first, five pages. Give me a table of author, text, views, likes, reposts and link. Then re-read the three most viewed posts and tell me how their views changed.
What the agent sends to the call tool first
{
"endpoint": "x.posts.search",
"params": {
"query": "\"claude code\" lang:zh"
},
"idempotency_key": "<a new UUID>"
}