Made by Desearch (desearch.ai).
This is a small, self-hosted search box for a product, a docs site, or an agent. Type a search, pick a date range, and get ranked web pages, each with its published date when the page has one, and the passage that best matches the search. An optional Answer button adds a short answer that is shown only next to the links it cites. It is one HTML page, two JSON endpoints (GET /api/search and GET /api/answer), and a CLI. Standard library only, plain HTTP.
The problem
When you add search to a product or an agent, a list of links is not enough. People want to see when a page was published and which part of it matches the search before they click. Building that yourself means crawling, ranking, date parsing and passage extraction. This app uses one search call that returns pages you can show and cite, and an optional second call for a short answer.
What the app does
- Takes a search (
q, 2 to 300 characters, at least one letter or number), a mode (fastorbalanced), a result count (10, 15 or 20) and a date range. - The range is
any,day,week,monthoryear(counted back from today, UTC), or explicitfromandtodates (YYYY-MM-DD). The page also offers custom dates, which sendfromandto. - Returns ranked pages with title, domain, link, published date and the best-matching passage (
highlightsonPOST /search). - Labels a page with no published date as "date not given", and a dated page outside the window as "outside date window".
- Shows results in the order the API returned, and also offers "newest first".
- Optional Answer step:
POST /desearch/ai/searchwithtoolsset toweb,streamingfalse,result_typeLINKS_WITH_FINAL_SUMMARYandcount10. - Serves the same search and answer through
GET /api/searchandGET /api/answerfor agents, and through a CLI.
The README notes that some results have no published date (1 to 2 of 10 on evergreen queries). With a date window set, every result in those runs was dated inside it.
How it works
- Check the input. A blank search, a search under 2 or over 300 characters, a search with no letter or number, a bad mode or count, or a
fromdate aftertoreturns HTTP 400 and makes no paid call./api/answertakesqonly. - Search.
search_bodyinsearchapp/core.pypostsquery,mode,countandhighlights: true, plusstart_dateandend_datewhen they are set. The API filters on the page's published date.anysends no dates. - Clean the results. Keep the API's order, drop duplicate URLs, take the first highlight, collapse whitespace and cut it at about 420 characters, and flag a missing date or a date outside the window. Passages are otherwise shown as returned.
- Answer (optional).
answer_bodysendsprompt,tools: ["web"],streaming: false,result_type: "LINKS_WITH_FINAL_SUMMARY"andcount: 10. The app keeps a sentence only when every citation points at a link that same answer call returned. If the service sends its fixed decline reply, the page shows that text and no summary lines. - Serve. The HTML page and the JSON endpoints share
searchapp/service.py. Result responses sendCache-Control: public, max-age=0, s-maxage=21600, so a CDN can answer a repeat for 6 hours. A cached response costs nothing.
The code
The key is read from --key-file, DESEARCH_API_KEY_FILE, or DESEARCH_API_KEY. It is sent only in the Authorization header.
The README's first call:
curl -s https://api.desearch.ai/search \
-H "Authorization: $DESEARCH_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "open source LLM released this week", "mode": "fast", "count": 10, "start_date": "2026-10-12", "end_date": "2026-10-18"}'That example does not set highlights. The app does. search_body in searchapp/core.py (lines 94 to 101):
def search_body(q, mode="fast", count=10, start=None, end=None):
"""POST /search body (live OpenAPI 2026-10-19). highlights=true returns the best-matching passage."""
b = {"query": q, "mode": mode, "count": count, "highlights": True}
if start:
b["start_date"] = start
if end:
b["end_date"] = end
return bThe Answer body, from the same file (lines 104 to 106):
def answer_body(q):
"""POST /desearch/ai/search body for the Answer button: web only, one JSON response with links + completion."""
return {"prompt": q, "tools": ["web"], "streaming": False, "result_type": "LINKS_WITH_FINAL_SUMMARY", "count": 10}searchapp/client.py sends the body with plain HTTP and sets a User-Agent (lines 39 to 42):
hdr = {"Authorization": self.key, "User-Agent": UA}
if data is not None:
hdr["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, method=method, headers=hdr)The README says a Python urllib call without a User-Agent is blocked with HTTP 403 (error code: 1010) before it reaches the API.
Files, from the README:
searchapp/core.py: input checks, request bodies, result cleanup, citation check, limiter, cap and cache. No network.searchapp/client.py: plain-HTTP client (User-Agent, key redaction guard, cost from the header or the body).searchapp/service.py: the search and answer logic shared by the local server and the hosted functions.api/search.py,api/answer.py,public/index.html,vercel.json: the hosted version.app.py: local server and CLI.test_core.py: offline tests.samples/: a saved run.
Endpoints
POST /searchPOST /desearch/ai/search
Example output
Saved sample in sample-output.md and samples/python-314-free-threading.json, from this command at 2026-10-19 09:31 Asia/Tbilisi:
python3 app.py --cli "python 3.14 free threading performance" --range year --answerThe search used fast mode and 10 results, with start_date 2025-10-19 and end_date null. The CLI prints that window as from 2025-10-19 to today. Cost $0.005, 0.48 s. The saved file reports returned 10, shown 8, duplicates 0, no date 0 and outside the window 0. Results whose URL contains a personal handle (2 of 10) are left out of the saved sample.
| Rank | Domain | Published |
|---|---|---|
| 1 | compare.iprice.ph | 2025-10-25 |
| 2 | youtube.com | 2025-11-10 |
| 3 | stackademic.com | 2026-09-18 |
| 4 | byteiota.com | 2026-07-03 |
| 5 | dev.to | 2026-04-13 |
| 6 | tech.yahoo.com | 2026-10-17 |
| 7 | c-sharpcorner.com | 2026-10-12 |
| 8 | byteiota.com | 2026-08-13 |
Each row in the app also shows the page title and its matching passage. The first result's passage begins: "Early benchmarks show dramatic improvements in request processing rates for certain workloads".
The answer call cost $0.004 and took 9.8 s. It returned 10 links. The saved run sets declined to true, and the text it shows is: "The sources do not provide specific information about Python 3.14's performance with threading." 2 of 10 answer links also appear in the search results.
Run it yourself
Clone desearch-py-examples and open ai-search-engine-web-app. Then run the README's local commands:
python3 -m pip install -r requirements-dev.txt # only pytest, for the tests
export DESEARCH_API_KEY_FILE=/path/to/key # or DESEARCH_API_KEY=your_key, or --key-file
python3 app.py # web app at http://127.0.0.1:8000The README's one-off command:
python3 app.py --cli "python 3.14 free threading performance" --range year --answer --json out.json--range day|week|month|yearlimits results by published date.fromandtoon/api/searchset explicit dates instead. The default range isany.--answeradds the cited answer. That is one more call.--json out.jsonsaves the full result.
Local checks that hit the app, from the same README block:
curl -s "http://127.0.0.1:8000/api/search?q=python+3.14+free+threading&range=year"
curl -s "http://127.0.0.1:8000/api/answer?q=python+3.14+free+threading"What it costs
In the runs recorded on 2026-10-19: $0.005 per search (POST /search, 10 results, either mode) and $0.004 per answer (POST /desearch/ai/search, web, count 10). Cached requests cost nothing. The README states that the default $1.00 daily cap allows up to 200 uncached searches a day. On the hosted version, search and answer are separate functions, and each has its own $1.00 cap. See pricing.
Ideas to extend it
- Add your site name to the search when you want pages from your own docs or blog.
- Set
start_dateto the date of the user's last search for a "since my last visit" view. - Give an agent
GET /api/searchso it can cite dated pages. - Save each search and its passages when you want a small set for checking your own ranking.
