Build a Grok Web Research Agent

Build a cost-aware Grok agent that researches live web sources.

Introduction

30 Second Summary

Today’s facts can change before a confident answer reaches your screen. Without visible sources, it is hard to tell whether that answer reflects current information.

In this project, you will build a Python terminal research agent that sends current questions to Grok through the xAI Responses API. Its server-side Web Search workflow produces an inspectable research record: answer text, source URLs, tool usage, and exact request cost.

What You'll Build

You will enter a current question in macOS Terminal and see a researched answer followed by the evidence and price of the request.

By the end of this project, you'll have:

  • A reusable research prompt that accepts current-information questions from your terminal.
  • Visible research evidence that includes the returned answer plus a deduplicated list of citation URLs.
  • Per-request usage and cost reporting that shows Web Search activity, total server-side tool usage, and the exact billed cost.
  • Secret Mission: Restrict the agent to docs.x.ai and x.ai. Then verify that every returned citation stays inside that source boundary.

Are there any prerequisites?

You need internet access plus a payment method for prepaid xAI API credits. The agent reports each paid request’s exact cost to keep spending visible.

Before We Start

This is your moment to commit to building an inspectable terminal research agent. Choose the current-information topic you want to investigate first so your work has a clear purpose from the start.

Set Up xAI Access and Your Mac Workspace

Your research agent needs funded access to the xAI API before it can make a request. It also needs a local workspace that keeps your credential out of agent.py.

This step sets up API authentication with a session-only credential. You will also prepare the folder where your Python client takes shape.

In this step, get ready to:
  • Fund your xAI developer account with prepaid credits.
  • Store an API key only in the current Terminal session.
  • Create the local workspace and verify that Python launches.
Fund your xAI account

The xAI Console manages your developer account. Prepaid credits supply the balance used by API requests.

Auto top-up can purchase more credits when the balance falls. You will leave it disabled so future purchases stay manual.

You should land in the console while signed in. Your account is ready for billing setup.

  • Open Billing.
  • Select API spend management.

You should see the controls for prepaid credits. This is where you decide how much API spending to make available.

Adding a payment method can feel like a big commitment. You stay in control by choosing the prepaid balance yourself.

  • Purchase prepaid credits for this project.
  • Leave auto top-up disabled.

That money-sensitive step is complete. Your balance can fund requests.

Future credit purchases remain manual. The agent only spends credits when you run an API request.

Having trouble with billing?

  • Confirm that xAI Console shows you as signed in before returning to the billing area.
  • Use the current billing destination that manages API spending if the console layout has changed.

Keep payment details private. Help me find prepaid API credit settings in xAI Console.

Store your API key safely

An API key is a secret credential that authorizes paid requests. The XAI_API_KEY environment variable gives your future script access while the current macOS Terminal session remains open.

Protect your API key

Anyone with this key can authorize requests against your prepaid balance. Treat the reveal like a password.

  • Keep the key reveal out of screenshots.
  • Keep the real key out of agent.py.
  • Avoid sharing the key in messages or support requests.
  • Return to xAI Console from the previous substep.
  • Select the API Keys page.

You should see the page that manages your developer keys. The next action creates the credential for this project.

  • Use the page's key-creation control to create an API key.
  • Copy the generated key when it is displayed.

Your copied key currently exists in your clipboard. The next action places it in a temporary shell environment.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.

You should see the operating system search bar. Use it to find the terminal application.

  • Type Terminal into the search bar.
  • Press Enter to open Terminal.

You should see a shell prompt waiting for a command. This shell stores the key only for its current session.

  • Set the current Terminal session's key by replacing the placeholder with your copied key in this command:
export XAI_API_KEY="your_api_key"

What does this command do?

The export command makes XAI_API_KEY available to programs launched from this Terminal session.

The quotation marks keep the key together as one value. Your Python script can read that value without storing the credential in its code.

  • Keep this Terminal window open for the rest of the project.

You should see a fresh shell prompt after the command. That prompt confirms the shell accepted the environment variable assignment.

Terminal waiting for more input?

  • Check that the command has one quotation mark before the key.
  • Check that the command has one quotation mark after the key.
  • Run the corrected command again without sharing the real credential.

Keep your key private while troubleshooting. Help me set XAI_API_KEY safely.

Your credential is ready without being written to disk. Closing this Terminal session removes the environment value from that shell.

Prepare your Mac workspace

A dedicated folder keeps the research agent in one predictable location. The empty agent.py file becomes the starting point for the first request in the next step.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The cd command changes Terminal's current folder. The ~ character represents your Mac home folder.

  • Create your project folder and move into it by running these commands:
mkdir grok-research-agent
cd grok-research-agent

What do these commands do?

The first command creates the grok-research-agent folder on your Desktop.

The second command makes that folder Terminal's current location. Files created from this location belong to your research agent.

Your Terminal prompt should now end with grok-research-agent. That visible folder name confirms you moved into the workspace.

Folder already exists?

  • Run the second command from the block again to enter the existing folder.
  • Confirm that your prompt ends with grok-research-agent before continuing.

Keep the existing folder if it belongs to this project. Help me enter the project folder.

Opening the folder in Visual Studio Code turns it into your editor workspace. The Explorer sidebar then shows every project file in one place.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.

You should see the operating system search bar. Use it to find your installed editor.

  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.

You should see a Visual Studio Code window. The next actions connect it to the folder you created.

  • Click File in the top menu bar.
  • Click Open Folder....

You should see a folder picker. Use it to locate the project on your Desktop.

  • Select Desktop in the Finder sidebar.

You should see the grok-research-agent folder in the picker.

  • Select the grok-research-agent folder.
  • Click Open.

You should see grok-research-agent at the top of the Explorer sidebar. This confirms Visual Studio Code opened the correct workspace.

  • Approve the workspace trust prompt if one appears.
  • Select the Explorer icon in the left Activity Bar.
  • Use the New File control in the Explorer sidebar.

You should see a filename field beneath the workspace folder. The file created here stays inside grok-research-agent.

  • Enter agent.py in the filename field.
  • Press Enter to create the file.

You should see an empty agent.py tab. The same file should appear beneath grok-research-agent in Explorer.

Do not see agent.py?

  • Confirm that Explorer shows the grok-research-agent workspace.
  • Create agent.py from the New File control beside that workspace.
  • Check that the filename ends with .py.

Keep the file completely empty for this step. Help me create agent.py in the correct workspace.

✔️ Awesome, I've got everything!

Your empty agent.py file matches the target artifact for this step.

ⓧ I'd like to double check the full code

The exact agent.py reference for this step is a blank file with no text.

The final check confirms that Python launches from the same Terminal session that holds your API key. The interactive prompt gives you visible proof that the runtime is ready.

Before you run the check, what prompt do you expect Python to display?

  • Start the Python interpreter from the grok-research-agent folder by running this command:
python3

What does this command do?

The python3 command starts Python's interactive interpreter. Seeing >>> proves that Terminal can launch Python.

You should now see the >>> prompt. Python is waiting for input.

  • Return to the shell by entering this at the >>> prompt:
exit()

What does this command do?

The exit() command closes the interactive Python interpreter. Terminal returns to the shell inside grok-research-agent.

You saw the >>> prompt before returning to the shell. That confirms Python is ready inside your project folder.

Python prompt missing?

  • Check that you typed python3 as one word.
  • Enter exit() exactly as shown if Terminal remains at the Python prompt.

Keep the same Terminal session open after the check. Help me launch Python from Terminal.

Your funded account is ready. Your session-only credential remains outside the code.

No API request has left your Mac yet. Next, you will send a plain Grok request and inspect the live evidence it leaves out.

Ask Grok Without Agent Tools

Your prepaid credits and local workspace are ready. The next goal is to send a current-information question from agent.py.

A plain model response can sound current even when you cannot inspect its evidence. In this step, you will send a fixed question to the xAI Responses API and measure what the response provides.

In this step, get ready to:
  • Build a JSON payload for a fixed current-information question.
  • Send your first request to the xAI Responses API.
  • Inspect the evidence returned with the answer.
Build the plain request

The Python standard library can prepare the request without extra packages. The payload contains a model name plus the question that Grok receives.

  • Switch back to agent.py in Visual Studio Code.
  • Add the request configuration by pasting this code:
import json
import os
import urllib.request

API_URL = "https://api.x.ai/v1/responses"
MODEL = "grok-4.6"

api_key = os.getenv("XAI_API_KEY")
if not api_key:
    raise SystemExit("XAI_API_KEY is not set in this Terminal session.")

question = "What are the latest developments in AI agents?"
payload = {
    "model": MODEL,
    "input": [
        {
            "role": "user",
            "content": question,
        }
    ],
}

What does this code do?

  • The imports provide tools for JSON conversion, environment access, and HTTP requests.
  • API_URL identifies the Responses API endpoint.
  • MODEL selects grok-4.6 for the request.
  • os.getenv("XAI_API_KEY") reads the API key from your current Terminal session.
  • payload packages the fixed question for Grok without an agent tool configuration.
  • Save agent.py.
  • Confirm that payload has two top-level keys named model plus input.

Seeing editor warnings?

  • Check that each opening brace has a matching closing brace.
  • Check that the lines inside payload use the same indentation as the reference.

Ask for help with the payload structure:

Send the request and extract the answer

A REST API request sends the JSON payload to xAI over HTTP. The returned answer text lives inside response items whose content type is output_text.

  • Add the HTTP request and answer parser below the existing payload block by pasting this code:
request = urllib.request.Request(
    API_URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    data = json.loads(response.read().decode("utf-8"))

answer_parts = []
for item in data.get("output", []):
    if item.get("type") == "message":
        for content in item.get("content", []):
            if content.get("type") == "output_text":
                answer_parts.append(content.get("text", ""))

print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")

How does the request work?

  • urllib.request.Request creates a POST request containing the encoded payload.
  • Authorization sends the API key through a bearer header.
  • urlopen sends the request and returns the response.
  • answer_parts collects text from every returned output_text block.
  • join combines those parts into one readable answer.
  • Save agent.py.
  • Return to the Terminal from earlier.
  • Send your first API request by running this command:
python3 agent.py

What should I see?

You should see an Answer: heading followed by Grok's response to the fixed question. This confirms that authentication, request delivery, and answer parsing work.

Request not completing?

  • Confirm that you returned to the same Terminal session where you set XAI_API_KEY.
  • Confirm that Terminal is still inside the grok-research-agent folder.
  • Confirm that you saved agent.py before running it.

Ask for help diagnosing the failed request:

Expose the missing evidence

Citations expose the source URLs behind an answer. The server-side tool count reveals whether a server tool ran during the request.

  • Find these output lines near the bottom of agent.py:
print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")

What does the current output show?

These lines print only the model's answer. They provide no visible source state or server-side tool count.

  • Replace those lines with this evidence-reporting block:
citation_urls = []
usage = data.get("usage", {})
server_side_tools = usage.get("num_server_side_tools_used", 0)

print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")
print("\nSources:")
print("No citation URLs were returned.")
print(f"\nServer-side tools used: {server_side_tools}")

What does the evidence report show?

  • citation_urls records the current empty citation state.
  • usage reads the request's usage details.
  • server_side_tools reads the number of server-side tools used.
  • The final lines print the answer, source state, and server-side tool count as separate results.
  • Save agent.py.

Before you run it, do you think the confident answer will come with inspectable live-source evidence?

  • Run the completed plain request by using this command:
python3 agent.py

What does the result prove?

You should see an answer under Answer:. Under Sources:, you should see No citation URLs were returned..

Server-side tools used: 0 confirms that no server-side tool supported this request.

That is the designed shortfall. The answer has no live-source evidence you can inspect.

Good work. Your first API request now makes its missing evidence visible.

Seeing the API key message?

  • Return to the Terminal session where XAI_API_KEY is set.
  • Run the script again from the grok-research-agent folder.

Ask for help restoring the session-only API key safely:

✔️ My code matches

Your file matches the finished plain-request version for this step.

ⓧ I'd like to double check the full code

  • Compare your saved agent.py with this complete reference:
import json
import os
import urllib.request

API_URL = "https://api.x.ai/v1/responses"
MODEL = "grok-4.6"

api_key = os.getenv("XAI_API_KEY")
if not api_key:
    raise SystemExit("XAI_API_KEY is not set in this Terminal session.")

question = "What are the latest developments in AI agents?"
payload = {
    "model": MODEL,
    "input": [
        {
            "role": "user",
            "content": question,
        }
    ],
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    data = json.loads(response.read().decode("utf-8"))

answer_parts = []
for item in data.get("output", []):
    if item.get("type") == "message":
        for content in item.get("content", []):
            if content.get("type") == "output_text":
                answer_parts.append(content.get("text", ""))

citation_urls = []
usage = data.get("usage", {})
server_side_tools = usage.get("num_server_side_tools_used", 0)

print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")
print("\nSources:")
print("No citation URLs were returned.")
print(f"\nServer-side tools used: {server_side_tools}")

How to use this reference

The reference shows the exact completed file for this step. Matching it preserves the plain request that produced the empty evidence state.

You have proved that a fluent response can arrive without inspectable current evidence. Next, you will give Grok Web Search and compare the evidence-rich result.

Give Grok Web Search

Your plain Grok request already returns an answer. Its empty sources list shows that the response has no live evidence you can inspect.

The xAI Responses API can give Grok Web Search as a server-side tool. That tool gives the response a path to current information with traceable sources.

In this step, get ready to:
  • Enable server-side Web Search in the request.
  • Extract research activity and unique citation URLs.
  • Compare the researched result with the unsupported response from Step 2.
Enable Web Search

A tools array advertises the capabilities Grok can choose for a request. Adding the Web Search object gives Grok access to live web information.

  • In agent.py, find the payload block.
  • Insert this tools array immediately after the input list:
    "tools": [
        {
            "type": "web_search",
        }
    ],

What does this code do?

The tools array becomes part of the request's JSON body. The web_search object makes live search available without forcing Grok to use it a fixed number of times.

Grok decides whether Web Search is useful for the question. This is the agentic choice you can inspect through the response usage data.

  • Save agent.py.

This next request consumes prepaid xAI credits. Since auto top-up is disabled, this run cannot trigger an automatic credit purchase.

Before you run it, do you think the server-side tool count will stay at 0 or increase?

  • Send the same current-information question by running this command:
python3 agent.py

What should I see?

You should see a researched answer. The value beside Server-side tools used should be positive when Grok chooses Web Search.

The Sources section still prints its fixed message because the script does not read citation annotations yet. The positive tool count proves that live research is now available.

That is the pivotal shift. Your request now gives Grok a live research capability.

Server-side tool count still at zero?

  • Check that the tools array sits inside payload.
  • Check that web_search uses lowercase letters with an underscore.
  • Confirm that your current Terminal session still has XAI_API_KEY set.

Help me diagnose why my xAI Responses API request reports zero server-side tools.

Read the research evidence

The response can contain Web Search activity items alongside message items. Each output_text block can also carry annotations that point to cited URLs.

  • In agent.py, find the block beginning with answer_parts = [].
  • Replace the existing block through server_side_tools = usage.get("num_server_side_tools_used", 0) with this response parser:
answer_parts = []
citation_urls = []
web_search_calls = 0

for item in data.get("output", []):
    if item.get("type") == "web_search_call":
        web_search_calls += 1

    if item.get("type") == "message":
        for content in item.get("content", []):
            if content.get("type") == "output_text":
                answer_parts.append(content.get("text", ""))

                for annotation in content.get("annotations", []):
                    if annotation.get("type") == "url_citation":
                        url = annotation.get("url")
                        if url and url not in citation_urls:
                            citation_urls.append(url)

How does the parser find evidence?

  • The web_search_calls counter records every output item whose type is web_search_call.
  • The message branch finds the final answer content.
  • The url_citation branch reads each returned citation URL.
  • The final condition prevents the same URL from appearing more than once.
Display and verify the evidence

The parser now holds the answer text, source URLs, and activity count. Clear output labels make each piece of evidence visible in the terminal.

  • In agent.py, find the printing block that begins with print("\nAnswer:\n").
  • Replace that printing block with this evidence report:
print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")

print("\nSources:")
if citation_urls:
    for number, url in enumerate(citation_urls, start=1):
        print(f"{number}. {url}")
else:
    print("No citation URLs were returned.")

usage = data.get("usage", {})
server_side_tools = usage.get("num_server_side_tools_used", 0)
print(f"\nWeb Search activity items: {web_search_calls}")
print(f"Server-side tools used: {server_side_tools}")

What does the report show?

  • The answer section joins every text fragment into one readable response.
  • The sources section numbers each unique citation URL.
  • The activity line reports how many web_search_call items appeared in the response.
  • The final line reports the total number of server-side tools used.

✔️ Awesome, I've got everything!

Your agent.py file now enables Web Search and exposes its research evidence.

ⓧ I'd like to double check the full code

import json
import os
import urllib.request

API_URL = "https://api.x.ai/v1/responses"
MODEL = "grok-4.6"

api_key = os.getenv("XAI_API_KEY")
if not api_key:
    raise SystemExit("XAI_API_KEY is not set in this Terminal session.")

question = "What are the latest developments in AI agents?"
payload = {
    "model": MODEL,
    "input": [
        {
            "role": "user",
            "content": question,
        }
    ],
    "tools": [
        {
            "type": "web_search",
        }
    ],
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    data = json.loads(response.read().decode("utf-8"))

answer_parts = []
citation_urls = []
web_search_calls = 0

for item in data.get("output", []):
    if item.get("type") == "web_search_call":
        web_search_calls += 1

    if item.get("type") == "message":
        for content in item.get("content", []):
            if content.get("type") == "output_text":
                answer_parts.append(content.get("text", ""))

                for annotation in content.get("annotations", []):
                    if annotation.get("type") == "url_citation":
                        url = annotation.get("url")
                        if url and url not in citation_urls:
                            citation_urls.append(url)

print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")

print("\nSources:")
if citation_urls:
    for number, url in enumerate(citation_urls, start=1):
        print(f"{number}. {url}")
else:
    print("No citation URLs were returned.")

usage = data.get("usage", {})
server_side_tools = usage.get("num_server_side_tools_used", 0)
print(f"\nWeb Search activity items: {web_search_calls}")
print(f"Server-side tools used: {server_side_tools}")

This reference matches the complete version of agent.py for this step.

  • Save agent.py.

Before you run it, which lines do you expect to prove that Grok used live research?

  • Send the fixed current-information question again by running this command:
python3 agent.py

What should I see?

You should see the researched answer followed by Sources, Web Search activity items, and Server-side tools used.

At least one Web Search activity item or a positive server-side tool count should show that live research occurred. Any returned citation URLs appear as a numbered list.

xAI does not guarantee citations on every answer. A positive tool signal still proves that Grok used a server-side research tool.

Missing activity or sources?

  • Check that the entire tools array remains inside payload if both tool counts are zero.
  • Check that the annotation loop remains inside the output_text branch if citation URLs are missing.
  • Treat an empty source list with a positive tool count as a valid researched response without returned citation annotations.

Help me debug missing Web Search activity or citation URLs.

You now have visible evidence that separates an agentic research result from a plain model response. The answer is backed by observable tool activity and any citation URLs Grok returns.

Grok can now research the web with visible evidence. Next, you'll make the agent accept any question while reporting the exact cost of each request.

Make the Agent Interactive and Cost-Aware

Your last run proved that Grok can use Web Search for live information. The terminal also exposed its server-side activity.

A fixed question makes that demo hard to reuse. The xAI Responses API charges each request. Without a visible price, repeated experiments become risky.

In this step, get ready to:
  • Accept a new research question on every run.
  • Calculate the exact billed cost for each request.
  • Verify the final answer, sources, tool activity, and cost report.
Ask for a question at runtime

An interactive prompt lets one script research a new topic on every run. Stripping whitespace gives an empty submission a clean exit path.

  • In agent.py, find the line that assigns the fixed question.
  • Replace that line with this input check:
question = input("Research question: ").strip()
if not question:
    raise SystemExit("Please enter a research question.")

What does this code do?

  • The input() call pauses the script so you can enter a different question on each run.
  • The .strip() call removes whitespace from both ends of your question.
  • The if not question check stops the script before it sends an empty request.
  • Save agent.py.

Before you run this, what should happen if you submit an empty question?

  • Start the updated script by running this command:
python3 agent.py

What should you see?

The terminal pauses at Research question:. The script is now waiting for your input.

  • Press Enter without typing a question.

The script stops with Please enter a research question. before making an API request.

Good catch. That clean exit proves the agent handles blank input before it can spend credits.

Prompt does not appear?

  • Confirm that you replaced only the fixed question line in agent.py.
  • Check the new input() line for matching parentheses and quotation marks.

Help me fix the interactive question prompt in my Python agent.

Report the exact request cost

Every response can include its total cost in usage.cost_in_usd_ticks. Dividing that value by 10_000_000_000 converts the billing ticks into US dollars.

  • In agent.py, locate the final line that prints print(f"Server-side tools used: {server_side_tools}"):
  • Add the cost report directly below it by copying this code:
cost_ticks = usage.get("cost_in_usd_ticks")
if cost_ticks is not None:
    cost_usd = cost_ticks / 10_000_000_000
    print(f"Cost: ${cost_usd:.6f}")
else:
    print("Cost was not reported in this response.")

How does cost tracking work?

  • The cost_ticks variable reads the total billed amount from the response usage data.
  • The cost_usd calculation converts that amount into dollars.
  • The :.6f format displays six digits after the decimal point so small requests remain visible.
  • The fallback message keeps the report readable when a response does not contain cost data.
  • Save agent.py.
Run the cost-aware agent

This final check consumes prepaid xAI credits. The printed cost keeps that spending visible after every request.

Before you run it, which report sections do you expect the terminal to print after Grok finishes?

  • Start your completed agent by running this command:
python3 agent.py

What happens next?

The script waits at Research question:. Your next input becomes the question sent to Grok.

  • Type What are the latest developments in AI agents? at the prompt.

You will see an answer followed by a deduplicated Sources section.

The final lines show Web Search activity items.

Server-side tools used follows on the next line.

A numeric Cost value closes the report.

How do you judge the result?

  • A web_search_call activity item confirms that Web Search ran.
  • A positive Server-side tools used value also confirms server-side tool activity.
  • Citation URLs appear under Sources when the response includes URL annotations.
  • The Cost line reports the exact billed amount for this request.

Missing part of the report?

  • Confirm that you are still using the Terminal session from earlier if the script reports a missing XAI_API_KEY value.
  • Return to the xAI Console from earlier to confirm that your prepaid credit balance can fund another request.
  • Treat an empty Sources list as valid when the response still shows Web Search activity because citations are not guaranteed for every answer.

Help me debug the missing output in my cost-aware Grok agent.

Strong finish. Your reusable agent now exposes the price behind each researched answer.

✔️ Awesome, I've got everything!

Your agent.py file is saved with interactive input and per-request cost reporting.

ⓧ I'd like to double check the full code

import json
import os
import urllib.request

API_URL = "https://api.x.ai/v1/responses"
MODEL = "grok-4.6"

api_key = os.getenv("XAI_API_KEY")
if not api_key:
    raise SystemExit("XAI_API_KEY is not set in this Terminal session.")

question = input("Research question: ").strip()
if not question:
    raise SystemExit("Please enter a research question.")

payload = {
    "model": MODEL,
    "input": [
        {
            "role": "user",
            "content": question,
        }
    ],
    "tools": [
        {
            "type": "web_search",
        }
    ],
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    data = json.loads(response.read().decode("utf-8"))

answer_parts = []
citation_urls = []
web_search_calls = 0

for item in data.get("output", []):
    if item.get("type") == "web_search_call":
        web_search_calls += 1

    if item.get("type") == "message":
        for content in item.get("content", []):
            if content.get("type") == "output_text":
                answer_parts.append(content.get("text", ""))

                for annotation in content.get("annotations", []):
                    if annotation.get("type") == "url_citation":
                        url = annotation.get("url")
                        if url and url not in citation_urls:
                            citation_urls.append(url)

print("\nAnswer:\n")
print("\n".join(answer_parts) or "No answer text was returned.")

print("\nSources:")
if citation_urls:
    for number, url in enumerate(citation_urls, start=1):
        print(f"{number}. {url}")
else:
    print("No citation URLs were returned.")

usage = data.get("usage", {})
server_side_tools = usage.get("num_server_side_tools_used", 0)
print(f"\nWeb Search activity items: {web_search_calls}")
print(f"Server-side tools used: {server_side_tools}")

cost_ticks = usage.get("cost_in_usd_ticks")
if cost_ticks is not None:
    cost_usd = cost_ticks / 10_000_000_000
    print(f"Cost: ${cost_usd:.6f}")
else:
    print("Cost was not reported in this response.")

What should match?

Compare each section with your saved agent.py file. The interactive question block and cost-reporting block should appear in the same positions shown here.

Secret mission

Restrict Research to Official xAI Sources

Constrain Grok's live research to official xAI domains. Then inspect the returned citations to prove that the source boundary works while tool usage and cost remain visible.

Clean Up Your Resources

Every new request to the xAI Responses API consumes prepaid credits.

Web Search costs $5 per 1,000 tool invocations. Model-token charges are billed separately.

Agentic cost varies because Grok decides how many server-side tools to use.

  • Leave auto top-up disabled unless you intend to purchase credits automatically.

Nice Work!

Nice Work!

You pulled it off. Your agent.py terminal agent now researches current questions through the xAI Responses API.

You've learned how to:

  • Build a Python REST client with only standard-library modules. Send authenticated requests to the xAI Responses API.
  • Turn a plain model request into a Web Search agent. Expose tool activity for every researched answer. Extract deduplicated citation URLs from response annotations.
  • Accept current-information questions through an interactive prompt. Convert billing ticks into the exact billed cost for each request.
  • Secret Mission: Constrain research to official xAI sources with allowed_domains. Verify that every displayed citation stays within docs.x.ai or x.ai.

Ready to quiz yourself?