← Back to Blog

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.

📦 Full Source Code Note: The complete, working source code is broken into numbered steps below. Each step builds on the last, so by the time you reach Step 5, you'll have a fully functional agent. Copy each block in order and you're done.

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
  • anthropic SDK 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:

terminal
pip install anthropic python-dotenv

Then create a .env file in your project root:

.env
ANTHROPIC_API_KEY=sk-ant-your-key-here

Now create your main project file and verify the connection works:

verify_connection.py
import 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.

💡 Tip: Always load your API key from environment variables, never hardcode it in your source files. If you push a hardcoded key to a public repo, Anthropic will automatically revoke it.

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.py
LEAD_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.

lead_scorer.py
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.py
import 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.py
SAMPLE_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:

terminal
python run_agent.py

Here's a condensed version of what the actual output looks like:

sample output
Processing 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
============