Back to use cases

    Use case / Developers / Python 3 / Published

    Search Public X Posts with Filters on a Local Page

    Run a local page to search public X posts with language, likes and date filters, then download the same rows as CSV. Invalid forms do not call the API.

    Card for Search Public X Posts with Filters on a Local Page

    Tweet Search is a one-page local web app for public X posts. You type a query, set filters, and see time, text, likes, replies and a link. Download CSV exports the same search. The server is the Python standard library. It calls GET /twitter. Author handles are not shown, and mentions in the text are shown as @user.

    How this differs

    Export Twitter search results to CSV is a command-line export of one query and a date window. Find X posts for a prompt is one AI links call that prints the posts it received. This example is a local page: language, likes, replies, reposts, verified, video, image, quote and dates are form fields, the results render in the browser, and a bad form returns HTTP 400 without calling the API.

    The problem

    A one-shot script is a good export. It is a poor way to try filters. Changing language, a likes floor, or a date means editing the command and running it again. A small form lets you see what came back, including rows that fall outside the filter you set, and then download that page as CSV.

    What the app does

    • One search box, plus an optional "any of" list that becomes (a OR b) in the query.
    • Filters: sort (Latest or Top), count (1 to 100), language, min likes, min replies, min reposts, verified, blue verified, video, image, quote, and a date range.
    • Checks the form before any API call. A blank query, a count outside 1 to 100, a bad language code, a negative number, a bad date, a reversed range, or a future start date returns HTTP 400 and does not call the API.
    • Shows how many posts came back versus how many you asked for, and flags rows that fall outside your filters.
    • Links use https://x.com/i/status/<id>.

    How it works

    1. Validate. core.validate builds the GET /twitter params or raises FormError.
    2. Search. The local server calls GET /twitter with the key in the Authorization header.
    3. Rows. Each post becomes time (UTC), text, likes, replies, reposts and a status link. core.row rewrites mentions to @user.
    4. CSV. /search.csv sends the same rows as text/csv.

    The code

    The query builder:

    def build_query(terms):
        """Several terms always become (a OR b). A single term is passed as-is."""
        terms = [t.strip() for t in terms if t and t.strip()]
        if not terms:
            raise FormError("Enter a search term.")
        return terms[0] if len(terms) == 1 else "(" + " OR ".join(terms) + ")"
    

    One result row:

    def row(p):
        """One public result row. No author name or handle: the link uses x.com/i/status/<id>."""
        t = parse_time(p.get("created_at"))
        u = p.get("user") or {}
        return {"id": str(p.get("id", "")),
                "created_utc": t.strftime("%Y-%m-%d %H:%M") if t else "",
                "text": HANDLE_RE.sub("@user", " ".join(str(p.get("text") or "").split())),
                "lang": p.get("lang") or "",
                "likes": p.get("like_count") or 0, "replies": p.get("reply_count") or 0,
                "reposts": p.get("retweet_count") or 0,
                "verified": bool(u.get("verified")), "blue": bool(u.get("is_blue_verified")),
                "video": has_media(p, "video"), "image": has_media(p, "photo"),
                "link": f"https://x.com/i/status/{p.get('id')}"}
    

    The page calls the API only after validation:

                if u.path not in ("/search", "/search.csv"):
                    return self._send(404, "not found", "text/plain")
                try:
                    params = core.validate(form)
    

    The key is read from DESEARCH_API_KEY and is not written into the HTML. Optional logs store Authorization: <redacted>.

    Endpoints

    • GET /twitter

    Example output

    A search on 2026-10-16 sent a term plus an "any of" group, sort Latest, count 15. The page noted 15 of 15 requested posts returned in 5.55 s and the header cost was $0.00225. Separate checks of language, minimum likes, replies, reposts, blue verified, video, quote and a three-day window each returned about 20 posts. The image filter had 1 row whose returned media had no photo. The app flagged that row.

    Run it yourself

    The public examples folder linked above includes app.py, core.py, offline tests and a README. Install its requirements, set DESEARCH_API_KEY from console.desearch.ai, run python3 app.py --port 8000, and open http://127.0.0.1:8000.

    What it costs

    The 15-post sample cost $0.00225 by X-Desearch-Cost-Usd. A separate run of 20 posts cost $0.003. A form that fails validation makes no call. See pricing.

    Ideas to extend it

    • Remember the last form in the browser so the next search starts from it.
    • Add the query string as a column in the CSV.
    • Save each response under --log-dir when you want a raw copy with the key redacted.

    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.

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

    Prompt you can paste for this example:

    Use Desearch X search with the filters I set: sort, count, language, minimum likes, replies, reposts, verified, video, image, quote and dates. Show time, text, likes, replies and an x.com/i/status link. Do not show author handles. Do not add posts you did not retrieve.

    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