What You'll Build
By the end of this Claude API tutorial, you'll have a fully working Python agent that takes raw property data — bedrooms, bathrooms, square footage, location — and automatically generates a polished, SEO-optimized real estate listing in seconds. The agent uses Claude's tool_use feature to orchestrate two tools: one that structures your property data and one that applies SEO optimization to the final copy.
This is the exact kind of real estate automation with AI that we build for brokerages and property managers here in Southwest Florida. If you're a developer or a real estate business owner who wants to eliminate hours of manual listing copy, this tutorial is for you.
The complete, working code for this project is spread across the step-by-step sections below. Each step builds on the last, so by Step 5 you'll have everything you need to run it. If you want to skip ahead, the full
listing_agent.py file is assembled in Step 3.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one here)
- Basic familiarity with Python classes and functions
anthropicSDK installed (pip install anthropic)- A text editor or IDE (VS Code works great)
Step 1: Setting Up Your Claude API Environment
First, let's get your environment ready. Install the Anthropic SDK and set your API key as an environment variable — don't hardcode it in your script.
terminalpip install anthropic # Set your API key (Mac/Linux) export ANTHROPIC_API_KEY="your-api-key-here" # Set your API key (Windows PowerShell) $env:ANTHROPIC_API_KEY="your-api-key-here"
Now create a new file called listing_agent.py and add this basic setup at the top. We're pulling the API key from the environment so it never ends up in your codebase.
import anthropic import json import os # Initialize the Anthropic client using the ANTHROPIC_API_KEY env variable client = anthropic.Anthropic() MODEL = "claude-sonnet-4-6"
That's it for setup. The SDK automatically picks up your ANTHROPIC_API_KEY environment variable, so the client initializes with zero extra config.
Step 2: Creating the Property Data Tool
Claude's tool_use feature lets you define functions that the model can call during a conversation. We're going to define two tools: property_data_tool and seo_optimizer_tool. Think of these as the agent's hands — Claude decides when to use them, and your Python code actually executes them.
The first tool takes raw property details and structures them into a clean, consistent format. This is important because listing inputs from real estate agents are often messy and inconsistent.
listing_agent.pyimport anthropic
import json
import os
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
# Tool definitions that Claude can call during the agent loop
TOOLS = [
{
"name": "property_data_tool",
"description": (
"Structures and validates raw property data into a clean, "
"standardized format for listing generation. Call this first "
"with the raw property input before generating any listing copy."
),
"input_schema": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Full property address including city and state"
},
"price": {
"type": "number",
"description": "Listing price in USD"
},
"bedrooms": {
"type": "integer",
"description": "Number of bedrooms"
},
"bathrooms": {
"type": "number",
"description": "Number of bathrooms, e.g. 2.5"
},
"square_feet": {
"type": "integer",
"description": "Total interior square footage"
},
"property_type": {
"type": "string",
"description": "Type of property, e.g. Single Family, Condo, Townhouse"
},
"features": {
"type": "array",
"items": {"type": "string"},
"description": "List of key property features and amenities"
},
"year_built": {
"type": "integer",
"description": "Year the property was built"
},
"lot_size": {
"type": "string",
"description": "Lot size as a string, e.g. 0.25 acres or 10,890 sq ft"
}
},
"required": ["address", "price", "bedrooms", "bathrooms", "square_feet", "property_type", "features"]
}
},
{
"name": "seo_optimizer_tool",
"description": (
"Takes a draft real estate listing and optimizes it for search engines. "
"Adds location-based keywords, improves headline structure, and ensures "
"the listing will rank well for local property searches. Call this after "
"you have generated the initial listing draft."
),
"input_schema": {
"type": "object",
"properties": {
"draft_listing": {
"type": "string",
"description": "The full draft listing text to be SEO-optimized"
},
"target_keywords": {
"type": "array",
"items": {"type": "string"},
"description": "List of SEO keywords to incorporate naturally"
},
"location": {
"type": "string",
"description": "Primary location/city for local SEO targeting"
}
},
"required": ["draft_listing", "target_keywords", "location"]
}
}
]
Notice the input_schema field on each tool — that's a standard JSON Schema definition that tells Claude exactly what arguments each function expects. Claude will validate its own inputs against this schema before calling the tool.
Step 3: Building the Listing Generator Agent
Now we build the core agent class. This is where the AI agent development logic lives — a loop that keeps calling Claude until it finishes generating the full listing.
The key insight here is the agentic loop: Claude responds, we check if it wants to use a tool, we execute that tool and send the result back, and we repeat until Claude returns a final text response with no more tool calls.
listing_agent.pyimport anthropic
import json
import os
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
TOOLS = [
{
"name": "property_data_tool",
"description": (
"Structures and validates raw property data into a clean, "
"standardized format for listing generation. Call this first "
"with the raw property input before generating any listing copy."
),
"input_schema": {
"type": "object",
"properties": {
"address": {"type": "string", "description": "Full property address including city and state"},
"price": {"type": "number", "description": "Listing price in USD"},
"bedrooms": {"type": "integer", "description": "Number of bedrooms"},
"bathrooms": {"type": "number", "description": "Number of bathrooms, e.g. 2.5"},
"square_feet": {"type": "integer", "description": "Total interior square footage"},
"property_type": {"type": "string", "description": "Type of property"},
"features": {"type": "array", "items": {"type": "string"}, "description": "Key features"},
"year_built": {"type": "integer", "description": "Year property was built"},
"lot_size": {"type": "string", "description": "Lot size as a string"}
},
"required": ["address", "price", "bedrooms", "bathrooms", "square_feet", "property_type", "features"]
}
},
{
"name": "seo_optimizer_tool",
"description": (
"Takes a draft real estate listing and optimizes it for search engines. "
"Adds location-based keywords, improves headline structure, and ensures "
"the listing will rank well for local property searches."
),
"input_schema": {
"type": "object",
"properties": {
"draft_listing": {"type": "string", "description": "The draft listing text to optimize"},
"target_keywords": {"type": "array", "items": {"type": "string"}, "description": "SEO keywords"},
"location": {"type": "string", "description": "Primary city for local SEO"}
},
"required": ["draft_listing", "target_keywords", "location"]
}
}
]
def execute_property_data_tool(tool_input: dict) -> dict:
"""
Processes and structures the raw property data.
In production you could pull from an MLS API here instead.
"""
structured = {
"address": tool_input["address"],
"price_formatted": f"${tool_input['price']:,.0f}",
"price_raw": tool_input["price"],
"bedrooms": tool_input["bedrooms"],
"bathrooms": tool_input["bathrooms"],
"square_feet": f"{tool_input['square_feet']:,}",
"price_per_sqft": f"${tool_input['price'] / tool_input['square_feet']:,.0f}",
"property_type": tool_input["property_type"],
"features": tool_input["features"],
"year_built": tool_input.get("year_built", "Not specified"),
"lot_size": tool_input.get("lot_size", "Not specified"),
"status": "validated"
}
return structured
def execute_seo_optimizer_tool(tool_input: dict) -> dict:
"""
Wraps the draft listing with SEO metadata and formatting guidance.
Claude will use this output to write the final polished version.
"""
keywords = tool_input["target_keywords"]
location = tool_input["location"]
draft = tool_input["draft_listing"]
# Build SEO metadata that Claude will use to refine the final listing
seo_result = {
"optimized_draft": draft,
"suggested_title_tag": f"{location} Real Estate | {keywords[0] if keywords else 'Home for Sale'}",
"meta_description_suggestion": (
f"Find this stunning property in {location}. "
f"{draft[:120].strip()}..."
),
"keywords_to_include": keywords,
"location_signals": [
location,
f"{location} real estate",
f"homes for sale in {location}",
f"{location} property listings"
],
"seo_score": "pending_final_review",
"instructions": (
"Rewrite the listing using the keywords naturally. "
"Start with a compelling headline containing the location. "
"Use short paragraphs. Include a strong call to action at the end."
)
}
return seo_result
def process_tool_call(tool_name: str, tool_input: dict) -> str:
"""Routes tool calls from Claude to the correct Python function."""
if tool_name == "property_data_tool":
result = execute_property_data_tool(tool_input)
elif tool_name == "seo_optimizer_tool":
result = execute_seo_optimizer_tool(tool_input)
else:
result = {"error": f"Unknown tool: {tool_name}"}
# Return result as a JSON string so Claude can parse it
return json.dumps(result, indent=2)
class RealEstateListingAgent:
"""
An autonomous agent that generates SEO-optimized real estate listings
using Claude's tool_use feature to orchestrate data processing and SEO.
"""
def __init__(self):
self.client = anthropic.Anthropic()
self.model = MODEL
self.tools = TOOLS
self.system_prompt = (
"You are an expert real estate copywriter and SEO specialist. "
"Your job is to generate compelling, SEO-optimized property listings. "
"Always start by calling property_data_tool to structure the input data. "
"Then write a full draft listing (headline, description, features, CTA). "
"Then call seo_optimizer_tool to get SEO guidance. "
"Finally, produce the polished, final listing incorporating the SEO feedback. "
"Be specific, descriptive, and highlight what makes each property unique."
)
def run(self, property_input: dict) -> str:
"""
Runs the full agent loop for a given property input dictionary.
Returns the final SEO-optimized listing as a string.
"""
print("\n🏠 Starting listing generation agent...\n")
# Kick off the conversation with the raw property data
messages = [
{
"role": "user",
"content": (
f"Please generate a complete, SEO-optimized real estate listing "
f"for this property:\n\n{json.dumps(property_input, indent=2)}"
)
}
]
# Agentic loop — keeps running until Claude stops calling tools
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=self.tools,
messages=messages
)
print(f"📡 Claude stop reason: {response.stop_reason}")
# If Claude is done, extract and return the final text response
if response.stop_reason == "end_turn":
final_text = ""
for block in response.content:
if hasattr(block, "text"):
final_text += block.text
return final_text
# If Claude wants to use a tool, process each tool call
if response.stop_reason == "tool_use":
# Add Claude's response (with tool calls) to the message history
messages.append({"role": "assistant", "content": response.content})
# Collect all tool results in a single user message
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"🔧 Tool called: {block.name}")
print(f" Input: {json.dumps(block.input, indent=2)[:200]}...")
result = process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Send all tool results back in one user message
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason — break to avoid infinite loop
print(f"⚠️ Unexpected stop reason: {response.stop_reason}")
break
return "Agent loop ended without a final response."
while True loop handles this back-and-forth automatically. Without it, you'd only get Claude's first response, which is just a tool call — not the finished listing.
Step 4: Implementing the SEO Optimization Tool
The SEO tool is already defined in the TOOLS list and executed by execute_seo_optimizer_tool above. But let's talk about what it actually does, because this is where the real estate automation with AI gets interesting.
When Claude calls seo_optimizer_tool, it passes a draft listing it just wrote, a list of target keywords it derived from the property data, and the target location. Our Python function returns structured SEO guidance — keyword lists, meta description suggestions, location signals — and Claude uses all of that to rewrite the final listing.
You can extend execute_seo_optimizer_tool to call a real SEO API (like Semrush or DataForSEO) to pull actual search volume data for keywords. For now it returns structured guidance that Claude knows how to interpret.
Step 5: Running Your First Property Listing
Now let's wire everything together with a sample property input and run the agent. Add this to the bottom of listing_agent.py.
if __name__ == "__main__":
# Example JSON property input — this is what you'd feed in from your database or form
sample_property = {
"address": "4821 Gulf Shore Blvd N, Naples, FL 34103",
"price": 2_450_000,
"bedrooms": 4,
"bathrooms": 3.5,
"square_feet": 3_200,
"property_type": "Single Family",
"year_built": 2019,
"lot_size": "0.31 acres",
"features": [
"Gulf views",
"Resort-style pool and spa",
"Gourmet chef's kitchen with Sub-Zero appliances",
"Primary suite with dual walk-in closets",
"Three-car garage",
"Whole-home generator",
"Smart home automation system",
"Walking distance to Venetian Village shops and restaurants",
"No HOA fees",
"Hurricane impact windows and doors"
]
}
# Run the agent
agent = RealEstateListingAgent()
listing = agent.run(sample_property)
print("\n" + "="*60)
print("FINAL GENERATED LISTING")
print("="*60)
print(listing)
Run it with python listing_agent.py. Here's what the output looks like:
🏠 Starting listing generation agent...
📡 Claude stop reason: tool_use
🔧 Tool called: property_data_tool
Input: {
"address": "4821 Gulf Shore Blvd N, Naples, FL 34103",
"price": 2450000,
"bedrooms": 4, ...
📡 Claude stop reason: tool_use
🔧 Tool called: seo_optimizer_tool
Input: {
"draft_listing": "Stunning Gulf-View Estate in Naples, FL...",
"target_keywords": ["Naples FL luxury homes for sale", ...
📡 Claude stop reason: end_turn
============================================================
FINAL GENERATED LISTING
============================================================
# Stunning Gulf-View Estate in Naples, FL | 4BR/3.5BA — $2,450,000
**Discover luxury waterfront living at 4821 Gulf Shore Blvd N, Naples, FL 34103** —
one of Southwest Florida's most sought-after addresses. This exceptional 4-bedroom,
3.5-bathroom single-family home spans 3,200 sq ft of meticulously designed living
space, delivering a price-per-square-foot value of $766 in the heart of Naples real
estate's premier corridor.
## Why This Naples Home Stands Apart
Built in 2019, this residence was designed for buyers who refuse to compromise.
Sweeping Gulf views greet you from the moment you step inside, setting the tone for
a home where every detail has been considered.
**Gourmet chef's kitchen** anchored by Sub-Zero appliances makes entertaining
effortless. The resort-style pool and spa transform your backyard into a private
retreat — all on a generous 0.31-acre lot with no HOA fees to limit how you live.
## Luxury Features at a Glance
- 🌊 Unobstructed Gulf views
- 🏊 Resort-style pool and spa
- 🍳 Chef's kitchen with Sub-Zero appliances
- 🚗 Three-car garage
- ⚡ Whole-home generator for year-round peace of mind
- 🏠 Smart home automation system
- 🪟 Hurricane impact windows and doors throughout
- 🛍️ Steps from Venetian Village shops and dining
## Prime Naples Location
Homes for sale in Naples FL at this price point rarely offer this combination of
Gulf views, modern construction, and walkability. Venetian Village — Naples' iconic
waterfront shopping and dining district — is just steps away.
## Schedule Your Private Showing
Homes for sale in Naples FL with Gulf views at this price move fast. Contact your
agent today to arrange a private tour of this exceptional Naples luxury real estate
opportunity before it's gone.
**Listed at $2,450,000 | 4 BR | 3.5 BA | 3,200 Sq Ft | Built 2019 | No HOA**
How It Works
Here's the plain-English version of what just happened. You sent Claude a JSON blob of raw property data. Claude read the system prompt, decided to call property_data_tool first, and passed the property fields as arguments.
Your Python code ran execute_property_data_tool, formatted the price and square footage nicely, calculated price-per-sqft, and returned structured JSON. Claude read that