Build a Fiduciary Car Buying Agent
Build a Python CLI agent that negotiates car deals exclusively for you.
Introduction
30 Second Summary
Buying a car is one of the biggest purchases most people make, and it's one of the few where the seller expects you to negotiate. The problem is that dealers have software, training, and years of experience on their side. You walk in with a vague sense of "what feels fair."
In this project, you will build a Python command-line agent that uses Claude to calculate your true out-the-door cost, compare the dealer's price to fair market value, and generate a complete multi-round negotiation playbook with specific dollar amounts.
What You'll Build
You will type in a car deal scenario and watch your agent crunch the numbers, then hand you a step-by-step playbook with exact offers, counter-offers, and scripts you can use at the dealership.
By the end of this project, you'll have:
- A working CLI agent that calculates your true out-the-door price (tax, title, registration, and doc fees) for any state, so you never negotiate on sticker price alone.
- A market value comparison tool that tells you whether the dealer's asking price is overpriced, fair, or a good deal.
- A complete multi-round negotiation playbook with specific dollar amounts for your opening offer, counter-offers, walk-away price, and word-for-word scripts to use with the dealer.
- Secret Mission: Add F&I office defense mode that generates scripts to decline overpriced add-ons like paint protection and extended warranties.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, functions, and dictionaries. You will also need a free Anthropic API account, which you will create in Step 1.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. Knowing your exact numbers before walking into a dealership flips the power dynamic entirely in your favor.
Set Up Your Environment
Your agent needs two things before it can reason about car deals: the Anthropic Python SDK (the library that lets your code talk to Claude) and an API key (your personal credential for accessing Claude).
In this step, you'll create a dedicated project folder, install the SDK, and connect to your Anthropic account so Claude can respond to your requests.
In this step, get ready to:
- Create a project folder with a virtual environment and install the Anthropic SDK.
- Create an Anthropic account and set up your API key.
- Verify Claude responds to a test message.
Create your project folder and install the SDK
Everything your agent needs will live in one folder on your Desktop. A virtual environment keeps this project's packages separate from the rest of your system.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir car-buying-agent
cd car-buying-agent
Now you'll create a virtual environment to keep your project's packages isolated.
macOS
- Create and activate your virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
Windows
- Create and activate your virtual environment by running these commands:
python -m venv venv
venv\Scripts\activate
Your terminal prompt now starts with (venv). This confirms the virtual environment is active.
What is a virtual environment?
A virtual environment is an isolated space for Python packages. Packages you install here stay in this project and won't interfere with anything else on your machine.
- Install the Anthropic SDK by running:
pip install anthropic
You'll see several lines of download progress, ending with a "Successfully installed" message.
Seeing a "command not found" error?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, re-run the activate command from the platform tab above.
Help me debug this
Before you run the next command, what do you think it will print?
- Confirm the SDK installed correctly by running:
python3 -c "import anthropic; print(anthropic.__version__)"
You should see a version number like 0.116.0 printed in your terminal. This proves the SDK is installed and importable.
Create an Anthropic account and export your API key
The SDK is installed, but it needs a key to authenticate with Claude's servers. You'll create a free Anthropic account and generate an API key now.
- Open your browser and go to console.anthropic.com.
- Create a new account (or sign in if you already have one).
- Click API Keys in the left sidebar.
- Click Create Key.
- Give your key a name like car-buying-agent and click Create Key.
- Copy the key immediately. You will not be able to see it again after closing this dialog.
What about cost?
New Anthropic accounts receive a small amount of free credits to test the API. This project uses Claude Sonnet 4.6 at $3 per million input tokens and $15 per million output tokens. A full run of this agent costs well under a cent.
Now you'll make the key available to your Python code by exporting it as an environment variable.
macOS
- Export your API key by running this command (replace the placeholder with your actual key):
export ANTHROPIC_API_KEY="your-api-key-here"
Windows
- Export your API key by running this command (replace the placeholder with your actual key):
$env:ANTHROPIC_API_KEY = "your-api-key-here"
Why an environment variable?
The Anthropic SDK automatically reads the ANTHROPIC_API_KEY environment variable when you create a client. This keeps your secret key out of your code files so you never accidentally commit it to version control.
Key not working later?
Environment variables set with export or $env: only last for the current terminal session. If you close your terminal and reopen it, you'll need to run the export command again.
Help me debug my API key
Verify Claude responds to a test message
The SDK is installed and your key is set. Time to prove the full chain works by sending Claude a message and seeing it respond.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop folder, select the car-buying-agent folder, and click Open.
VS Code opens with your project folder in the sidebar.
- Click File in the top menu bar.
- Click New File.
- Save it immediately as test_setup.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Add the following code to test_setup.py:
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Say hello!"}]
)
print(message.content[0].text)
What does this code do?
- client = anthropic.Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable.
- client.messages.create(...) sends a message to Claude and waits for a response.
- message.content[0].text extracts Claude's text reply from the response object.
- Save test_setup.py.
Now open a terminal inside VS Code to run your script.
- Open the integrated terminal by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
Activate your venv in the new terminal
VS Code may open a fresh terminal session that does not have your virtual environment active. If you don't see (venv) in the prompt, activate it again using the same command from earlier (source venv/bin/activate on macOS or venv\Scripts\activate on Windows).
You'll also need to re-export your API key in this new terminal session.
Before you run this, what do you expect Claude to respond with?
- Run your test script:
python3 test_setup.py
You should see a friendly greeting from Claude printed in your terminal. The exact wording varies, but any text response confirms the full chain works.
Seeing an authentication error?
Make sure you exported your API key in this terminal session. Run your export command again, then retry python3 test_setup.py. Also verify there are no extra spaces or quotes around the key value.
Help me fix my test script
✔️ Awesome, I've got everything!
Great. Double check you've saved test_setup.py and that Claude's response printed in your terminal.
ⓧ I'd like to double check the full code
Here's the complete test_setup.py file:
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Say hello!"}]
)
print(message.content[0].text)
Your environment is wired up and Claude is listening. Next up, you'll ask Claude for car buying help without any tools and see exactly why generic advice falls short.
Ask Claude Without Tools
Your environment is ready and Claude can respond to messages. The next question is whether Claude can actually help you negotiate a specific car deal with real numbers.
Let's put Claude to the test. You'll describe a real deal scenario and see whether it hands you actionable dollar amounts or just tells you to "do your research."
In this step, get ready to:
- Create agent.py with a basic Claude API call.
- Run the script and observe what Claude can (and can't) tell you about a specific deal.
Create the agent script
You'll ask Claude about a specific deal: a 2023 Toyota Camry with 25,000 miles, asking $27,500, in Texas. This gives Claude every detail it would need to help you.
- Create a new file called agent.py in your car-buying-agent folder and paste in the following code:
import anthropic
# Create the API client (reads ANTHROPIC_API_KEY from environment)
client = anthropic.Anthropic()
# Ask Claude about a specific car deal with all the details
messages = [
{
"role": "user",
"content": "I'm looking at a 2023 Toyota Camry with 25,000 miles. The dealer is asking $27,500 and I'm in Texas. What should I do?"
}
]
# Send the message with no tools and no system prompt
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages
)
# Print whatever Claude says
print(response.content[0].text)
What does this code do?
This is a bare-bones Claude call. No tools, no system prompt, no special instructions.
- messages contains a single user message describing a real car deal scenario with year, make, model, mileage, price, and state.
- client.messages.create() sends the message to Claude Sonnet 4.6 with no additional capabilities.
- Claude has to answer purely from its training data. It has no access to current market prices, state tax rates, or fee calculations.
- Save agent.py.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before running it.
ⓧ I'd like to double check the full code
Here's the complete agent.py file:
import anthropic
client = anthropic.Anthropic()
messages = [
{
"role": "user",
"content": "I'm looking at a 2023 Toyota Camry with 25,000 miles. The dealer is asking $27,500 and I'm in Texas. What should I do?"
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages
)
print(response.content[0].text)
Run the script and observe the gap
Before you run this, think about what you'd want Claude to tell you. If you were about to walk into a dealership, what specific numbers would make you feel prepared?
- Run the script by entering this command:
python3 agent.py
Read through Claude's response carefully. You'll notice something: Claude gives you general advice like "research comparable vehicles" or "consider negotiating 10-15% below asking." It talks in ranges and suggestions.
This is the gap. You gave Claude every detail about your deal, and it still can't give you specific numbers.
What's missing from Claude's response?
Look for these specific things that Claude could NOT provide:
- No exact out-the-door price. Claude cannot calculate Texas sales tax (6.25%), title ($33), registration ($75), and doc fees for your specific deal.
- No specific opening offer amount. Claude cannot look up what a 2023 Camry with 25,000 miles is actually worth right now.
- No round-by-round strategy with dollar amounts. Without real market data, Claude cannot build you a concrete playbook.
Claude is smart, but it's stuck giving generic advice because it has no access to the data that would let it give you real numbers. That's exactly what you'll fix next.
Seeing an error instead of a response?
- If you see an authentication error, your API key may not be exported in this terminal session. Run export ANTHROPIC_API_KEY="your-key-here" again with your actual key.
- If you see a module not found error, make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
Still stuck? Help me debug my agent.py script
You've just experienced the core limitation: Claude is helpful but generic without data. In the next step, you'll give Claude its first tool, an OTD calculator, and watch the response transform from vague advice into exact dollar amounts.
Build the OTD Calculator Tool
In the last step, you saw the problem firsthand. Claude gave you general advice like "research comparable vehicles" instead of calculating the exact numbers for your deal.
The fix is giving Claude access to data it cannot produce from training alone. In this step, you'll build an OTD calculator tool that knows your state's exact tax rate, title fees, registration fees, and doc fee caps. Once Claude can call this tool, it stops guessing and starts calculating.
In this step, get ready to:
- Build a state fee database and OTD calculator function.
- Define a tool schema so Claude knows the calculator exists.
- Wire up the agentic loop that processes tool calls and feeds results back to Claude.
Add the state fee database
A car's sticker price is never what you actually pay. Sales tax, title fees, registration, and dealer doc fees pile on top. These vary wildly by state.
- In agent.py, select all the existing code and delete it. Replace it with this new foundation:
import json
import anthropic
client = anthropic.Anthropic()
# State-specific tax rates, title fees, registration fees, and doc fee caps
STATE_DATA = {
"CA": {"sales_tax": 0.0725, "title_fee": 23, "registration_fee": 60, "doc_fee_cap": 85},
"TX": {"sales_tax": 0.0625, "title_fee": 33, "registration_fee": 75, "doc_fee_cap": 150},
"FL": {"sales_tax": 0.06, "title_fee": 75.25, "registration_fee": 225, "doc_fee_cap": 995},
"NY": {"sales_tax": 0.08, "title_fee": 50, "registration_fee": 26, "doc_fee_cap": 175},
"IL": {"sales_tax": 0.0625, "title_fee": 150, "registration_fee": 151, "doc_fee_cap": 324.24},
"PA": {"sales_tax": 0.06, "title_fee": 58, "registration_fee": 70, "doc_fee_cap": None},
"OH": {"sales_tax": 0.0575, "title_fee": 15, "registration_fee": 31, "doc_fee_cap": None},
"GA": {"sales_tax": 0.04, "title_fee": 18, "registration_fee": 20, "doc_fee_cap": None},
"NC": {"sales_tax": 0.03, "title_fee": 56, "registration_fee": 36, "doc_fee_cap": 899},
"MI": {"sales_tax": 0.06, "title_fee": 15, "registration_fee": 125, "doc_fee_cap": None},
}
What does this code do?
You've added a new import and a data dictionary that acts as your tool's knowledge base.
- json is imported because tool results need to be serialized as JSON strings when sent back to Claude.
- STATE_DATA stores tax rates, title fees, registration fees, and doc fee caps for 10 states. Some states (like CA and TX) cap how much dealers can charge in doc fees. Others (like PA and OH) have no cap at all.
Add the OTD calculator function
The state data is useless without a function that applies it to a specific deal. The calculator takes a sale price and state code, then returns an itemized breakdown of the true out-the-door cost.
- Add this function below the STATE_DATA dictionary:
def calculate_otd_price(sale_price, state, doc_fee=None):
# Validate the state exists in our database
state = state.upper()
if state not in STATE_DATA:
return {"error": f"State '{state}' not in database. Available: {', '.join(sorted(STATE_DATA.keys()))}"}
data = STATE_DATA[state]
# Default doc fee is $499; cap it if the state has a limit
if doc_fee is None:
doc_fee = 499
if data["doc_fee_cap"] is not None:
doc_fee = min(doc_fee, data["doc_fee_cap"])
# Calculate all the fees that pile on top of the sale price
sales_tax = sale_price * data["sales_tax"]
title_fee = data["title_fee"]
registration_fee = data["registration_fee"]
otd_price = sale_price + sales_tax + title_fee + registration_fee + doc_fee
return {
"sale_price": sale_price,
"state": state,
"sales_tax_rate": f"{data['sales_tax'] * 100}%",
"sales_tax_amount": round(sales_tax, 2),
"title_fee": title_fee,
"registration_fee": registration_fee,
"doc_fee": doc_fee,
"doc_fee_capped": data["doc_fee_cap"] is not None,
"out_the_door_price": round(otd_price, 2),
}
What does this function do?
- It takes a sale price, state code, and optional doc fee as inputs.
- The doc fee capping logic protects buyers. In Texas, even if a dealer charges $499, the law caps it at $150. The function enforces this automatically.
- It returns a dictionary with every line item broken out separately, plus the final OTD total. This gives Claude (and the buyer) full transparency.
- Save agent.py.
- Test the function works by running this command in your terminal:
python3 -c "from agent import calculate_otd_price; print(calculate_otd_price(27500, 'TX'))"
You should see a dictionary with an itemized breakdown. Notice the doc fee is $150 (capped by Texas law), not the default $499.
Seeing an import error?
- Make sure you saved agent.py before running the test command.
- Confirm your terminal is in the car-buying-agent folder where agent.py lives.
- Check that your virtual environment is still activated (you should see (venv) in your terminal prompt).
Why isn't my calculate_otd_price function importing correctly?
Define the tool schema and wire up the agentic loop
Your function works, but Claude does not know it exists yet. Claude discovers tools through a JSON Schema definition that describes the tool's name, purpose, and parameters.
- Add the tool schema and a dispatcher function below calculate_otd_price:
# Tool definition tells Claude what tools are available and how to call them
tools = [
{
"name": "calculate_otd_price",
"description": "Calculate the complete out-the-door (OTD) price for a vehicle purchase, including sales tax, title, registration, and doc fees for a specific state.",
"input_schema": {
"type": "object",
"properties": {
"sale_price": {"type": "number", "description": "The negotiated sale price of the vehicle in dollars"},
"state": {"type": "string", "description": "Two-letter state code (e.g., CA, TX, FL)"},
"doc_fee": {"type": "number", "description": "Dealer documentation fee in dollars. If not provided, assumes $499."},
},
"required": ["sale_price", "state"],
},
},
]
# Dispatcher routes tool calls to the correct function
def run_tool(name, tool_input):
if name == "calculate_otd_price":
return calculate_otd_price(**tool_input)
else:
return {"error": f"Unknown tool: {name}"}
What does this code do?
- The tools array is passed to Claude's API. Claude reads the schema and decides when to call the tool based on the user's question.
- input_schema uses JSON Schema format to describe each parameter's type and purpose. required tells Claude which parameters are mandatory.
- run_tool is a dispatcher. When Claude requests a tool call, your code routes it to the correct Python function. You will add more tools here in the next step.
Now you need the agentic loop that actually processes Claude's tool calls. Claude responds with a tool request instead of text, your code executes the tool, then sends the result back. This cycle repeats until Claude has enough data to answer.
- Add the run_agent function below run_tool:
def run_agent(user_message):
"""Run the agent with tool use."""
messages = [{"role": "user", "content": user_message}]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
# Keep looping while Claude requests tool calls
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" [Tool Call] {block.name}({json.dumps(block.input)})")
result = run_tool(block.name, block.input)
print(f" [Result] {json.dumps(result)}")
print()
tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
# Print Claude's final text response
for block in response.content:
if block.type == "text":
print(block.text)
How does the agentic loop work?
This is the pattern that turns a single API call into an intelligent agent.
- When stop_reason == "tool_use", Claude is saying "I need data before I can answer." Your code executes the tool and sends the result back.
- Each tool_result must include the matching tool_use_id so Claude knows which request it fulfills.
- The while loop means Claude can call tools multiple times before giving a final answer. When it has enough data, it responds with stop_reason == "end_turn" and text content.
- Finally, add the entry point at the very bottom of agent.py:
if __name__ == "__main__":
run_agent("I'm looking at a 2023 Toyota Camry with 25,000 miles. The dealer is asking $27,500 and I'm in Texas.")
- Save agent.py.
Run the agent and see the difference
Your agent now has a tool. Before you run it, think back to what Claude said in Step 2 when you asked the exact same question about a 2023 Toyota Camry in Texas.
- Before you run this, do you think Claude will give you a specific OTD dollar amount this time? What do you expect the Texas sales tax to add to a $27,500 car?
- Run the agent by executing:
python3 agent.py
You should see Claude call the tool first (printed as [Tool Call] and [Result]), then give you an itemized OTD breakdown with exact dollar amounts for tax, title, registration, doc fee, and the total out-the-door price.
What changed from Step 2?
Same question, completely different answer. In Step 2, Claude said things like "research the market" and "check your state's fees." Now Claude actually checks the fees itself and gives you exact numbers.
This is the power of tool use. Claude's reasoning ability stays the same. You just gave it access to data it could not produce on its own.
Not seeing tool calls in the output?
- Make sure your tools=tools parameter is included in both client.messages.create() calls (the initial one and the one inside the while loop).
- Check that your ANTHROPIC_API_KEY environment variable is still set. Run echo $ANTHROPIC_API_KEY to verify.
- If you see an authentication error, re-export your key with export ANTHROPIC_API_KEY="your-key-here".
Why isn't Claude calling my calculate_otd_price tool?
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py and confirmed Claude is now calling your tool and returning specific dollar amounts.
ⓧ I'd like to double check the full code
Here is the complete agent.py at this point. Your file has these sections in order. Scroll up and confirm each one is present:
- import json and import anthropic at the top.
- client = anthropic.Anthropic() creating the API client.
- STATE_DATA dictionary with 10 states (CA, TX, FL, NY, IL, PA, OH, GA, NC, MI).
- calculate_otd_price() function with doc fee capping logic.
- tools array with the JSON schema for calculate_otd_price.
- run_tool() dispatcher function.
- run_agent() with the while loop checking stop_reason == "tool_use".
- if __name__ == "__main__": block calling run_agent with the Toyota Camry scenario.
Your agent can now crunch OTD numbers with real state data. But knowing the TRUE cost is only half the battle. In the next step, you will add market value comparison so Claude can tell you whether the dealer's price is actually fair.
Add Market Value Comparison
Your agent can calculate the true out-the-door cost, but it cannot yet tell you whether the dealer's price is fair. Knowing the total cost is only half the picture. The other half is knowing what the car is actually worth.
In this step, you will add a market value lookup tool that gives Claude access to fair market pricing data. Once Claude can compare what the dealer is asking against what the car is actually worth, it can identify overpriced deals and give you a concrete dollar amount to use as negotiation leverage.
In this step, get ready to:
- Build a market value lookup function with mileage adjustment logic.
- Register the new tool so Claude can call it alongside the OTD calculator.
- Run the agent and see Claude combine both data sources to identify overpriced deals.
Add the market value data and lookup function
Claude's training data includes general car knowledge, but it cannot look up a specific vehicle's current fair market value. You will give it a data source with low, mid, and high price ranges for popular vehicles across multiple model years.
- In agent.py, find the closing brace of the STATE_DATA dictionary.
- Add the following MARKET_DATA dictionary directly below it:
MARKET_DATA = {
"Toyota Camry": {
"2022": {"low": 22000, "mid": 24500, "high": 27000},
"2023": {"low": 24000, "mid": 26500, "high": 29000},
"2024": {"low": 27000, "mid": 29500, "high": 32000},
},
"Honda Civic": {
"2022": {"low": 20000, "mid": 22000, "high": 24500},
"2023": {"low": 22000, "mid": 24500, "high": 27000},
"2024": {"low": 25000, "mid": 27000, "high": 29500},
},
"Toyota RAV4": {
"2022": {"low": 26000, "mid": 29000, "high": 32000},
"2023": {"low": 28000, "mid": 31000, "high": 34000},
"2024": {"low": 31000, "mid": 34000, "high": 37000},
},
"Honda CR-V": {
"2022": {"low": 25000, "mid": 28000, "high": 31000},
"2023": {"low": 27000, "mid": 30000, "high": 33000},
"2024": {"low": 30000, "mid": 33000, "high": 36000},
},
"Ford F-150": {
"2022": {"low": 32000, "mid": 38000, "high": 44000},
"2023": {"low": 35000, "mid": 41000, "high": 47000},
"2024": {"low": 38000, "mid": 44000, "high": 50000},
},
}
What does this data represent?
Each vehicle has three price points: low (below average condition or high miles), mid (fair market value), and high (excellent condition or low miles). The mid value is your anchor for negotiation.
In production, you would replace this with a real API like KBB or Edmunds. For this project, simulated data teaches the same tool-use pattern.
Now you need the function that Claude will call to query this data. It takes a year, make, model, and optional mileage, then returns the fair market range with mileage adjustments.
- Add the get_market_value function directly below the calculate_otd_price function:
def get_market_value(year, make, model, mileage=None):
# Build lookup key from make and model
vehicle_key = f"{make} {model}"
year_str = str(year)
if vehicle_key not in MARKET_DATA:
return {"error": f"Vehicle '{vehicle_key}' not in database.", "available_vehicles": list(MARKET_DATA.keys())}
if year_str not in MARKET_DATA[vehicle_key]:
return {"error": f"Year {year} not available for {vehicle_key}.", "available_years": list(MARKET_DATA[vehicle_key].keys())}
values = MARKET_DATA[vehicle_key][year_str].copy()
# Adjust values based on mileage vs. expected mileage
if mileage:
avg_annual_miles = 12000
current_year = 2026
expected_miles = (current_year - int(year_str)) * avg_annual_miles
mile_diff = mileage - expected_miles
adjustment = -(mile_diff * 0.10)
values = {"low": round(values["low"] + adjustment), "mid": round(values["mid"] + adjustment), "high": round(values["high"] + adjustment)}
return {"year": year, "make": make, "model": model, "mileage": mileage, "fair_market_value": values, "source": "Simulated market data (replace with real API in production)"}
What does the mileage adjustment do?
The function assumes 12,000 miles per year as average. If the car has fewer miles than expected, its value goes up. If it has more miles than expected, its value goes down. The adjustment is $0.10 per mile of difference.
For a 2023 vehicle in 2026, expected mileage is 36,000. A car with only 25,000 miles is 11,000 under expected, so its value increases by $1,100.
- Save agent.py.
- Test the function directly by running this command:
python3 -c "from agent import get_market_value; import json; print(json.dumps(get_market_value(2023, 'Toyota', 'Camry', 25000), indent=2))"
You should see a JSON object with fair_market_value containing adjusted low, mid, and high values. The mid value should be higher than the base 26,500 because 25,000 miles is below the expected 36,000.
Seeing an ImportError or NameError?
- Make sure MARKET_DATA is defined before the get_market_value function. Python reads top to bottom.
- Check that you are running the command from inside the car-buying-agent folder where agent.py lives.
- Confirm your virtual environment is still active (you should see (venv) in your terminal prompt).
Help me debug my get_market_value function import error.
Register the tool and update the dispatcher
The function works, but Claude does not know it exists yet. You need to add a tool schema that tells Claude what the function does and what parameters it accepts.
- In agent.py, find the tools list. It currently has one item (the calculate_otd_price schema).
- Add the following tool definition as a second item in the tools list, after the closing brace of the first tool:
{
"name": "get_market_value",
"description": "Look up the fair market value range (low, mid, high) for a specific vehicle based on year, make, model, and optionally mileage.",
"input_schema": {
"type": "object",
"properties": {
"year": {"type": "integer", "description": "Model year of the vehicle"},
"make": {"type": "string", "description": "Vehicle manufacturer (e.g., Toyota, Honda, Ford)"},
"model": {"type": "string", "description": "Vehicle model (e.g., Camry, Civic, F-150)"},
"mileage": {"type": "integer", "description": "Current odometer reading in miles"},
},
"required": ["year", "make", "model"],
},
},
What does the tool schema do?
The schema is how Claude discovers your function. The description tells Claude when to use the tool. The input_schema tells it exactly what parameters to pass and in what format.
Notice that mileage is not in the required array. Claude will include it when the user mentions mileage, and skip it otherwise.
Finally, the dispatcher needs to know how to route calls to your new function.
- In the run_tool function, find this line:
else:
return {"error": f"Unknown tool: {name}"}
- Add an elif branch for get_market_value above the else, so the function now looks like this:
def run_tool(name, tool_input):
if name == "calculate_otd_price":
return calculate_otd_price(**tool_input)
elif name == "get_market_value":
return get_market_value(**tool_input)
else:
return {"error": f"Unknown tool: {name}"}
What does the dispatcher do?
When Claude decides to use a tool, it returns the tool name and inputs in its response. Your agentic loop calls run_tool with that name to execute the right Python function. Without this routing, Claude would ask for the tool but nothing would happen.
- Save agent.py.
Not sure where to add the elif?
The elif must come between the existing if name == "calculate_otd_price" block and the else block. Python evaluates conditions in order, so the else must always be last.
Help me fix my run_tool dispatcher function.
Run the agent and observe both tools in action
Claude now has two tools available. Before you run, think about what you expect to happen. Will Claude call one tool or both? In what order?
- Run the agent by executing:
python3 agent.py
You should see Claude call both get_market_value and calculate_otd_price in the [Tool Call] output. Claude now states whether the dealer's price is above, at, or below fair market value with specific dollar comparisons.
What should I see?
Claude combines both data sources to reason about the deal. It uses market value to identify the gap (e.g., the dealer is asking $27,500 but fair market mid is around $26,500), then uses the OTD calculator to show the true total cost at that inflated price.
This gap becomes your negotiation leverage. Without the market value tool, Claude could only tell you the total cost. Now it can tell you the total cost AND whether that cost is justified.
Claude only calls one tool?
- Check that your tools list has two items. Both schemas should be inside the same list brackets.
- Make sure the second tool schema has a comma after the first tool's closing brace.
- Verify that run_tool has the elif branch for get_market_value. If the dispatcher returns an error, Claude will not retry.
Help me figure out why Claude is not calling both tools.
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py and both tool calls appear in your output.
ⓧ I'd like to double check the full code
Scroll up and confirm you have each of these sections in order in your agent.py:
- Imports (json and anthropic) and client initialization.
- STATE_DATA dictionary with 10 states.
- MARKET_DATA dictionary with 5 vehicles across 2022-2024.
- calculate_otd_price function.
- get_market_value function with mileage adjustment logic.
- tools list with TWO tool schemas (calculate_otd_price and get_market_value).
- run_tool with if/elif/else handling both tool names.
- run_agent function and if __name__ == "__main__" block.
Your agent now knows the true cost AND whether that cost is fair. In the next step, you will add a fiduciary system prompt that turns this data into a complete multi-round negotiation playbook with specific dollar amounts for every counter-offer.
Build the Fiduciary Agent
In the previous step, you gave Claude access to tools and saw it calculate real OTD numbers and compare prices to market value. The data is flowing, but there is a problem.
Claude is still giving balanced, both-sides advice. It tells you what the numbers are, but it does not tell you what to DO with them. A fiduciary system prompt transforms Claude from a neutral calculator into an aggressive buyer's advocate with a specific negotiation playbook.
In this step, get ready to:
- Add a fiduciary system prompt that forces Claude to advocate exclusively for the buyer.
- Build a CLI interface that lets you describe any car deal in natural language.
- Run the complete agent and receive a multi-round negotiation playbook with specific dollar amounts.
Add the fiduciary system prompt
A system prompt sets the rules Claude must follow before it sees any user message. Your fiduciary prompt will mandate OTD-only negotiation, require specific dollar amounts at every round, and forbid balanced "on the other hand" advice.
- In agent.py, find the run_tool function. Add the following SYSTEM_PROMPT constant directly below it (before run_agent):
SYSTEM_PROMPT = """You are a fiduciary car buying agent. Your ONLY obligation is to the buyer. You must:
1. ALWAYS negotiate the Out-The-Door (OTD) price, never the sale price alone. The OTD is the total amount the buyer pays to drive the car home.
2. Use the calculate_otd_price tool to show the buyer the TRUE total cost at any proposed sale price.
3. Use the get_market_value tool to determine if the asking price is fair. Compare the dealer's price against the market range.
4. Generate a complete multi-round negotiation playbook with SPECIFIC dollar amounts:
- Opening offer (typically 10-15% below fair market mid value)
- Round 2 counter (split the difference, but stay below market mid)
- Round 3 counter (your best offer, at or just below market mid)
- Walk-away price (the absolute maximum the buyer should pay)
5. For each round, provide:
- The exact dollar amount to offer
- What to say to the dealer (a script)
- What anchors to cite (competing OTD, market data, time on lot)
6. Identify dealer tactics to watch for:
- Payment-focused negotiation ("What monthly payment works for you?")
- Add-on bundling in the F&I office
- "Let me talk to my manager" pressure tactics
- Today-only urgency
7. Set a hard walk-away threshold. If the dealer will not meet it, the buyer walks. No exceptions.
Remember: You work for the BUYER, not the dealer. Every dollar saved is your success."""
What does this system prompt do?
Each numbered rule constrains Claude's behavior. Rule 1 prevents the dealer trick of negotiating sale price while hiding fees. Rules 4-5 force Claude to produce a complete playbook with exact dollar amounts instead of vague "negotiate lower" advice.
Rule 7 is the hard stop. Without it, Claude might keep suggesting compromises forever. A fiduciary agent needs a line it will not cross.
What is a fiduciary obligation?
A fiduciary is someone legally bound to act in YOUR best interest, not their own. Financial advisors and lawyers are fiduciaries. By telling Claude it has a fiduciary obligation to the buyer, you eliminate the "balanced perspective" behavior that makes AI assistants hedge their recommendations.
Now you need to wire the system prompt into your agent loop.
- In the run_agent function, find the first client.messages.create() call. It currently looks like this:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
- Add system=SYSTEM_PROMPT, between the max_tokens line and the tools line, so it looks like this:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=tools,
messages=messages,
)
- Find the second client.messages.create() call inside the while loop and make the same change. Add system=SYSTEM_PROMPT, in the same position.
Before you run the script, what do you think will be different about Claude's response now that it has a fiduciary system prompt?
- Save agent.py and run the script to see the fiduciary agent in action:
python3 agent.py
You should see Claude call both tools, then produce a structured negotiation playbook with specific dollar amounts for your opening offer, counter-offers, and a hard walk-away price. Instead of "consider negotiating," it now gives you exact numbers and scripts.
Still seeing generic advice without specific dollar amounts?
Make sure you added system=SYSTEM_PROMPT to BOTH client.messages.create() calls in run_agent. If you only updated one, Claude loses the system prompt context during the tool-use loop.
Also check that SYSTEM_PROMPT is defined BEFORE the run_agent function. Python reads top to bottom.
Help me debug my system prompt setup.
Add the CLI interface for user input
The hardcoded scenario got you this far, but a real agent should accept any deal. You will replace the one-liner in __main__ with an interactive CLI that prompts the user to describe their situation.
- In agent.py, find the if __name__ == "__main__": block at the bottom. It currently looks like this:
if __name__ == "__main__":
run_agent("I'm looking at a 2023 Toyota Camry with 25,000 miles. The dealer is asking $27,500 and I'm in Texas.")
- Replace that entire block with the following interactive CLI:
if __name__ == "__main__":
print("=" * 60)
print("FIDUCIARY CAR BUYING AGENT")
print("I work for YOU, not the dealer.")
print("=" * 60)
print()
print("Tell me about the vehicle you're looking at.")
print("Include: year, make, model, mileage, asking price, and your state.")
print()
print("Example: I'm looking at a 2023 Toyota Camry with 25,000 miles.")
print(" The dealer is asking $27,500 and I'm in Texas.")
print()
user_input = input("Your situation: ")
print()
print("Analyzing your deal and building your negotiation playbook...")
print("-" * 60)
print()
run_agent(user_input)
What does this CLI do?
The banner makes the agent's loyalty clear upfront. The example line shows the user exactly what kind of input works best. input() pauses the script and waits for the user to type their scenario in natural language.
Before you save, there are two small improvements to make. The tool descriptions should tell Claude HOW to use each tool in a fiduciary context.
- In the tools array, find the calculate_otd_price tool's description string. It currently ends with:
"description": "Calculate the complete out-the-door (OTD) price for a vehicle purchase, including sales tax, title, registration, and doc fees for a specific state.",
- Add the directive sentence at the end so it reads:
"description": "Calculate the complete out-the-door (OTD) price for a vehicle purchase, including sales tax, title, registration, and doc fees for a specific state. Use this to show the buyer the TRUE total cost.",
- Find the get_market_value tool's description string. It currently ends with:
"description": "Look up the fair market value range (low, mid, high) for a specific vehicle based on year, make, model, and optionally mileage.",
- Add the directive sentence at the end so it reads:
"description": "Look up the fair market value range (low, mid, high) for a specific vehicle based on year, make, model, and optionally mileage. Use this to determine if the asking price is fair, overpriced, or a good deal.",
- Save agent.py.
Why update the tool descriptions?
Tool descriptions are instructions Claude reads before deciding which tool to call. Adding directive language like "Use this to show the buyer the TRUE total cost" reinforces the fiduciary behavior. Claude sees both the system prompt AND the tool descriptions, so they should align.
Run the complete agent end-to-end
Time to test the full fiduciary agent with an interactive scenario. Before you run it, think about what a complete negotiation playbook should include.
- Run the agent:
python3 agent.py
- You will see the banner and a prompt. Enter a scenario like:
I'm looking at a 2023 Toyota Camry with 25,000 miles. The dealer is asking $27,500 and I'm in Texas.
Do you think the agent will tell you to pay $27,500, or will it give you a lower opening number? What kind of walk-away price do you expect based on the market data from earlier?
After a few seconds, you will see the tool calls print to the terminal followed by a complete negotiation playbook. Your output should include an opening offer amount, round 2-4 counter-offers with escalating dollar amounts, a hard walk-away price, word-for-word scripts for what to say to the dealer, and a list of dealer tactics to watch for.
What should I see?
The tool calls will print first (market value lookup and OTD calculation). Then you will see a structured playbook. The opening offer should be 10-15% below the fair market mid value. For a 2023 Camry at $26,500 mid, expect an opening around $22,500-$23,500.
Each round includes an exact dollar figure, a script, and reasoning. The walk-away price should be at or below the market mid OTD. Every response will be different because Claude generates it fresh each time, but the structure is consistent.
Script exits immediately without waiting for input?
Check that you replaced the entire old __main__ block. If the old one-liner run_agent("I'm looking at...") is still there, the script runs that hardcoded call and exits before reaching input().
Help me fix my CLI interface.
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py with all your changes.
ⓧ I'd like to double check the full code
Your agent.py has grown throughout this project. Here are the key sections to confirm are in place, in order:
- Imports (json and anthropic) and client initialization at the top.
- STATE_DATA dictionary with 10 states.
- MARKET_DATA dictionary with 5 vehicles.
- calculate_otd_price() function.
- get_market_value() function.
- tools array with both tool schemas (updated descriptions ending with directive sentences).
- run_tool() dispatcher function.
- SYSTEM_PROMPT constant with 7 rules.
- run_agent() function with system=SYSTEM_PROMPT in BOTH client.messages.create() calls.
- Interactive __main__ block with banner, example, input(), and run_agent(user_input).
Your fiduciary agent is complete. You now have a tool that turns any car deal into a data-driven negotiation where you know your number, your walk-away, and your script before you ever talk to a salesperson. Next up, the Secret Mission adds F&I office defense mode to protect you from overpriced add-ons.
Secret mission
Add F&I Office Defense Mode
The negotiation doesn't end at the sales desk. The F&I (Finance & Insurance) office is where dealers recover margin through overpriced add-ons like paint protection and extended warranties. Can you arm your agent with a tool that exposes dealer markups and generates specific decline scripts?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with a pay-as-you-go API, so there are no ongoing costs unless you actively run the agent.
Resources you used:
- The car-buying-agent project folder (includes agent.py, test_setup.py, and the venv/ virtual environment).
- Anthropic API key (stored at console.anthropic.com).
- The ANTHROPIC_API_KEY environment variable in your terminal session.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your agent or want to add more tools.
- Your car-buying-agent folder stays on your Desktop.
- Your Anthropic API key remains active. You only pay when you run the agent.
- The virtual environment stays ready for the next time you source venv/bin/activate.
Pause - I'll come back to this later
Deactivate the virtual environment and clear the API key from your current shell. Your files and API key stay intact for when you return.
- Deactivate the virtual environment by running:
deactivate
- Clear the API key from your current terminal session by running:
unset ANTHROPIC_API_KEY
- When you come back, navigate to the car-buying-agent folder, activate the virtual environment, and re-export your key.
Delete - I don't want to use this again
Remove all project files and revoke your API key so nothing remains.
- Deactivate the virtual environment first (if still active) by running:
deactivate
- Delete the entire project folder by running:
macOS
rm -rf ~/Desktop/car-buying-agent
Windows
rmdir /s /q %USERPROFILE%\Desktop\car-buying-agent
- Clear the API key from your terminal session by running:
unset ANTHROPIC_API_KEY
- Revoke your Anthropic API key:
- Go to console.anthropic.com and open the API Keys page.
- Find the key you created for this project and click the delete or revoke option next to it.
- Confirm the deletion when prompted.
Nice Work!
Nice Work!
You just built a fiduciary car buying agent that works exclusively for you. The next time you walk into a dealership, you'll have specific dollar amounts, counter-offer scripts, and a hard walk-away price before the salesperson even says hello.
You've learned how to:
- Build a Python CLI agent with custom tool use that gives Claude access to real data it cannot produce from training alone (state-specific tax rates, market values, fee structures).
- Design a fiduciary system prompt that transforms Claude from a neutral assistant into a buyer's advocate with a complete multi-round negotiation playbook containing specific dollar amounts, scripts, and dealer tactic warnings.
- Implement an agentic loop that processes tool calls and feeds results back to Claude, letting the agent reason across multiple data sources before delivering its final recommendation.
- Secret Mission: Added F&I office defense mode that exposes dealer markups on add-ons and generates specific decline scripts with cheaper alternatives.
Ready to quiz yourself?