Back to use cases

    Use case / X DATA / Python 3 / Published

    Twitter analytics for a public X account in Python

    Build twitter analytics for a public X account with Python. Export post metrics, engagement rates and a summary from retrieved posts, with a saved sample.

    A public account flowing through post retrieval into engagement metrics and a CSV report

    This Python example provides twitter analytics for one public X account. It exports post metrics and a short engagement report. This page shows a stored sample; there is no hosted demo.

    The problem

    A timeline mixes original posts, replies, quotes and reposts. Analysts need a consistent way to compare engagement across those types and see which posts contributed to the result. A follower count alone does not explain how recent posts performed.

    What the app does

    • Retrieves recent posts for one public account, with a configurable page limit.
    • Writes per-post metrics, engagement rates and exclusion reasons to CSV.
    • Summarizes medians, means, posting frequency, post types and media.
    • Lists top posts and compares weekday and hour slots in a chosen time zone.
    • Saves raw responses so the report can be rebuilt offline.

    How it works

    1. Retrieve posts. The SDK calls GET /twitter/user/posts with username and follows next_cursor. The default is three pages; --pages accepts 1 to 10. There is no page size setting.
    2. Prepare rows. Deduplicate posts by id, sort by date and classify post type and media. Preserve returned and repeated counts in a page log.
    3. Calculate rates. Engagements sum likes, reposts, replies, quotes and bookmarks. Divide by views or followers and multiply by 100. Follower rates use the follower count at run time.
    4. Apply exclusions. Leave reposts out of rates. Posts without views can still contribute to the per-follower rate. The summary lists exclusions.
    5. Export. Write a post CSV, summary CSV and markdown report. Weekday and hour comparisons use median engagements and prefer slots with at least two posts. Treat small samples as hints.

    The report covers retrieved posts, not full account history. It does not establish follower growth or complete thread coverage.

    The code

    These are verbatim excerpts from x_account_analytics.py. The key comes only from a private key file, not environment variables. Imports and now() are defined in the full script.

    PYTHON
    def read_key(path):
        p = os.path.expanduser(path)
        if not os.path.isfile(p):
            raise SystemExit(f"key file not found: {p} (put your Desearch API key in it, one line)")
        with open(p, encoding="utf-8") as f:
            k = f.read().strip()
        if not k:
            raise SystemExit(f"key file is empty: {p}")
        return k
    

    The fetch function follows cursors and records per-page metadata:

    PYTHON
    async def fetch(username, pages, key):
        import aiohttp
        from desearch_py import Desearch
        raw = {"started": now(), "username": username, "pages_requested": pages, "calls": []}
        cursor, seen_cursors = None, set()
        async with Desearch(api_key=key) as client:
            for page in range(1, pages + 1):
                at, t0 = now(), time.time()
                rec = {"page": page, "cursor_sent": cursor, "at": at}
                try:
                    res = await client.x_user_posts(username, cursor=cursor, include_metadata=True)
                    body = res.data.model_dump(mode="json", exclude_unset=True)
                    meta = res.metadata
                    rec.update(status=200, cost_usd=meta.cost_usd, usage_count=meta.usage_count, service=meta.service,
                               body_cost_usd=body.get("cost_usd"), body_usage_count=body.get("usage_count"), body=body)
                except aiohttp.ClientResponseError as e:
                    h = e.headers or {}
                    rec.update(status=e.status, error=e.message, cost_usd=h.get("X-Desearch-Cost-Usd"),
                               usage_count=h.get("X-Desearch-Usage-Count"), body=None)
                except Exception as e:  # network or validation error: record it and stop
                    rec.update(status=None, error=repr(e)[:500], body=None)
                rec["seconds"] = round(time.time() - t0, 2)
                raw["calls"].append(rec)
                n = len((rec.get("body") or {}).get("tweets") or [])
                print(f"{at} page {page} status={rec['status']} {rec['seconds']}s posts={n} cost={rec.get('cost_usd')}",
                      file=sys.stderr, flush=True)
                if rec["status"] != 200 or not n:
                    break
                cursor = rec["body"].get("next_cursor")
                if not cursor or cursor in seen_cursors:
                    break
                seen_cursors.add(cursor)
        return raw
    

    View the code

    Endpoints

    • GET /twitter/user/posts

    Example output

    This table is copied from samples/sample-summary.md, rebuilt from the stored 2026-10-19 sample. It covers 18 posts. The public sample omits posts containing outside links or other product names, so its results differ from the full live run.

    metricmedianmean
    engagements per post3.54.278
    ER per view % (n=18)4.6694.576
    ER per follower %0.2020.246

    The full live run had 43 unique posts from 44 returned posts. The table above describes only the smaller stored sample.

    Run it yourself

    Get the example folder and install its requirements from that directory:

    BASH
    pip install -r requirements.txt

    Rebuild the stored sample without a key or API calls using the README command:

    BASH
    python x_account_analytics.py --from-raw samples/sample-raw.json --out output/sample

    For live collection, create a key at the console and save it in a private file. Run x_account_analytics.py with --username set to the public account and --key-file pointing to that file. The default key path is ~/.desearch_api_key. Use --pages for the page limit, --tz for an IANA time zone and --out for the output prefix.

    What it costs

    The live three-page run on 2026-10-19 cost $0.0066 for 44 returned posts, measured from response cost metadata. It produced 43 unique posts after deduplication. The pages took 2.44, 2.42 and 1.79 seconds respectively. These are observations from that run, not a promised runtime. Rebuilding the stored sample makes no API calls. See pricing.

    Ideas to extend it

    • Save dated snapshots to compare the same account over time.
    • Compare post types across accounts using the same retrieved window.
    • Add a review column for analysts to annotate top posts.

    Use with AI agents

    Point an MCP client at the hosted Desearch server. Replace the placeholder with a key from the console. Do not commit the key.

    JSON
    {
      "mcpServers": {
        "desearch": {
          "url": "https://mcp.desearch.ai/mcp",
          "headers": {
            "Authorization": "Bearer <key>"
          }
        }
      }
    }

    Prompt you can paste for this example:

    Build twitter analytics for one public X account using Desearch GET /twitter/user/posts. Follow next_cursor within a chosen page limit, deduplicate by post id and sort by date. Sum likes, reposts, replies, quotes and bookmarks for engagements. Exclude reposts from rates. Compute engagement rates per view and per follower using followers at run time. Keep posts without views out of per-view rates. Export per-post CSV, summary CSV, markdown and raw responses. Report the retrieved window, exclusions and page log. Do not claim full account history or infer growth from one follower snapshot. Read the API key only from a private key file, as the source does.

    Build your own example.

    Give your agents access to web and social search. Get an API key to start building, or talk to us about your integration.

    Read docs