Recipes: mentions, polling and datasets
Ready-to-run examples for brand mentions, cashtag and contract searches, polling without duplicates, following a set of accounts through a list, and collecting a search window to JSON Lines in parallel.
Short, complete programs for the jobs people use TwittrAPI for most. They use curl or the Python SDK with the simple format, and read the key from TWITTRAPI_API_KEY. Every page of results is one billed call; calls that fail on our side are refunded and 429 is not charged (billing).
Brand mentions
Mentions come in three shapes: the handle, the name and the domain. Catch all three, drop your own account and plain retweets:
(@acme OR "acme app" OR url:acme.com) -from:acme -filter:nativeretweetsMentions of the last day, English, without job posts and giveaways. The filters object adds operators for you:
curl -G 'https://api.twittrapi.com/search' \
-H 'X-API-KEY: YOUR_API_KEY' \
--data-urlencode 'searchItem=(@acme OR "acme app" OR url:acme.com) -from:acme' \
--data-urlencode 'category=Latest' \
--data-urlencode 'filters={"lang":"en","noneOfTheseWords":["hiring","giveaway"],"withinTime":"1d"}' \
--data-urlencode 'format=simple'Common words need context: "acme" alone matches everything, "acme" plus your product words or domain does not. For a campaign hashtag with traction only, use filters={"hashtags":["AcmeSummer"],"minimumLikesCount":20,"removeReplies":true}. All operators and filter keys: search reference.
Cashtags and contract addresses
Latest $BTC or $ETH tweets with some engagement, no replies:
curl -G 'https://api.twittrapi.com/search' \
-H 'X-API-KEY: YOUR_API_KEY' \
--data-urlencode 'searchItem=$BTC OR $ETH' \
--data-urlencode 'category=Latest' \
--data-urlencode 'filters={"minimumLikesCount":5,"removeReplies":true,"noneOfTheseWords":["giveaway","airdrop"]}' \
--data-urlencode 'format=simple'- Cashtags are plain text in the query:
$BTC,$SOL OR $JUP. The$must be URL-encoded (%24);--data-urlencodeand the SDKs do that for you. - Contract addresses are matched as text. Quote them,
"0x6982508145454ce325ddbe47a25d4ec3d2311933", and also search the project handle and domain (url:example.xyz). - Query plus filters can be up to 512 characters. Square brackets are rejected with 422.
Poll a search without duplicates
Ask for the newest tweets every 30 seconds and pass the highest ID you have as since_id: (or sinceId in filters). Each tweet arrives once, oldest first. The SDK retries 429 and 5xx with backoff. To start from a point in time instead of a tweet you have, turn the date into an ID with the tweet ID to date converter.
import time
from twittrapi import TwittrAPI, TwittrAPIError
client = TwittrAPI(response_format="simple") # key from TWITTRAPI_API_KEY
QUERY = "($BTC OR $ETH) -filter:replies"
newest = None # highest tweet ID seen so far
while True:
query = f"{QUERY} since_id:{newest}" if newest else QUERY
try:
page = client.search.tweets(query, category="Latest")
except TwittrAPIError as err: # 429 and 5xx are already retried by the SDK
print("error:", err)
time.sleep(30)
continue
tweets = [t for t in page["data"] if not t.get("unavailable")]
for t in sorted(tweets, key=lambda t: int(t["id"])): # oldest first
author = t["author"]["username"] if t["author"] else "?"
print(t["createdAt"], author, t["text"][:140])
if tweets:
newest = max((t["id"] for t in tweets), key=int)
time.sleep(30)Every poll is one call whether or not it finds something new, so the interval sets the cost. For new tweets from a fixed set of accounts within seconds, without running the loop yourself, see account monitoring.
Follow a set of accounts with one call
Put the accounts you follow on a public X list. List tweets returns the newest tweets of all members, so one call per poll covers the whole list, however many accounts are on it. The first run prints the current page; after that only new tweets.
import time
from twittrapi import TwittrAPI
client = TwittrAPI(response_format="simple")
LIST_ID = "1234567890" # a public X list with the accounts you follow
seen: set[str] = set()
while True:
page = client.lists.tweets(LIST_ID, count=20)
fresh = [t for t in page["data"] if not t.get("unavailable") and t["id"] not in seen]
for t in sorted(fresh, key=lambda t: int(t["id"])):
author = t["author"]["username"] if t["author"] else "?"
print(t["createdAt"], author, t["text"][:140], t["url"])
seen.update(t["id"] for t in fresh) # keep the newest few thousand in production
time.sleep(15)Collect a search window to JSON Lines
With the simple format every page carries nextCursor and hasNextPage, so the loop has a clear end. The SDK's paginate follows the cursor for you and retries 429 and 5xx with backoff.
import json
from twittrapi import TwittrAPI
client = TwittrAPI(response_format="simple", max_retries=5) # key from TWITTRAPI_API_KEY
def collect(query: str, day: str, next_day: str, path: str) -> int:
"""Write every tweet of one day to JSON Lines. Returns the pages fetched (= calls)."""
pages = 0
with open(path, "w", encoding="utf-8") as out:
for page in client.paginate(
client.search.tweets,
f"{query} since:{day} until:{next_day}",
category="Latest",
count=100,
max_pages=None, # follow nextCursor until hasNextPage is false
):
pages += 1
for t in page["data"]:
if t.get("unavailable"):
continue
row = {"id": t["id"], "createdAt": t["createdAt"], "lang": t["lang"],
"authorId": (t["author"] or {}).get("id"), "text": t["text"]}
out.write(json.dumps(row, ensure_ascii=False) + "\n")
return pagescountis a hint (1 to 100); X decides the real page size.- Cursors are X's and can expire after a while. If a run can be interrupted, store the last cursor with your progress and resume soon.
Going faster: split the time range
Pagination is sequential: page 2 needs the cursor from page 1. At roughly a second per response, one cursor chain gives about one page per second, however high your rate limit is. Split the range into windows (a day, an hour) and run the windows in parallel:
from concurrent.futures import ThreadPoolExecutor
from datetime import date, timedelta
QUERY = "(climate OR klima) lang:de"
days = [date(2026, 9, 1) + timedelta(d) for d in range(30)]
def run(day: date) -> None:
path = f"climate_de_{day}.jsonl"
pages = collect(QUERY, str(day), str(day + timedelta(1)), path)
print(f"{day} {pages} pages → {path}")
with ThreadPoolExecutor(max_workers=25) as pool: # stays under the included rate limit
list(pool.map(run, days))Keep the number of workers below your rate limit in requests per second. Higher limits are monthly add-ons you can book for the month of a crawl.