X Keyword Monitor is a daily check for a few phrases on X. You keep a last-check time. Each run searches public posts, keeps only posts newer than that time, drops link-only posts, and prints the time to save for the next run. Authors are shown as a user. The rows are what Desearch returned, not a full count of the conversation.
How this differs
The Twitter complaints monitor pairs a brand with complaint words and sorts posts into themes. Export Twitter search results to CSV writes one date window to a file and does not remember the last run. This script stores a last-check time and, on the next run, keeps only posts created after it.
The problem
Typing the same X searches every morning means scrolling past posts you already saw. A keyword list without a cutoff fills up with yesterday. A community manager wants the new posts, a short summary, and a timestamp to paste in next time.
What the app does
- Reads keywords, a likes floor, a count and
last_check(UTC) fromconfig.json. - Calls
GET /twitterwith the phrases OR'd inside parentheses, from the date of the last check through today. - Keeps posts created after
last_check. - Drops posts whose text has none of the keywords, and posts that are only a link.
- Sorts what remains newest first, writes three summary lines, and prints the next last-check time.
- Shows each author as "a user".
How it works
- Search.
GET /twitterwithsort=Latest,min_likes,count,start_dateset to the date oflast_check, andend_dateset to today (YYYY-MM-DD). - Filter. The date on the request is a whole day, so the script drops posts that are not newer than the exact last-check time.
- Write
output/monitor.mdandoutput/posts.csv. CopyNext LAST CHECKback intolast_check.
The code
The query and the request:
def keyword_query(keywords):
parts = ['"%s"' % term if " " in term else term for term in keywords]
return "(" + " OR ".join(parts) + ")"
def x_params(cfg, now):
last = parse_time(cfg["last_check"])
return {
"query": keyword_query(cfg["keywords"]),
"sort": "Latest",
"start_date": last.date().isoformat(),
"end_date": now.date().isoformat(),
"min_likes": int(cfg.get("min_likes", 5)),
"count": int(cfg.get("count", 20)),
}
Link-only posts are dropped after the response:
def link_only(text):
stripped = re.sub(r"https?://\S+", " ", text or "")
stripped = re.sub(r"www\.\S+", " ", stripped)
return re.sub(r"[^\w]+", "", stripped) == ""
The live call:
def run_live(cfg, session=None, now=None):
key = api_key()
if session is None:
import requests
session = requests.Session()
session.headers["Authorization"] = key
if now is None:
now = parse_time(cfg["now"]) if cfg.get("now") else dt.datetime.now(dt.timezone.utc)
params = x_params(cfg, now)
res = session.get(BASE + "/twitter", params=params, timeout=90)
try:
body = res.json()
except ValueError:
body = {"_text": getattr(res, "text", "")[:500]}
raw = {
"run_at": now.isoformat(timespec="seconds"),
"now": now.isoformat(timespec="seconds"),
"last_check": cfg["last_check"],
"cfg": cfg,
"x": {"status": res.status_code, "body": body, "params": params},
}
return raw, key
The key is sent only in the Authorization header and is not written to the output.
Endpoints
GET /twitter
Example output
The 2026-10-15 sample used three home-coffee phrases, at least 5 likes, from 2026-10-14 11:35 UTC to 2026-10-15 11:35 UTC. 20 posts came back, 12 were newer than the last check, and 11 were kept. 1 link-only post was dropped. 8 posts were older than the last check. Authors in the public sample are shown as a user.
Run it yourself
The public examples folder linked above includes the script, config.json, a saved prompt and a README. Install its requirements, set DESEARCH_API_KEY from console.desearch.ai, and use the run command in that README. You can also paste the saved prompt into an MCP client pointed at https://mcp.desearch.ai/mcp.
What it costs
Each run is one GET /twitter call. The sample write-up does not publish a dollar total from X-Desearch-Cost-Usd. See pricing.
Ideas to extend it
- Run it on a schedule and append each CSV.
- Add a language note when you only want one language.
- Keep the dropped link-only rows in a second file.
