What You'll Build
If you've ever lost a hot lead because it got buried in a spreadsheet, this tutorial is for you. You're going to build a real estate lead scoring agent in Python that uses the Claude API to automatically evaluate inbound leads, assign a score from 1–100, and explain exactly why each lead ranked where it did.
By the end, you'll have a working multi-tool agent that can process 100+ leads without any manual review. It's the same kind of system we build for real estate clients here in Southwest Florida at Naples AI — and you can run it locally in under an hour.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic Python knowledge — you don't need to be an expert
anthropicSDK installed (pip install anthropic)- A terminal and a text editor or VS Code
Step 1: Set Up Your Claude API Environment
First, install the Anthropic SDK and set your API key as an environment variable. I keep mine in a .env file so I don't accidentally commit it to GitHub.
Run this in your terminal to install the SDK:
terminalpip install anthropic python-dotenv
Then create a .env file in your project root:
ANTHROPIC_API_KEY=sk-ant-your-key-here
Now create your main project file and verify the connection works:
verify_connection.pyimport os
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=64,
messages=[{"role": "user", "content": "Say: API connection successful."}]
)
print(message.content[0].text)
You should see: API connection successful. If you get an auth error, double-check your key in the .env file. Once that's working, move on.
Step 2: Define Lead Scoring Tools and Criteria
This is where the agent gets its intelligence. Claude's tool use feature lets you define functions the model can call during its reasoning loop. We're defining two tools: one to score a lead and one to flag it for follow-up.
Here's the tool definitions file. The JSON schemas tell Claude exactly what data each tool expects, so it fills them in correctly from the lead data you pass it.
tools.pyLEAD_SCORING_TOOLS = [
{
"name": "score_lead",
"description": (
"Evaluates a real estate lead and assigns a score from 1 to 100 "
"based on budget, timeline, motivation, property type, and contact quality. "
"Use this tool to analyze every lead before making a recommendation."
),
"input_schema": {
"type": "object",
"properties": {
"lead_score": {
"type": "integer",
"description": "Overall lead quality score from 1 (cold) to 100 (hot). Base this on all available lead data.",
"minimum": 1,
"maximum": 100
},
"budget_score": {
"type": "integer",
"description": "Score from 1-25 based on stated budget. 25 = budget matches local market, 1 = no budget stated.",
"minimum": 1,
"maximum": 25
},
"timeline_score": {
"type": "integer",
"description": "Score from 1-25 based on purchase timeline. 25 = within 30 days, 1 = no timeline stated.",
"minimum": 1,
"maximum": 25
},
"motivation_score": {
"type": "integer",
"description": "Score from 1-25 based on how motivated the buyer appears. 25 = relocating for job or life event, 1 = just browsing.",
"minimum": 1,
"maximum": 25
},
"contact_quality_score": {
"type": "integer",
"description": "Score from 1-25 based on contact info quality. 25 = phone + email + scheduled call, 1 = email only.",
"minimum": 1,
"maximum": 25
},
"tier": {
"type": "string",
"enum": ["HOT", "WARM", "COLD"],
"description": "Lead tier. HOT = score 75-100, WARM = 40-74, COLD = 1-39."
},
"reasoning": {
"type": "string",
"description": "2-3 sentence plain-English explanation of why this lead received this score."
}
},
"required": [
"lead_score",
"budget_score",
"timeline_score",
"motivation_score",
"contact_quality_score",
"tier",
"reasoning"
]
}
},
{
"name": "flag_for_followup",
"description": (
"Flags a lead for immediate agent follow-up and sets a recommended contact method. "
"Call this tool after score_lead if the lead tier is HOT or WARM."
),
"input_schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["IMMEDIATE", "WITHIN_24H", "THIS_WEEK"],
"description": "How quickly an agent should follow up. HOT leads = IMMEDIATE."
},
"recommended_contact_method": {
"type": "string",
"enum": ["PHONE_CALL", "TEXT_MESSAGE", "EMAIL"],
"description": "Best way to reach this lead based on their contact info and behavior."
},
"talking_points": {
"type": "array",
"items": {"type": "string"},
"description": "3 specific talking points for the agent to use when contacting this lead.",
"minItems": 3,
"maxItems": 3
}
},
"required": ["priority", "recommended_contact_method", "talking_points"]
}
}
]
The sub-scores add up to a max of 100, which makes the total score transparent and auditable. That matters a lot when a sales manager asks why lead A ranked higher than lead B.
Step 3: Build the Multi-Tool Agent Class
Now we build the agent itself. The LeadScorerAgent class wraps the Claude client, holds the tool definitions, and processes each lead through the model. The key pattern here is tool_use — Claude decides which tools to call and in what order.
import os
import json
import anthropic
from dotenv import load_dotenv
from tools import LEAD_SCORING_TOOLS
load_dotenv()
SYSTEM_PROMPT = """You are a real estate lead qualification specialist working for a
Southwest Florida real estate agency. Your job is to evaluate inbound leads and score
them objectively based on budget alignment, purchase timeline, buyer motivation, and
contact quality.
Always call score_lead first. If the lead scores WARM or HOT, also call flag_for_followup
with specific, actionable talking points tailored to that lead's situation.
Be direct and honest in your reasoning. A lead with no budget and no timeline is COLD —
say so clearly. Don't inflate scores."""
class LeadScorerAgent:
def __init__(self):
self.client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY")
)
self.model = "claude-sonnet-4-5"
self.tools = LEAD_SCORING_TOOLS
self.system_prompt = SYSTEM_PROMPT
def format_lead_for_prompt(self, lead: dict) -> str:
"""Convert a lead dict into a clear text prompt for the model."""
lines = ["Evaluate this real estate lead:\n"]
for key, value in lead.items():
# Format key from snake_case to Title Case for readability
label = key.replace("_", " ").title()
lines.append(f"- {label}: {value}")
return "\n".join(lines)
def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""
Simulate tool execution and return a result string.
In production, this would write to a CRM or database.
"""
if tool_name == "score_lead":
return json.dumps({
"status": "scored",
"lead_score": tool_input["lead_score"],
"tier": tool_input["tier"]
})
elif tool_name == "flag_for_followup":
return json.dumps({
"status": "flagged",
"priority": tool_input["priority"],
"contact_method": tool_input["recommended_contact_method"]
})
return json.dumps({"status": "unknown_tool"})
def score_lead(self, lead: dict) -> dict:
"""
Run the agent loop for a single lead.
Returns a structured result dict with score, tier, and followup details.
"""
user_message = self.format_lead_for_prompt(lead)
messages = [{"role": "user", "content": user_message}]
result = {
"lead_name": lead.get("name", "Unknown"),
"score_data": None,
"followup_data": None,
"raw_tool_calls": []
}
# Run the agent loop — it continues until Claude stops calling tools
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_prompt,
tools=self.tools,
messages=messages
)
# Collect any tool calls Claude made in this response
tool_use_blocks = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_use_blocks:
# No more tool calls — agent is done
break
# Build the assistant message with all content blocks
messages.append({
"role": "assistant",
"content": response.content
})
# Execute each tool and collect results
tool_results = []
for tool_block in tool_use_blocks:
tool_name = tool_block.name
tool_input = tool_block.input
result["raw_tool_calls"].append({
"tool": tool_name,
"input": tool_input
})
# Store structured data from each tool call
if tool_name == "score_lead":
result["score_data"] = tool_input
elif tool_name == "flag_for_followup":
result["followup_data"] = tool_input
# Simulate tool execution and get result
tool_output = self.process_tool_call(tool_name, tool_input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": tool_output
})
# Feed tool results back into the conversation
messages.append({
"role": "user",
"content": tool_results
})
# Stop if Claude indicated it's done
if response.stop_reason == "end_turn":
break
return result
The while True loop is the heart of the agent pattern. Claude keeps calling tools until it has everything it needs, then stops. This is what makes it a real agent rather than just a single API call.
Step 4: Implement the Agent Run Loop
Now we write the runner that processes a batch of leads and prints formatted results. This is the script you'd actually run from the terminal — or schedule as a cron job to process leads overnight.
run_agent.pyimport json
from lead_scorer import LeadScorerAgent
def print_lead_result(result: dict) -> None:
"""Print a formatted summary of a scored lead to the terminal."""
print("\n" + "=" * 60)
print(f"LEAD: {result['lead_name']}")
print("=" * 60)
score_data = result.get("score_data")
if score_data:
tier = score_data.get("tier", "N/A")
score = score_data.get("lead_score", "N/A")
tier_emoji = {"HOT": "🔥", "WARM": "🌡️", "COLD": "❄️"}.get(tier, "")
print(f"Overall Score: {score}/100 {tier_emoji} {tier}")
print(f"\nSub-scores:")
print(f" Budget: {score_data.get('budget_score', 'N/A')}/25")
print(f" Timeline: {score_data.get('timeline_score', 'N/A')}/25")
print(f" Motivation: {score_data.get('motivation_score', 'N/A')}/25")
print(f" Contact Quality: {score_data.get('contact_quality_score', 'N/A')}/25")
print(f"\nReasoning: {score_data.get('reasoning', '')}")
followup_data = result.get("followup_data")
if followup_data:
print(f"\nFollow-Up Priority: {followup_data.get('priority', 'N/A')}")
print(f"Contact Method: {followup_data.get('recommended_contact_method', 'N/A')}")
print("\nTalking Points:")
for i, point in enumerate(followup_data.get("talking_points", []), 1):
print(f" {i}. {point}")
else:
print("\nNo follow-up flagged (COLD lead).")
print("=" * 60)
def run_batch(leads: list) -> list:
"""Process a list of leads through the scoring agent."""
agent = LeadScorerAgent()
all_results = []
print(f"\nProcessing {len(leads)} leads...\n")
for i, lead in enumerate(leads, 1):
print(f"Scoring lead {i}/{len(leads)}: {lead.get('name', 'Unknown')}...")
result = agent.score_lead(lead)
print_lead_result(result)
all_results.append(result)
# Summary report at the end
print("\n" + "=" * 60)
print("BATCH SUMMARY")
print("=" * 60)
tiers = {"HOT": 0, "WARM": 0, "COLD": 0}
for r in all_results:
tier = r.get("score_data", {}).get("tier", "COLD")
tiers[tier] = tiers.get(tier, 0) + 1
print(f"🔥 HOT leads: {tiers['HOT']}")
print(f"🌡️ WARM leads: {tiers['WARM']}")
print(f"❄️ COLD leads: {tiers['COLD']}")
print(f"Total processed: {len(all_results)}")
return all_results
if __name__ == "__main__":
from sample_leads import SAMPLE_LEADS
results = run_batch(SAMPLE_LEADS)
# Optionally save results to JSON for CRM import
with open("scored_leads.json", "w") as f:
json.dump(results, f, indent=2)
print("\nResults saved to scored_leads.json")
Step 5: Test with Real Estate Lead Data
Here are three sample leads that cover the full range — a hot buyer relocating from out of state, a warm lead who's still shopping, and a cold tire-kicker with no budget. Run these and you'll see exactly how the agent differentiates between them.
sample_leads.pySAMPLE_LEADS = [
{
"name": "Maria Gonzalez",
"email": "[email protected]",
"phone": "239-555-0147",
"source": "Zillow inquiry",
"message": (
"Hi, my husband and I are relocating from Chicago to Naples for his new job "
"starting July 1st. We need to be in a home by June 15th. Our budget is $750,000 "
"to $850,000. We're looking for 4 bed, 3 bath with a pool, preferably in a gated "
"community. We're pre-approved with Chase. Can we schedule a call this week?"
),
"property_type": "Single Family Home",
"budget_stated": "$750,000 - $850,000",
"timeline": "Need to close by June 15th",
"pre_approved": True,
"contact_info_complete": True
},
{
"name": "Derek Thompson",
"email": "[email protected]",
"phone": "239-555-0293",
"source": "Website contact form",
"message": (
"Looking to buy a condo in Naples or Bonita Springs sometime in the next 6 months. "
"Budget is around $350,000-$400,000. Want 2 bed, 2 bath. Not in a huge rush but "
"actively looking. Haven't started the mortgage process yet but have good credit."
),
"property_type": "Condo",
"budget_stated": "$350,000 - $400,000",
"timeline": "6 months",
"pre_approved": False,
"contact_info_complete": True
},
{
"name": "Anonymous User",
"email": "[email protected]",
"phone": None,
"source": "Facebook Ad click",
"message": (
"Just curious what prices are like in Naples. Maybe someday would be nice to have "
"a place down there. No real budget in mind."
),
"property_type": "Unknown",
"budget_stated": None,
"timeline": "No timeline",
"pre_approved": False,
"contact_info_complete": False
}
]
Run everything with:
terminalpython run_agent.py
Here's a condensed version of what the actual output looks like:
sample outputProcessing 3 leads...
Scoring lead 1/3: Maria Gonzalez...
============================================================
LEAD: Maria Gonzalez
============================================================
Overall Score: 96/100 🔥 HOT
Sub-scores:
Budget: 24/25
Timeline: 25/25
Motivation: 24/25
Contact Quality: 23/25
Reasoning: Maria is a highly motivated buyer with a firm deadline driven by a job
relocation, a clear budget range that aligns well with Naples inventory, and mortgage
pre-approval already in place. She provided full contact info and is requesting a call
this week, signaling strong intent.
Follow-Up Priority: IMMEDIATE
Contact Method: PHONE_CALL
Talking Points:
1. Confirm available listings in gated communities with pools between $750K-$850K
that can close before June 15th.
2. Offer to connect her with a local real estate attorney who handles fast closings
for corporate relocations.
3. Ask about school district preferences for her children given the July 1st start date.
============================================================
Scoring lead 2/3: Derek Thompson...
============================================================
LEAD: Derek Thompson
============================================================
Overall Score: 58/100 🌡️ WARM
Sub-scores:
Budget: 18/25
Timeline: 12/25
Motivation: 15/25
Contact Quality: 13/25
Reasoning: Derek has a realistic budget for the Naples/Bonita Springs condo market
and is actively looking, but his 6-month timeline and lack of pre-approval reduce
urgency. Worth nurturing with listings and a gentle push toward mortgage pre-qualification.
Follow-Up Priority: WITHIN_24H
Contact Method: EMAIL
...
============================================================
Scoring lead 3/3: Anonymous User...
============================================================
LEAD: Anonymous User
============