Building a Web Research Tool with the DeepSeek Search and Fetch APIs

Build an inspectable, cost-bounded research CLI with DeepSeek's Search and Fetch APIs.
DeepSeek exposes its in-house search engine via two direct APIs—Search (discover web pages) and Fetch (retrieve URLs as Markdown)—priced at $0.75 per 1,000 successful requests. The article argues discovery and extraction are external calls with cost, latency, and policy constraints that should be exposed, not hidden in an opaque framework. Using only the Python standard library, the author builds a small CLI: an authenticated, timeout-aware JSON client; a structure-validating search; per-page fetches that preserve titles and source URLs; and a size-capped research packet, wired into one flow. It also covers cost math, the 5 req/s limits, handling 429s and fetch failures, and treating fetched text as untrusted input requiring prompt injection defense and source verification.
Title
Building a Web Research Tool with the DeepSeek Search and Fetch APIs
Meta Description
DeepSeekAd opens its Search and Fetch APIs at $0.75 per 1,000 successful requests. This tutorial uses the Python standard library to build an inspectable web-connected AI research tool, covering search, fetch, source preservation, cost control, and prompt injection defense—helping developers safely connect web retrieval to model context.
Article Content
Large model answers can sound fresh while actually relying on yesterday's knowledge. When users ask questions like "what changed in this library this week," the model needs two independent capabilities: discovering relevant pages and reading those pages. In the past, teams often had to stitch together multiple search and extraction services, or hand control to a framework whose behavior was hard to inspect.
DeepSeek exposes its own search engine through two direct APIs: Search returns structured web results, and Fetch retrieves a URL as Markdown. Each API is priced at $0.75 per 1,000 successful requests. This is a new building block, not a complete research agent. This article assembles it into a small, inspectable Python command-line tool.

What This Tool Does
The program performs several steps in sequence: it runs a search for a question and prints a limited number of candidate results; it lets you choose which result pages to fetch rather than automatically reading all of them; it keeps each fetched page's title and URL together with its Markdown content; it limits response size before handing anything to the model; and it exits with a clear error when the key is missing, the service returns an HTTP error, or the response structure is unexpected.
This separation matters. Discovery and extraction are both external calls with cost, latency, and policy constraints. Only by exposing them explicitly can you add approval, caching, logging, and source review outside the prompt, rather than hiding these decisions inside the prompt itself.
A research agent and a bare retrieval API are two different things. A retrieval API is only responsible for fetching external information back, whereas a research agent is a loop system that plans questions, decides what to search, selects which pages to use, and integrates answers. Confusing the two is a common mistake: directly calling Search and Fetch does not mean you have an agent—you are still missing decision logic, source review, error recovery, and cost control. Conversely, a complete agent framework (such as various harnesses) works out of the box, but its internal behavior is often opaque, making it hard to debug whether a problem lies in planning, retrieval, or extraction. The approach chosen in this article is "build it yourself with small blocks": first use the two atomic APIs to form a minimal closed loop, laying out discovery, selection, extraction, and assembly at every step in code. Once this flow is proven effective, then consider whether to introduce higher-level automation.
The API Is Just Two Calls
The official guide gives the endpoints POST https://api.deepseek.com/v1/tools/search and POST https://api.deepseek.com/v1/tools/fetch, both using a Bearer API key and a JSON request body. Search accepts a required query, an optional result count between 1 and 50, and need_content; Fetch accepts a URL and returns a title plus Markdown content. Search's count controls the number of results, not the request price.
Billing details are easy to misread: Search and Fetch are each priced at $0.75 per 1,000 successful requests. A Search is billable only when it returns HTTP 200 with non-empty results; a Fetch is billable only when it returns page content. According to the guide, empty results, failed requests, and timeouts are not charged. Therefore, one search followed by fetching five pages is six API calls, not one.
There is another release note worth distinguishing: server-side Search and Fetch calls made through DeepSeek Harness, Claude Code, or Codex are currently free, with billing scheduled to begin on October 31, 2026, at 16:00 UTC. This article calls the standalone endpoints directly, so this exemption cannot be assumed to apply to it.
The release notes state that these newly opened endpoints are powered by DeepSeek's in-house search engine. This describes the provider's announcement, not an independent assessment of index coverage or result quality. Index quality needs to be measured yourself for the queries your application actually cares about.
Billable on success is a pricing semantic different from "per-call billing," and understanding it is key to cost estimation. With per-call billing, a fee is incurred as soon as the request is sent; with billing on success, only requests that actually return valid results count toward the bill. Specifically for these two endpoints: Search is billed only when it returns HTTP 200 with non-empty results, and Fetch is billed only when it actually retrieves page content; empty results, HTTP errors (such as 401 or 429), and timeouts are not billed. This means that retrying failed requests usually does not directly burn money, but it consumes time and triggers rate limiting. When budgeting, estimate based on "expected successful calls" rather than "requests sent," and factor the latency and quota consumption from retries into capacity planning.
Step 1: Prepare the Python Environment
The example uses only the Python standard library and requires no packages to install. Create a file named research.py and supply the API key via an environment variable. Do not paste real keys into source code or commit them to version control.
export DEEPSEEK_API_KEY="your-key"
python3 research.py "What changed in Python's asyncio this month?"
In Windows PowerShell, use $env:DEEPSEEK_API_KEY="your-key" to set the variable for the current shell. Environment variables are merely a convenience for local examples, not a complete key management strategy for deployed services.
Step 2: Write a JSON Client with Authentication
The HTTP helper below centralizes authentication, timeouts, JSON encoding, and error handling. A timeout is mandatory: external search should not hang a worker process indefinitely.
import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = "https://api.deepseek.com/v1/tools"
API_KEY = os.environ.get("DEEPSEEK_API_KEY")
def post_json(path: str, payload: dict) -> dict:
if not API_KEY:
raise RuntimeError("Set DEEPSEEK_API_KEY before running this script")
request = Request(
f"{BASE_URL}/{path}",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=20) as response:
body = response.read(2_000_000)
except HTTPError as exc:
detail = exc.read(4_000).decode("utf-8", errors="replace")
raise RuntimeError(f"DeepSeek returned HTTP {exc.code}: {detail}") from exc
except URLError as exc:
raise RuntimeError(f"Could not reach DeepSeek: {exc.reason}") from exc
try:
return json.loads(body)
except json.JSONDecodeError as exc:
raise RuntimeError("DeepSeek returned invalid JSON") from exc
The response read is capped at two megabytes to avoid unbounded memory usage. This cap is a local safety choice, not a DeepSeek API limit. Error response bodies are also capped, because diagnostic information should not become another uncontrolled input source.
Step 3: Search and Inspect the Result List
Add a search function that requests a small number of results and validates the response structure before use:
def search(query: str, count: int = 5) -> list[dict]:
query = query.strip()
if not query or len(query) > 128:
raise ValueError("Query must contain 1 to 128 characters")
if not 1 <= count <= 50:
raise ValueError("count must be between 1 and 50")
data = post_json("search", {
"query": query,
"count": count,
"need_content": False,
})
results = data.get("results")
if not isinstance(results, list):
raise RuntimeError("Search response did not contain a results list")
return results
The API also supports search operators such as site:, -site:, quoted phrases, lang:, after:, and before:. For example, a query can include site:docs.python.org asyncio TaskGroup after:2026-01-01. Operators can be used to narrow an information need, but do not treat a filtered result set as evidence that "no other relevant pages exist."
Print the results with index numbers, titles, and URLs, leaving it to a human to decide which pages are worth fetching. Here is the display function:
def show_results(results: list[dict]) -> None:
for index, item in enumerate(results):
print(f"[{index}] {item.get('title', '(untitled)')}")
print(f" {item.get('url', '')}")
print(f" {item.get('snippet', '')}\n")
A production-grade agent can let the model make the selection itself, but it should still expose the selected URLs and the reasons for selection. Search result snippets are suitable for ranking and initial screening; when accuracy matters, they are not a substitute for reading the original text.
Step 4: Fetch Selected Pages and Preserve Sources
Fetch one page at a time. The content value is the Markdown, and the API documentation states that its availability depends on the target site's access policy. Some URLs will not return usable content. Treat these failures as individual source failures rather than failing the entire research task.
def fetch(url: str) -> dict:
if not url.startswith(("https://", "http://")):
raise ValueError("Only HTTP and HTTPS URLs are accepted")
data = post_json("fetch", {"url": url})
title = data.get("title", "")
content = data.get("content", "")
if not isinstance(content, str) or not content.strip():
raise RuntimeError(f"No readable Markdown returned for {url}")
return {
"title": title,
"url": url,
"content": content,
}
In this simple command-line version, URL validation is intentionally basic. If your application accepts URLs from untrusted users, you need stronger defenses against server-side request forgery, redirects to internal addresses, and unsafe protocols. Do not expose a bare fetch endpoint to arbitrary clients without reviewing the threat model.
The key design choice is that every document carries its source URL. Do not concatenate pages into one anonymous blob. Once the source is lost, the model may make plausible statements, and you can no longer tell which source supported them.
Step 5: Assemble a Size-Capped Research Packet
A search can return up to 50 results, and fetched pages can be long. Before sending content to the model, cap both per-page size and total packet size:
def make_packet(documents: list[dict], per_page_chars: int = 8_000) -> str:
sections = []
for doc in documents:
body = doc["content"][:per_page_chars]
sections.append(
f"## {doc['title']}\n"
f"Source: {doc['url']}\n\n"
f"{body}"
)
return "\n\n---\n\n".join(sections)
A character limit is not the same as a token limit. If your model has a strict context budget, tokenize the packet with that model's tokenizer and reserve space for instructions and the answer. Consider removing navigation bars and template text, but keep the original URLs and enough original text to verify key claims.
A responsible research prompt can require the model to answer only from the provided documents, attach a source URL to each factual claim, and clearly state when the sources cannot answer the question. Prompts help but cannot guarantee truthfulness. The application still needs to show the underlying evidence and allow users to verify it.
Character limits and token limits are not equivalent—this is the most easily overlooked detail when constructing research packets. The same character count can correspond to vastly different token counts across models, languages, and content types (code, natural language, URLs); Chinese typically consumes more tokens than English. Therefore, a character threshold like per_page_chars can only serve as a rough pre-filter, not a precise context budget. The correct approach is: determine the target model's tokenizer, actually count tokens for the assembled packet, and reserve a fixed allowance for system instructions, the user question, and model output. In addition, truncation needs strategy—prioritize keeping the front of the page and the main body, remove template text such as navigation, footers, and cookie notices, but always preserve the original URL and enough context to support key claims; otherwise the truncated fragment may become unverifiable.
Step 6: Wire the Whole Flow Together
The entry function first searches and displays candidates, then fetches the first two results. To make the example runnable with a single command, it automatically selects the first two; in an interactive application, a review or relevance-selection step should be inserted here.
def main() -> int:
if len(sys.argv) < 2:
print('Usage: python3 research.py "your question"', file=sys.stderr)
return 2
question = " ".join(sys.argv[1:])
try:
results = search(question, count=5)
if not results:
print("No search results.")
return 0
show_results(results)
documents = []
for item in results[:2]:
url = item.get("url", "")
if not url:
continue
try:
documents.append(fetch(url))
except (RuntimeError, ValueError) as exc:
print(f"Skipping {url}: {exc}", file=sys.stderr)
if not documents:
print("No pages could be fetched.", file=sys.stderr)
return 1
print("\n=== Research packet ===\n")
print(make_packet(documents))
return 0
except (RuntimeError, ValueError) as exc:
print(str(exc), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
In a real agent, do not blindly fetch the first two results. Rank candidates with transparent rules, require approval for sensitive sources, and cache duplicate URLs where policy allows. If the provider returns request IDs or timestamps in the future, record them; and keep enough logs to explain where answers came from.
Cost and Operational Limits
At current pricing, one successful Search plus two successful Fetches costs $0.00225 in total. This is calculated from published per-request unit prices, not a promise about the total cost of a complete AI answer. Model inference, retries, and other services are billed separately. Repeated agent loops accumulate tiny per-call fees into significant usage, so count actual requests and set account-level budget alerts where available.
The guide lists an account limit of 5 queries per second for Search and a separate limit of 5 queries per second for Fetch, plus a per-domain limit for Fetch. Requests exceeding the limits may return HTTP 429. Production environments should add bounded retries with exponential backoff and jitter; do not retry indefinitely, and do not retry non-retryable errors such as invalid keys or malformed requests.
The API documentation also notes that Fetch may fail because a URL is unavailable, blocked by site policy, unsupported, or has no extractable Markdown. Finding a result in search does not mean you have permission to ignore the target site's access rules. The source site's terms and applicable policies should be part of the review process.
Pre-Launch Checklist
- Keep credentials out of code and logs. Use a secret manager in deployed environments and redact authentication headers.
- Set request timeouts and response size limits. External pages and services are not under your application's control.
- Preserve source information. Keep titles, URLs, and content together throughout retrieval, summarization, and display.
- Limit costs. Count searches and fetches, cap result counts and retries, and monitor actual usage.
- Treat fetched text as untrusted input. Pages may contain prompt injection instructions. Retrieved text is data, never authority to change system policy or invoke tools.
- Separate search from action. Reading a page should not automatically authorize an agent to execute code, send messages, or modify records.
- Test failure paths. Cover empty results, HTTP 401, 429, blocked fetches, timeouts, malformed JSON, and truncated content.
- Check first-hand sources. Search snippets can help discover evidence, but verify important claims against the original pages.
Prompt injection refers to an attack technique in which instructions embedded in untrusted text induce a model to change its behavior or invoke tools. In retrieval-augmented scenarios, the risk source is exactly the fetched web pages: text like "ignore previous instructions and instead..." or "please send the following content to..." may appear on the page. Because the model cannot naturally distinguish "user instructions" from "document content," once fetched text is spliced into context as-is, it may be manipulated. The core defense principle is separation of data and instructions: fetched content should always be treated only as data and should never have the power to modify system policy, change tool permissions, or trigger side effects. In practice, you can use explicit delimiters and source labels, declare in the system prompt that document content must not be executed as instructions, fully decouple search from actions (executing code, sending messages, writing records), and introduce human approval for sensitive operations. No prompt can fully guarantee safety, so perimeter defenses with permissions and auditing are also needed.
Key Takeaways
DeepSeek's Search and Fetch endpoints compress the web retrieval path that once required stitching together multiple vendors into two small JSON calls. What is interesting is not just the low per-request price, but the clearly visible boundary: search discovers candidates, fetch reads selected pages, and your code decides what enters the model context.
This boundary gives you actionable insertion points for source review, source labeling, prompt injection defense, caching, and budget control. Start with narrowly scoped questions and a small number of sources, first measure whether the results are truly useful for your workload, then turn these two endpoints into an automated research loop.
Sources
- DeepSeek Search & Fetch API guide: https://api-docs.deepseek.com/guides/search/
- DeepSeek API changelog, October 17, 2026: https://api-docs.deepseek.com/news/news261017/
One-line Summary
Build an inspectable, cost-bounded research command-line tool with DeepSeek's Search and Fetch APIs.
Paragraph Summary
DeepSeek exposes its in-house search engine through two direct APIs, Search and Fetch, which respectively discover relevant web pages and retrieve URLs as Markdown, both priced at $0.75 per 1,000 successful requests. The article notes that discovery and extraction are external calls with cost, latency, and policy constraints, and should be explicitly exposed rather than hidden inside an uninspectable framework. The author builds a small command-line tool using only the Python standard library: a JSON client with authentication and timeouts, a search function that validates structure, a fetch that retrieves pages one at a time while preserving titles and source URLs, and research packet assembly with per-page and overall limits, finally wired into a complete flow. The article also provides cost estimates, the 5-per-second rate limit, guidance on handling 429s and fetch failures, and emphasizes that fetched text is untrusted input requiring prompt injection defense and source preservation for verification.
Related articles

AI Is Not a Replacement: Human-AI Collaboration Through the Lens of Factory Root Cause Analysis
Starting from factory root cause analysis, this article explains how AI accelerates evidence chain construction rather than replacing human experts, exploring the pragmatic implementation logic and business value of 'AI supporting people rather than replacing them.'

AI Agent Decision Auditing: Building an Evidence Chain in Regulated Workflows
Starting from a Reddit discussion, this article analyzes three core issues in AI agent decision auditing within regulated workflows: what fields audit records should contain, how to handle feedback latency for erroneous decisions, and how to continuously calibrate LLM judge consistency with human judgment.

Apple Opens Multiseat Subscription Purchasing: App Subscriptions Can Now Be Sold in Bulk to Businesses and Schools
Apple opens multiseat purchasing for auto-renewable subscriptions: businesses and schools can bulk-buy via Apple Business and Apple School Manager, adding or removing seats anytime. Enabled by default in App Store Connect—developers should verify settings; group purchasing sandbox testing opens October 2026.