Reply Ledger is a small Python script that works as a Twitter reply tracker. Give it a public X handle and it pulls the account's recent posts and replies, then writes a ledger: how replies compare with original posts, who the account replies to most, reply volume by day and its best replies.
The problem
Replies are a big part of how brands and creators grow on X, but the built-in views mix them with everything else. Social and community teams want to see how much an account replies, to whom, and which replies actually got attention, for their own account or a peer.
What the app does
- Takes a public X handle and a look-back window.
- Pulls the timeline and the account's replies in that window.
- Compares replies, original posts, thread replies and reposts (median likes, views, bookmarks).
- Lists who the account replies to most and reply volume by day.
- Shows the top 10 replies by engagement with the post they answered.
- Lists busy own posts with no reply from the account in the sample.
How it works
- Timeline.
GET /twitter/user/postswith paging, newest first, until the window is covered. - Replies.
GET /twitter/replieswithuser,count=100andquery=since:<date>. - Parents.
GET /twitter/postfor the posts that the top replies answered. - Write the ledger and a snapshot with every call and its cost.
The code
def fetch(client, handle, since, pages, parents_n):
timeline, cursor, user = [], None, None
for p in range(pages):
params = {"username": handle} | ({"cursor": cursor} if cursor else {})
st, body = client.get("/twitter/user/posts", params, f"user-posts-p{p + 1}")
if st == 404 and p == 0:
return None, [], {} # handle not found: skip the replies call (it bills 10 units on empty)
if st != 200 or not isinstance(body, dict):
break
user = body.get("user") or user
tw = body.get("tweets") or []
timeline += tw
cursor = body.get("next_cursor")
oldest = min((parse_time(t.get("created_at")) for t in tw), default=None)
if not cursor or not tw or (oldest and oldest < since):
break
st, body = client.get("/twitter/replies", {"user": handle, "count": 100, "query": f"since:{since.date()}"}, "replies")
replies = body if st == 200 and isinstance(body, list) else []
tweets = in_window(own_only(merge(timeline, replies), handle), since)
parents = {}
top = sorted([t for t in tweets if kind(t, handle) == "reply"], key=engagement, reverse=True)[:parents_n]
for t in top:
pid = str(t.get("in_reply_to_status_id"))
try:
st, body = client.get("/twitter/post", {"id": pid}, f"parent-{pid}")
except RuntimeError:
break
if st == 200 and isinstance(body, dict) and body.get("text"):
parents[pid] = body
return user, tweets, parents
The folder has one script, replyledger.py, offline tests in test_replyledger.py and a real ledger in sample-output.md.
Endpoints
GET /twitter/user/postsGET /twitter/repliesGET /twitter/post
Example output
From runs on 2026-10-07: 30 days of a space agency's account took 11 calls and 121 billed units ($0.018). 14 days of a brand support account took 7 calls ($0.007). The ledger shows counts per post type, top reply targets, replies per day and the top replies by engagement.
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.
Useful flags: --days (default 30), --pages (timeline pages, default 5), --parents (parent posts to look up, default 5), --max-spend (default $0.25).
What it costs
On 2026-10-07, 30 days for one account cost $0.018 (11 calls, 121 billed units) and 14 days for another cost $0.007, from the X-Desearch-Cost-Usd headers. See pricing.
Ideas to extend it
- Compare your reply habits with two peers.
- Run it monthly and track reply share over time.
- Flag your busy posts that still have no reply from you.