X Trends Dashboard shows what is trending on X in several places side by side. The script asks for the top 30 trends in each place, marks trends that show up in two or more places, pulls a few of the latest posts for the top shared trends, and writes a static HTML dashboard plus CSV and JSON. Run it again with an earlier snapshot to see what moved.
The problem
A trend list for one city does not say whether that name is local. Comparing places means opening each list, matching names that differ by a hash mark or a space, and then checking again an hour later to see what changed.
What the app does
- Reads places as WOEID and name, a count from 30 to 100, and how many shared trends get sample posts.
- Pulls trends per place. A WOEID mismatch is left out of the shared comparison and is still drawn in the side-by-side table.
- Matches trend names across places, ignoring case, spaces and a leading
#. - Pulls latest posts for the top shared trends. Authors are shown as Account A, Account B and so on.
- Writes an HTML table, CSV, JSON, and a moves file when you pass an earlier snapshot.
How it works
- Trends.
GET /twitter/trends?woeid=<id>&count=30for each place. Count must be from 30 to 100. - Match. Names are normalized so
#TopicandTopiccount as one. - Posts.
GET /twitterwithsort=Latestfor the top shared trends you asked to sample. - Compare. With an earlier raw file, each row gets its previous rank or
new. - Write the dashboard. Sample post text replaces handles with
[account]and links with[link].
The code
def run_live(cfg, probes, samples):
import requests
key = api_key()
s = requests.Session(); s.headers["Authorization"] = key
raw = {"run_at": now(), "cfg": cfg, "trends": [], "x": [], "probes": []}
for w in cfg["places"]:
raw["trends"].append(call(s, "/twitter/trends", {"woeid": int(w), "count": cfg["count"]}, f"trends {w}"))
if samples:
for name, _ in shared_trends(raw)[: cfg["sample_top_shared"]]:
raw["x"].append(call(s, "/twitter", {"query": name, "sort": "Latest", "count": cfg["x_count"]}, "x " + name[:24]))
if probes:
raw["probes"].append(call(s, "/twitter", {"query": cfg["empty_query"], "sort": "Latest", "count": cfg["x_count"]}, "probe x empty result"))
raw["probes"].append(call(s, "/twitter/trends", {"woeid": 1, "count": cfg["bad_count"]}, "probe trends count=29"))
raw["probes"].append(call(s, "/twitter/trends", {"woeid": cfg["fallback_woeid"], "count": cfg["count"]}, "probe trends unknown woeid"))
return raw, key
The key comes from the environment and is not written to the output. --no-probes skips the check calls. --no-samples skips the post searches.
Endpoints
GET /twitter/trendsGET /twitter
Example output
The 2026-10-16 sample used 5 places and returned 150 trend rows. 6 trends were shared by 2 or more places. One trend appeared in all 5. A second snapshot 45 seconds later had 1 new row and 19 rows at a different rank. A full snapshot with 3 sample searches cost $0.027 by response headers.
Run it yourself
The public examples folder linked above includes the script and a README. Install its requirements, set DESEARCH_API_KEY from console.desearch.ai, and use the run command in that README. Open the HTML file in a browser for the side-by-side table.
What it costs
$0.027 for the sample snapshot that included 3 post searches, from the response headers on 2026-10-16. See pricing.
Ideas to extend it
- Run it on a timer and keep the moves CSV.
- Add the top post text under each shared trend in the HTML table.
- Highlight trends that jumped more than a set number of ranks.
