Your starting point
A repeatable three-row fixture, a source-ledger program and a review record that keeps missing evidence visible.
Before you begin. Python 3.9 or newer, a terminal and a text editor. The main exercise is offline and uses only the Python standard library. An optional API request needs a DeepSeek account and key and may incur a charge.
Product details and verification
Search and Fetch are separate authenticated JSON services; the reference documents missing dates and unavailable page content. Source ↗
The October 17 release announces standalone availability and distinguishes temporary free server-side calls from direct API pricing. Source ↗
The original ledger helper was tested locally with Python 3.9.6 on October 18, 2026. No DeepSeek API request, live extraction or complete research-agent workflow was executed.
A search response is a starting point for research. A result can have a useful title and still lack a date or readable page. Two links can also point to the same document. This exercise gives those cases explicit states before a summary is written.
You will create response.json and a small Python program, then produce ledger.json. The fixture contains three fictional search results and has exact expected outcomes. You can finish the exercise without an account or network connection. Keep its files in a fresh folder so the practice output remains separate from real research.
1. Decide what the ledger must preserve
DeepSeek announced standalone Search and Fetch access on October 17. The documented services return structured search results and extracted page text. The exercise below uses only the fields needed for an evidence ledger. It deliberately avoids copying relevance scores into a truth or popularity rating.
Our local contract is simple: keep every result’s position, title and URL; preserve an unknown date as null; identify repeated URLs; and distinguish a result with page text from one that still needs a page read. A record with text still requires human or application-level verification. The helper has no authority to approve a factual claim.
2. Save the fictional input
Create a fresh folder named search-ledger-lab. In that folder, save the following complete JSON as response.json. The example.org addresses are reserved examples; no page exists for you to verify in this exercise. The two studio URLs differ only by a fragment, which normally points to a section within a page. The third row has no content or date.
Write your expected result before running anything: three records, two distinct URL identities, the second record held as a duplicate, and the third requiring a page read. Leaving the third date unknown is an essential part of that result. A date invented from the time of your search would erase a real limitation.
{
"results": [
{
"title": "Fictional studio release",
"url": "https://example.org/studio#release",
"content": "Fictional content for a local test.",
"date": "2026-10-17T00:00:00Z"
},
{
"title": "Same release, another anchor",
"url": "https://example.org/studio#summary",
"content": "Fictional repeated content.",
"date": "2026-10-17T00:00:00Z"
},
{
"title": "Undated candidate",
"url": "https://example.org/note?id=2",
"content": "",
"date": null
}
]
}3. Create the ledger
Save the complete program below as ledger.py in the same folder. It uses only Python’s standard library. Run it from that folder so its relative input and output paths refer to the files you just created. The program never opens a URL or sends a request.
URL normalization here removes only fragments and normalizes the host’s case. Query parameters are preserved because they can select different documents. This intentionally modest rule will miss some aliases and tracking duplicates; it also avoids merging distinct articles simply because their paths resemble each other.
import json
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
def ledger(payload):
if not isinstance(payload, dict) or not isinstance(payload.get('results'), list):
raise ValueError('Expected an object containing a results array')
rows, seen = [], set()
for position, item in enumerate(payload['results'], 1):
if not isinstance(item, dict):
raise ValueError('Each result must be an object')
if not all(isinstance(item.get(k), str) for k in ('title', 'url', 'content')):
raise ValueError('Each result needs string title, url and content fields')
url = urlsplit(item['url'])
if url.scheme not in ('http', 'https') or not url.hostname or url.username or url.password:
raise ValueError('Expected an HTTP(S) URL without credentials')
# Strip fragments only. Query parameters can identify different documents.
identity = urlunsplit((url.scheme, url.netloc.lower(), url.path, url.query, ''))
duplicate = identity in seen
seen.add(identity)
date = item.get('date')
if date is not None and not isinstance(date, str):
raise ValueError('A date must be a string or null')
rows.append({
'position': position, 'title': item['title'], 'url': identity,
'date': date, 'duplicate': duplicate,
'has_content': bool(item['content'].strip()),
'review': 'HOLD_DUPLICATE' if duplicate else 'REVIEW_SOURCE'
if item['content'].strip() else 'NEEDS_PAGE',
})
return {'count': len(rows), 'unique_urls': len(seen), 'rows': rows}
if __name__ == '__main__':
payload = json.loads(Path('response.json').read_text(encoding='utf-8'))
report = ledger(payload)
# Refuse to replace a previous receipt silently.
with open('ledger.json', 'x', encoding='utf-8') as output:
json.dump(report, output, indent=2)
output.write('\n')
print(f"Saved {report['count']} rows; {report['unique_urls']} unique URLs.")python3 --version
python3 ledger.pyThe final line should be: Saved 3 rows; 2 unique URLs. Open ledger.json in your editor. Its review values should appear in this order: REVIEW_SOURCE, HOLD_DUPLICATE, NEEDS_PAGE. The last date should still be null. This is the finished machine-readable artifact for the exercise.
4. Check failure cases
Run the same command again. It should refuse to replace ledger.json with a FileExistsError. Rename that output to ledger-first.json before continuing. The deliberate refusal helps keep the first result available while you change inputs.
For an empty-results check, replace response.json temporarily with {"results": []}. Run the program again and expect zero rows and zero unique URLs. An empty search is a legitimate result to record. It should not turn into an invented article or an endless automatic retry. Move that output to ledger-empty.json, then restore the original fixture.
For an invalid-input check, change the first URL to file:///tmp/example. The command should stop with a ValueError and create no new ledger. Restore the HTTPS URL afterward. Repeat with content set to null: that also fails because this helper expects a string. These checks cover the fields used by this exercise. A production integration needs complete schema validation.
The local verification also checked a missing results array, a non-array results field, a non-object row, a URL containing credentials and two URLs with different query parameters. All behaved as intended. Those checks say nothing about the accuracy of a search result or the availability of a remote page.
5. Turn the ledger into a useful research handoff
For real research, open the selected original page through permitted access. Record which sentence supports the claim, the page’s own publication date if available, and the time you checked it. A provider summary can help you decide what to read; preserve the difference between that summary and the original evidence.
Use one record per subject and attach additional sources to it. A new article about an existing model may add an example, correction or new release event. It does not automatically create another model entry. Conversely, two products with similar names should remain distinct until primary evidence links their identity.
Subject:
Question being answered:
Original source URL:
Source publication date: unknown unless verified
Checked at:
Claim supported by the source:
Evidence location or section:
Other independent explanation:
Missing evidence:
Decision: publish / retain lead / exclude
Reason:For the fictional fixture, the appropriate final decision is retain lead because there are no real pages to inspect. That is a passing exercise outcome. The code keeps the remaining review work visible.
6. Optionally try one real search
This section was checked against documentation but was not executed. If you choose to use a real account, save the program below as live_search.py in a separate folder and run python3 live_search.py. The hidden terminal prompt asks for your key without putting it in the command line. It makes one request, writes live-response.json and stops. It has no retry loop.
Inspect the saved JSON before feeding it into the ledger. Copy it into a new exercise folder as response.json only when you are ready to review it. Preserve the request query and check time in your own receipt. A live response may have zero matches or missing fields; report those outcomes as observed.
import getpass
import json
import urllib.error
import urllib.request
from pathlib import Path
# This optional program makes one potentially billable request.
if Path("live-response.json").exists():
raise SystemExit("Move the previous live-response.json before another call.")
key = getpass.getpass("DeepSeek API key (hidden): ").strip()
if not key:
raise SystemExit("No key supplied; no request made.")
payload = {"query": "site:api-docs.deepseek.com search fetch",
"count": 3, "need_content": True}
request = urllib.request.Request(
"https://api.deepseek.com/v1/tools/search",
data=json.dumps(payload).encode("utf-8"),
headers={"Authorization": "Bearer " + key,
"Content-Type": "application/json"}, method="POST")
try:
with urllib.request.urlopen(request, timeout=30) as response:
body = response.read(2_000_001)
if len(body) > 2_000_000:
raise SystemExit("Response exceeded this example's 2 MB limit.")
parsed = json.loads(body)
except urllib.error.HTTPError as error:
raise SystemExit(f"HTTP {error.code}; no automatic retry.") from None
except (urllib.error.URLError, TimeoutError, ValueError):
raise SystemExit("Request or JSON failed; no automatic retry.") from None
with open("live-response.json", "x", encoding="utf-8") as output:
json.dump(parsed, output, indent=2)
print("Saved live-response.json. Inspect it before using the ledger helper.")7. Handle errors and know the limits
If Python cannot find response.json, check the current folder and filename. If JSON decoding fails, check commas, double quotes and brackets before retrying. If ledger.json already exists, inspect and rename it; the script intentionally preserves earlier output. A mismatch in the expected row states fails the local exercise and should be investigated before adapting the code.
For the optional remote request, stop on an authentication or balance error and inspect the account configuration. Respect a page-access rejection. Transient service or rate-limit errors need a bounded retry policy in a real application. The example does not implement one, so each later request is a separate deliberate action.
This ledger does not validate publication-date syntax, detect copied articles across domains, remove malicious page instructions or prove source independence. It also does not fetch pages, evaluate factual claims or enforce a production privacy policy. Those are separate responsibilities. Treat retrieved content as untrusted material when a model reads it, and keep it away from credentials and execution instructions.
Keep the original input, ledger, review record and any failed check together. A colleague should be able to reproduce the local result and see exactly which external behavior remains untested. When the service changes, revise the integration and retain the earlier receipt so a later comparison has a clear baseline.
Sources & next steps
Web · official
Search & Fetch API reference ↗
Read request, response, error, billing and rate-limit documentation. No API request was made.
DeepSeek · page checked 2026-10-18
Web · official
DeepSeek October 17 release ↗
Original dated announcement establishes the Search & Fetch launch and separate temporary harness pricing.
DeepSeek · page checked 2026-10-18 · Published 2026-10-17
Guide history
Revisions preserve the original instructions. A source recheck alone does not create a new revision.
Revision 2 · Simplified two sentences about validation scope and the fictional exercise outcome; all code and evidence are unchanged.
Revision 1 · New original response-ledger exercise with complete input and code, expected output, failure controls, an optional unexecuted API client and a reusable evidence review record.
Continue learning
WouldYouMerge: turn a code-review suspicion into a failing test
Practice a review from the contract through a failing test and repair. A prediction worksheet checks whether you can explain the behavior before running the code.