Launch Quote Ledger takes a few product launch posts on X. For each one it fetches the post by id, finds quote posts of that launch, and writes a ledger of who quoted it, what they said, and any disclosure wording in the public text, such as ad, sponsored or partner.
The problem
Quote posts are where people react to a launch, but they are spread across accounts. Opening each one, noting the text, and scanning for disclosure wording is slow when a launch has dozens of quotes.
What the app does
- Reads launch post ids from
config.json. - Loads each post with
GET /twitter/post. - Collects quote posts with
quoted_tweet_id:<id>and checks that each result quotes that id. - Scans the public text for disclosure wording and flags phrases that sit inside a negation, such as "not sponsored".
- Writes
quotes.csv, a Markdown table and a comparison of a few other query shapes.
How it works
- Load the post.
GET /twitter/post?id=<post id>returns the text, quote count and date. - Find quotes.
GET /twitterwithquoted_tweet_id:<id>,is_quote=true,countandsort=Latest. - Check the id. Each quote is kept when
quoted_status_idmatches the launch. - Scan wording. The disclosure column is text that was found, not a judgement.
- Write the ledger. Quoters in the Markdown table are shown as Account A, Account B and so on.
On 2026-10-15, every result of the quoted_tweet_id: query quoted the launch post: 60 of 60 across 3 launches. A search for the author handle with is_quote=true also returns quotes of that author's other posts, so the id filter is the one the ledger uses.
The code
def run_live(cfg):
import requests
key = api_key()
s = requests.Session(); s.headers["Authorization"] = key
raw = {"run_at": now(), "count": cfg["count"], "launches": [], "probes": []}
for L in cfg["launches"]:
url = f"https://x.com/{L['user']}/status/{L['id']}"
e = {"label": L["label"], "id": L["id"], "user": L["user"], "calls": {}}
e["calls"]["post"] = call(s, "/twitter/post", {"id": L["id"]}, f"{L['label']} post")
variants = {
"url_quote": {"query": f"url:{url}", "is_quote": "true"},
"handle_quote": {"query": f"@{L['user']}", "is_quote": "true"},
"handle_quote_blue": {"query": f"@{L['user']}", "is_quote": "true", "blue_verified": "true"},
"quoted_id_quote": {"query": f"quoted_tweet_id:{L['id']}", "is_quote": "true"},
}
for k, p in variants.items():
p = dict(p, count=cfg["count"], sort="Latest")
e["calls"][k] = call(s, "/twitter", p, f"{L['label']} {k}")
raw["launches"].append(e)
raw["probes"].append(call(s, "/twitter/post", {"id": cfg["bad_id"]}, "probe bad post id"))
raw["probes"].append(call(s, "/twitter/post", {"id": "not-a-number"}, "probe malformed post id"))
raw["probes"].append(call(s, "/twitter", {"query": cfg["empty_query"], "is_quote": "true", "count": 20}, "probe empty quote search"))
save_raw(raw, key)
return raw, key
quoted_tweet_id: worked in that run. It is an operator the example used, not a documented search feature. The key is read from the environment and is not written to the output.
Endpoints
GET /twitter/postGET /twitter
Example output
The 2026-10-15 sample is in sample-output.md. Each quoted_tweet_id: call that returned 20 quotes cost $0.003 by the response header. The CSV has one row per quote, with columns launch, variant, quote_id, quoter, quoter_blue, created_at, likes, views, disclosure_wording, negated and text.
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.
What it costs
$0.003 per 20-quote quoted_tweet_id: call in the 2026-10-15 sample. The post lookup is a separate call. See pricing.
Ideas to extend it
- Keep only quotes that include disclosure wording.
- Diff a second run to see new quotes since the last ledger.
- Add the quote count from the launch post next to the number of quotes you actually fetched.
