DeepSeek Search API: Wire It Into Your Coding Agent Before Oct 31 Billing

Wire the DeepSeek Search API and Fetch API into a coding agent: token-bucket limits, authority and date filters, and a hard budget gate before Oct 31 billing.

DeepSeek API Docs news post capture showing the top banner, the DeepSeek-V4.1-Flash-1017 release title, a sidebar entry dated 2026/10/17 and the benchmark table.
DeepSeek API Docs news post for DeepSeek-V4.1-Flash-1017, published October 17, 2026. The visible capture shows the top banner, the release title, a sidebar entry dated 2026/10/17 and the benchmark table; the Search & Fetch pricing and free-window announcement section sits below the fold. · Original source: https://api-docs.deepseek.com/news/news261017 · Original source

Your coding agent needs live web results, your current search vendor bills like a metered taxi, and a deadline is now running: the DeepSeek Search API and its Fetch companion opened to everyone on October 17, 2026, and per-request billing starts October 31 at 16:00 UTC. This guide wires both endpoints into an agent harness with a rate-limited client, a result-quality filter and a hard dollar ceiling, so the tool is useful on day one and does not surprise you on day fifteen. You need a DeepSeek API key, Python 3.9+, and any harness that can call a local tool or MCP server — Claude Code, Codex, or your own loop. By the end you will have a working wrapper whose every successful call is counted, filtered and priced, plus the exact decision rules for what enters your model’s context and what gets dropped at the door.

The reason to do this now rather than in November is that the free window is a real experiment you get to run on someone else’s meter for the server-side lane. Two weeks is enough time to measure quality on your own tasks, tune thresholds against results you actually see, and write the go/no-go decision down with receipts attached. Waiting until billing starts means your first data point costs money and your first tuning cycle does too.

What shipped on October 17 and what it costs

DeepSeek’s official news post announced that the company’s in-house search engine — which it says serves hundreds of millions of requests daily — now backs two standalone raw-JSON endpoints on api.deepseek.com, authenticated with the same bearer key as the model API. Each endpoint costs $0.75 per 1,000 calls, and calls made through server-side tools in DeepSeek Harness, Claude Code and Codex are free for DeepSeek API users until billing begins October 31, 2026 at 16:00 UTC (November 1 at 00:00 Beijing time in the docs’ pricing pages).

The exact contract, from the Search & Fetch guide:

Input Documented behavior Expected outcome for the operator
Search endpoint POST https://api.deepseek.com/v1/tools/search One key covers model and search
Fetch endpoint POST https://api.deepseek.com/v1/tools/fetch Returns one page as Markdown
Price per endpoint $0.75 per 1,000 successful requests Empty results and timeouts are free
Rate limit 5 QPS per endpoint, per account, independent A token bucket, not retries on hope
Query length 1–128 Unicode characters Push long context into ori_question
Result count 1–50, default 10 count does not change the price

Two consequences drive everything below. First, because only HTTP 200 responses that produce a result are billed, your wrapper should count successful calls precisely — a naive wrapper that counts attempts overstates cost, and one that ignores counts entirely discovers it on an invoice. Second, because the QPS limit is account-level regardless of which API key is used, every lane you run against the same account shares one bucket per endpoint. If you run a coordinator that fans out subagents, the pattern we covered in metering subagent fan-out applies here too: parallel searchers hit the same 5 QPS wall and start collecting 429s.

DeepSeek API Docs news post capture showing the top banner, the DeepSeek-V4.1-Flash-1017 release title, a sidebar entry dated 2026/10/17 and the benchmark table.
DeepSeek API Docs news post for DeepSeek-V4.1-Flash-1017, published October 17, 2026. The visible capture shows the top banner, the release title, a sidebar entry dated 2026/10/17 and the benchmark table; the Search & Fetch pricing and free-window announcement section sits below the fold. · Original source: https://api-docs.deepseek.com/news/news261017 · Original source

This is the second meter DeepSeek has added to a bill that used to be tokens-only. If you already run the model on off-peak and cache-hit rates per our V4.1-Flash cost runbook, search is the new line item in what we elsewhere called the three meters of an agent job: tokens, tool calls, and now retrieval calls. The announcement arrived alongside the V4.1-Flash-1017 model update, an MIT-licensed open-weights release with a 1M-token context window, and the same-day facts were independently reported by Automater News. None of that changes the integration work below; it only explains why a search layer appeared on the same bill as your model tokens.

Illustrative cost chart: each DeepSeek tool endpoint bills $0.75 per 1,000 successful calls, with the free server-side-tool window ending October 31, 2026.Illustrative cost chart: each DeepSeek tool endpoint bills $0.75 per 1,000 successful calls, with the free server-side-tool window ending October 31, 2026.
Original editorial chart of the documented per-request pricing ($0.75 per 1,000 successful requests per endpoint) against the free server-side-tool window that ends October 31, 2026 at 16:00 UTC. Costs are as documented by DeepSeek; the timeline is illustrative.

See the JSON shape first: two curl calls

Before writing any wrapper, look at the raw payload. These are the documented examples, verbatim in structure from the official guide:

# Search: expect HTTP 200 with a "results" array; each result carries
# title, url, snippet, summary, content, site_name, date, rerank_score, authority
curl https://api.deepseek.com/v1/tools/search \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "DeepSeek V4 release", "count": 5, "need_content": true}'

# Fetch: expect HTTP 200 with "title" and "content" holding the page as Markdown
# (a 404 here means no extractable content, and the call is not billed)
curl https://api.deepseek.com/v1/tools/fetch \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/article"}'

Expected observations, per the documented schema:

  1. Search returns "results": [...] — an empty array for no matches, which is a free call.
  2. Each result carries title, url, snippet (100–200 chars, for display), summary (600–800 chars, written for a model), content (empty unless need_content is true), site_name, site_icon, date (RFC 3339 UTC or null) and two ranking fields: rerank_score from 0 to 3, with 1.5 and above considered high quality, and authority from 0 to 4, where -1 means unknown and 3+ means highly authoritative.
  3. Fetch returns "title" and "content" — the page as Markdown text. A 404 from Fetch means no usable content was extractable, and it is not billed.

Run both calls once and save the output before you write a single line of wrapper code. You are looking for three things: which fields are null in practice on your queries, how long summary actually runs, and whether date is populated for the sources you care about. If your queries mostly return null dates, a date-window filter is dead weight and you should lean on rerank_score instead; if date is reliable, the window filter earns its place. That one observation decides a branch of the filter code below.

Queries keep their search syntax: site:, -site:, term and phrase exclusions, lang:, and after:/before: date bounds. A single query can combine them, e.g. DeepSeek site:github.com OR site:huggingface.co lang:en after:2025. Use after: server-side when freshness matters instead of filtering dates client-side — it removes junk before you pay attention to it, though the call costs the same either way. The ori_question field is the other underused lever: when your agent’s query is a compressed keyword string, passing the user’s original 1–128 character question alongside it gives the engine disambiguation context the query itself lost.

DeepSeek's Search & Fetch API guide capture showing the two-endpoint table and the bearer authentication table at the top of the page.
DeepSeek API Docs Search & Fetch guide. The visible capture shows the two-endpoint table (POST /v1/tools/search and /v1/tools/fetch) and the bearer authentication table; query operators, response schema, billing rules and the 5 QPS rate limits the wrapper implements appear further down the page, below the fold. · Original source: https://api-docs.deepseek.com/guides/search · Original source

The wrapper client: token bucket, backoff, budget gate

Here is the complete artifact. It is a single Python file with no dependencies beyond requests, exposing search() and fetch() that any harness or MCP host can call. Three things happen inside: a token bucket keeps you under 5 QPS per endpoint, 429/408/5xx responses back off and retry (the documented retryable set), and a per-run budget counter refuses to exceed a configured dollar ceiling by counting only billable successes.

# deepseek_tools.py — DeepSeek Search + Fetch wrapper
# Endpoints and semantics per https://api-docs.deepseek.com/guides/search
import os, time, requests

BASE = "https://api.deepseek.com/v1/tools"
PRICE_PER_CALL = 0.75 / 1000.0          # $0.75 per 1,000 successful requests, per endpoint
QPS = 5.0                               # account-level, per endpoint, independent

class TokenBucket:
    def __init__(self, rate=QPS, capacity=QPS):
        self.rate, self.capacity = rate, capacity
        self.tokens, self.last = float(capacity), time.monotonic()
    def wait(self):
        while True:
            now = time.monotonic()
            self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
            self.last = now
            if self.tokens >= 1.0:
                self.tokens -= 1.0
                return
            time.sleep((1.0 - self.tokens) / self.rate)

class DeepSeekTools:
    def __init__(self, api_key=None, budget_usd=1.00):
        self.key = api_key or os.environ["DEEPSEEK_API_KEY"]
        self.budget = budget_usd
        self.ok_search = self.ok_fetch = 0   # billable successes only
        self.buckets = {"search": TokenBucket(), "fetch": TokenBucket()}
        self.sess = requests.Session()
        self.sess.headers.update({"Authorization": f"Bearer {self.key}",
                                  "Content-Type": "application/json"})

    def spent(self):
        return (self.ok_search + self.ok_fetch) * PRICE_PER_CALL

    def _gate(self):
        if self.spent() >= self.budget:
            raise RuntimeError(f"search budget ceiling hit: ${self.spent():.4f} of ${self.budget:.2f}; "
                               f"{self.ok_search} search + {self.ok_fetch} fetch successes")

    def _post(self, which, payload):
        url, backoff = f"{BASE}/{which}", 0.5
        while True:
            self._gate()
            self.buckets[which].wait()
            r = self.sess.post(url, json=payload, timeout=30)
            if r.status_code == 200:
                return r.json()
            if r.status_code in (408, 429) or r.status_code >= 500:
                time.sleep(backoff); backoff = min(backoff * 2, 8.0); continue  # documented retryable
            r.raise_for_status()  # 400/401/402/403/404/413: fix the request, do not retry blind

    def search(self, query, count=10, need_content=False, ori_question=None, localization=None):
        assert 1 <= len(query.strip()) <= 128, "query must be 1-128 non-whitespace chars"
        body = {"query": query, "count": count, "need_content": need_content}
        if ori_question: body["ori_question"] = ori_question[:128]
        if localization: body["localization"] = localization
        data = self._post("search", body)
        results = data.get("results", [])
        if results: self.ok_search += 1        # empty results are not billed
        return results

    def fetch(self, url):
        assert len(url) <= 8192 and "://" in url and "@" not in url.split("://", 1)[1]
        data = self._post("fetch", {"url": url})
        if data.get("content"): self.ok_fetch += 1   # only content-bearing fetches bill
        return data

    def receipt(self):
        return {"search_successes": self.ok_search, "fetch_successes": self.ok_fetch,
                "spent_usd": round(self.spent(), 4), "budget_usd": self.budget,
                "overrun": self.spent() > self.budget}

Why these choices work:

  1. One bucket per endpoint. The docs state Search and Fetch limits are independent at the account level, so a fetch-heavy page crawl does not starve search; separate buckets mirror that exactly.
  2. Count successes, not attempts. ok_search increments only on a non-empty results array, and ok_fetch only when content is non-empty — matching the documented billing rule. A 429, a 404 from a paywalled page or an empty search decrements nothing.
  3. Retry only the documented retryable set. 408, 429 and 5xx back off; everything else raises, because retrying a 400 with the same body just spends your QPS.
  4. Fail closed on budget. _gate() raises before the call, so the ceiling caps spend rather than reporting an overrun after the fact.

The bucket deserves one more note. Its capacity equals the rate (five tokens), which permits a short burst of five immediate calls and then settles to one call every 200 milliseconds. That matches how an agent actually behaves — a burst of parallel tool calls from a fan-out, then quiet — without ever exceeding the account ceiling. If you later share one wrapper across processes, move the bucket into a single coordinator process; two processes each holding a private five-token bucket can jointly issue ten calls in one second and collect 429s you did not budget for.

Expected success observation on a test run: receipt() prints something like {'search_successes': 7, 'fetch_successes': 3, 'spent_usd': 0.0075, 'budget_usd': 1.0, 'overrun': False} — ten billable calls for three-quarters of a cent (illustrative arithmetic at the documented rate). Expected failure observation: set budget_usd=0.0037, run five successful searches (spent $0.00375), and the sixth call’s pre-request _gate() check — 0.00375 >= 0.0037 — raises RuntimeError: search budget ceiling hit before any HTTP request is made.

A filled trace of one worked run shows the decision points end to end:

Input Action Expected outcome
Query “DeepSeek Search API pricing”, count 5 search() with need_content=false HTTP 200, 5 ranked results, ok_search=1, $0.00075 modeled
Result list from step 1 filter_results(..., min_rerank=1.5, min_authority=2) 2 of 5 kept, sorted by rerank_score descending
Kept result URL fetch() on the top hit HTTP 200, Markdown content, ok_fetch=1
Sixth search with budget_usd=0.0037 _gate() fires pre-request (five successes already total $0.00375) RuntimeError raised, no HTTP call, no billing

Note the ordering discipline: the gate runs before the bucket wait, so a run that is already over ceiling fails immediately instead of sleeping first. That matters on overnight lanes where a stalled coordinator holding a full bucket would otherwise block a quieter lane sharing the process. It also makes the failure observable: the exception message carries the exact success counts, so a task log tells you which lane tripped the ceiling and by how much, without any dashboard archaeology.

Filter results before they reach the model context

Raw results are context poison: a 50-result count with need_content true can inject tens of thousands of tokens into your agent’s window. The response gives you the two levers for a cheap post-filter — rerank_score and authority — plus date. Here is the filter as a complete function, with concrete thresholds:

from datetime import datetime, timezone, timedelta

def filter_results(results, min_rerank=1.5, min_authority=2, within_days=None,
                   allow_unknown_authority=True, keep_top=5):
    """Drop weak results before they enter agent context.
    Thresholds: rerank >= 1.5 (docs: high quality), authority >= 2,
    date within window when given; authority -1 (unknown) passes only
    if allow_unknown_authority. Returns a trimmed, model-ready list."""
    cutoff = datetime.now(timezone.utc) - timedelta(days=within_days) if within_days else None
    kept = []
    for r in results:
        if r.get("rerank_score", 0) < min_rerank:
            continue
        auth = r.get("authority", -1)
        if auth == -1 and not allow_unknown_authority:
            continue
        if auth >= 0 and auth < min_authority:
            continue
        if cutoff and r.get("date"):
            try:
                if datetime.fromisoformat(r["date"].replace("Z", "+00:00")) < cutoff:
                    continue
            except ValueError:
                pass  # unparseable date: keep, the date field is optional
        kept.append(r)
    kept.sort(key=lambda r: r.get("rerank_score", 0), reverse=True)
    out = []
    for r in kept[:keep_top]:
        out.append({k: r.get(k) for k in ("title", "url", "summary", "date", "rerank_score", "authority")})
    return out

A filled worked example of the decision this function makes on one real-shaped response:

Result rerank_score authority date Decision Why
Official release note 2.4 3 2026-10-17 Keep High quality, highly authoritative, fresh
Vendor blog post 1.8 3 2026-06-01 Keep Passes both floors; outside a 30-day window it would drop
Forum thread 1.6 1 2026-10-16 Drop Authority 1 below floor 2
SEO scraper copy 0.9 2 2026-10-17 Drop Rerank below 1.5 high-quality line
Unindexed page 1.7 -1 null Keep Unknown authority allowed by flag

Choosing thresholds is a policy decision, not a constant. The docs define the two documented lines — rerank 1.5 as high quality, authority 3 as highly authoritative — so start at min_rerank=1.5 and treat min_authority as your skepticism dial. For a research lane summarizing a fast-moving release, min_authority=2 with a 30-day window keeps vendor blogs while dropping forum noise. For a citation lane whose output will be quoted, raise it to 3 and accept the smaller result set; fewer, stronger sources beat an exhaustive list your model cannot reason over. Flip allow_unknown_authority to false only when you would rather miss a good unindexed page than cite one you cannot vet — for internal docs work that trade is usually right, for open web research it usually is not.

Two tuning practices keep the thresholds honest. Log the drop reasons for a day — count how many results each rule removed — so when answer quality dips you know whether the authority floor or the date window is the suspect. And re-tune on your own queries, not on the vendor’s example payload: authority distributions on “kubernetes ingress annotation syntax” look nothing like distributions on a product launch, and thresholds copied from a launch-shaped demo will misfire on documentation-shaped work.

Note what the filter does not do: it does not fetch content by default. summary is documented as 600–800 characters written for a model — for ranking and citation, that plus the Fetch tool on demand beats pulling full page text for every candidate. The pipeline is search, filter, then selectively fetch only the survivors the agent actually intends to cite or quote.

Expose the tools to Claude Code, Codex or an MCP host

The wrapper is harness-agnostic because it is just two functions with a receipt. Wiring options, in order of effort:

  1. MCP server (works in Claude Code, Codex and any MCP host). Wrap search(), fetch() and receipt() as three tools — we covered why the protocol makes tool wiring portable in MCP explained. Keep the schema small: search(query, count?, need_content?), fetch(url), plus a zero-argument receipt() the agent can call to self-report spend.
  2. Direct tool definition in a custom loop. Register the same three functions in your harness’s tool table. Instruct the model, in the tool description, to call receipt() after any search-using answer — the receipt is what makes the agent’s citation of URLs and its cost visible in the transcript.
  3. Server-side tools in a named harness. If you run DeepSeek Harness, Claude Code or Codex against the DeepSeek API — which the first API call guide notes is supported directly, with deepseek-flash now served by V4.1-Flash-1017 — server-side tool calls already hit Search and Fetch and are free until the October 31 cutoff, per the news post. That path needs no wrapper, but it also gives you no per-run budget counter; treat it as the evaluation lane, and the wrapper as the production lane.

Diagram of an agent's search and fetch calls passing through token bucket, budget gate and quality filter guardrails before results enter the model context.Diagram of an agent's search and fetch calls passing through token bucket, budget gate and quality filter guardrails before results enter the model context.
Original editorial diagram: the guardrail pipeline between the agent and DeepSeek's Search and Fetch endpoints. Guardrail behavior is documented API semantics; the pipeline placement is proposed operator practice.

Success checks for any option:

  • [ ] The tool appears in the harness’s tool list on launch
  • [ ] A sample query returns filtered, ranked JSON with date and authority fields visible
  • [ ] receipt() at end of run stays under the configured ceiling
  • [ ] The agent cites fetched URLs in its final answer

One wiring detail decides whether the guardrails exist at all: the budget state must live in the wrapper process, not in the agent’s context. If you implement the ceiling as an instruction the model reads, a prompt injection or a confused turn can simply ignore it; implemented in _gate(), the ceiling is physically impossible to exceed regardless of what the model believes. The same applies to rate limiting — the agent should never see the bucket, only the wrapper’s pacing.

If you operate several agents on one machine, the counting habit belongs to the fleet layer, not just this tool: the discipline of watching what each lane spends is the core of AgentOps, and a per-run receipt is the smallest unit of that evidence you can add today.

A/B the free window, then set the go/no-go gate

The two weeks before October 31, 2026 at 16:00 UTC are a controlled experiment someone else is paying for (on the server-side-tool lane). Run it deliberately:

  1. Pick 20 representative tasks for your fleet — the ones that currently trigger your agent’s web search.
  2. Run each task twice this window: once with DeepSeek search wired in, once with your current provider, same model, same prompt.
  3. Record three numbers per task: answer quality on your own rubric, successful search+fetch call count from receipt(), and cost at the documented $0.75/1,000 rate (label these modeled numbers illustrative until your first real invoice confirms them).
  4. Set the go/no-go before October 31: for example, proceed if quality is within 10% of the incumbent and modeled search cost stays under 5% of the token cost per task. Write the decision down with the receipts attached.
  5. On November 1, re-check the pricing page — DeepSeek explicitly reserves the right to adjust prices, and the deduction rules note fees come from granted balance before topped-up balance, which matters if you are running on promotional credit.

Two details make the comparison fair. Sequence the two runs of each task in both orders across the set, so any index freshness drift during the window does not systematically favor whichever provider ran later. And score the answers blind if you can — strip the citation hostnames before rating — because knowing which provider produced an answer biases your own rubric more than any real quality gap.

Also budget your QPS during the experiment: 20 tasks at a handful of searches each is trivial, but if you point a fan-out coordinator at the lane, remember the 5 QPS account ceiling and the per-domain Fetch limit both apply while the lane is still free. A 429 storm during a free-window test tells you the bucket is missing before it costs anything — the cheapest possible time to learn that lesson.

Troubleshooting and rollback

The error surface is small and documented:

Observation Meaning Action
HTTP 429 Account QPS exceeded (Fetch also has a per-domain limit) Token bucket already handles this; if you see it, a second lane shares your account — coordinate buckets or slow the coordinator
HTTP 402 Insufficient balance Top up; granted balance is spent first
HTTP 404 on Fetch No extractable content, blocked URL or unsupported type Not billed; tell the agent to cite the search summary instead
HTTP 400 'count' must be between 1 and 50 Invalid field value Fix the request; the wrapper’s assert catches the query-length case client-side
HTTP 401 Missing or invalid key Check DEEPSEEK_API_KEY; same key serves model and tools
Empty results array Genuine no-match Free call; try query operators (site:, lang:, after:) before re-querying

Read 429s diagnostically rather than as noise. A lone 429 means your bucket was bypassed once — usually a direct call made outside the wrapper. Sustained 429s while the wrapper is in use means a second process or a second machine is drawing on the same account; the docs are explicit that the limit is account-level across every key, so the fix is coordination, not a bigger bucket. A 429 that appears only on Fetch against one domain, while other domains flow, is the documented per-domain Fetch limit — pace that host specifically or accept the backoff.

Rollback is trivial by construction: the tools are read-only per the docs, hold no state outside the run, and disable by removing them from the harness config or MCP registration. Your agent falls back to its built-in web search with no data or configuration lost. If you ran the experiment lane, the only artifact worth keeping is the receipts log — that is your evidence for the go/no-go, and it survives the tool being uninstalled.

FAQ

Does an empty search result cost money?

No. Billing counts only successful requests: Search must return a non-empty results array and Fetch must return page content. Failed requests, empty results and timeouts are not billed. That is why the wrapper counts successes rather than attempts, which would overstate modeled spend and trip the budget gate early.

Do Search and Fetch share one rate limit?

No. Each endpoint has its own account-level 5 QPS limit, calculated across every API key on the account, so the two traffic streams never consume each other’s quota. Fetch is additionally subject to a per-domain QPS limit, so crawling one site quickly can return 429 responses even while overall traffic stays below the account limit.

What changes on October 31, 2026?

Per-request billing starts at 16:00 UTC (00:00 Beijing time, November 1) for direct API calls and for server-side tool calls in DeepSeek Harness, Claude Code and Codex, at $0.75 per 1,000 successful requests per endpoint. Model token billing stays separate and unchanged, so the wrapper’s receipt and your token meter remain two distinct lines.

Sources

YOU'RE THROUGH THIS ONE.

Keep connecting the dots.

Back to the library