Answers

snscrape fails on Twitter with blocked (404). Is there a working replacement?

Short answer

snscrape's Twitter modules relied on logged-out access that X closed, and there is no fix in the project. Rebuild the pipeline on X post search ($0.006 per page of about 20 posts) and X account posts ($0.006 per page). A 10,000-post research corpus costs about $3.00.

If your thesis code still calls snscrape twitter-search, it has probably been printing blocked (404) since 2023. The Twitter scrapers in snscrape read pages that X used to show to logged-out visitors. X stopped doing that, and the issue on GitHub has stayed open without a fix. The rest of your pipeline can survive: swap the collection step for an API that returns the same kind of records, and keep your cleaning and analysis code.

The replacement, command by command

snscrape Use instead
twitter-search "query" X post search, same query syntax
twitter-user handle X account posts, newest first
user profile fields X account profile

Your old field names map like this: content is text, date is published_at, likeCount is metrics.likes, retweetCount is metrics.reposts, replyCount is metrics.comments and viewCount is metrics.views. Quote counts sit in ext.quotes when you ask for view=full.

Writing JSONL the way snscrape did

import json, os, requests

API = "https://api.okdata.online/v1"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}

def pages(path, params, limit):
    cursor = None
    for _ in range(limit):
        body = requests.get(f"{API}/{path}", headers=H, timeout=60,
                            params={**params, **({"cursor": cursor} if cursor else {})}).json()
        if body["code"] != "ok":
            raise RuntimeError(f"{body['code']}: {body['message']}")
        yield from body["data"]["items"]
        if not body["page"]["has_more"]:
            return
        cursor = body["page"]["next_cursor"]

query = "climate policy lang:en since:2026-10-01 until:2026-11-01"
with open("tweets.jsonl", "w", encoding="utf-8") as out:
    for post in pages("x/posts/search", {"query": query, "sort": "latest", "view": "full"}, limit=50):
        out.write(json.dumps(post, ensure_ascii=False) + "\n")

Budget

Corpus Pages Cost
1,000 posts 50 $0.30
10,000 posts 500 $3.00
50 accounts, 5 pages each 250 $1.50

Every page is one charge. Failed calls are free, so a crash halfway through only costs the pages you actually received.

Limits worth stating in your methods section

  • You get what X search returns to the public for that query. It is neither a random sample nor a complete census.
  • Protected accounts and deleted posts are out of reach, as they were with snscrape.
  • Follower networks and reply trees are not available.

Other routes

X's official full-archive search goes back to 2006 and is open to pay-per-use accounts at $0.005 per post read. It is the cleaner choice when a reviewer will ask exactly how the data was sampled. snscrape itself still covers some other sites. Whichever route you take, keep the exact queries and collection dates with the data. More on running collections like this is in the trend research guide.

Updated