← Back to Blog

If you're manually qualifying real estate leads, you're losing time on buyers who were never going to close. This tutorial walks you through exactly how to build an AI lead scoring agent in Python using the Claude API — one that reads lead data, analyzes buying intent, and returns a scored qualification report automatically.

This is the same kind of system we build for real estate clients at Naples AI. I'm going to show you the full working code and explain every part of it.

What You'll Build

You'll build a Python-based lead scoring agent that accepts raw lead data (budget, timeline, property preferences, contact behavior) and returns a structured qualification score with reasoning. The agent uses Claude's tool use feature to call specialized scoring functions — one for property match analysis and one for buyer readiness qualification.

By the end, you'll have a working agent loop you can drop into any real estate CRM or backend system.

Prerequisites

  • Python 3.9 or higher installed
  • An Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with Python classes and dictionaries
  • The anthropic Python package installed (pip install anthropic)
  • A text editor or IDE (VS Code works great)
📦 Full Source Code
The complete, working code for this tutorial is built step by step in the sections below. Each snippet is part of the same file — lead_scoring_agent.py. By Step 4, you'll have the entire agent ready to run. No pseudocode, no placeholders.

Step 1: Set Up the Anthropic SDK and API Keys

First, install the Anthropic SDK if you haven't already. Open your terminal and run:

terminal
pip install anthropic

Now create your project file and set up the SDK with your API key. I recommend storing the key in an environment variable rather than hardcoding it — you'll thank yourself later when you push this to GitHub.

lead_scoring_agent.py
import os
import json
import anthropic

# Load your API key from the environment
# Set it with: export ANTHROPIC_API_KEY="your-key-here"
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

That's it for setup. The anthropic.Anthropic() client is what we'll use to send messages and handle tool calls throughout the agent loop.

Step 2: Define Lead Data Schema and Scoring Tools

This is where things get interesting. Claude's tool use lets the model decide when to call a function and what arguments to pass. We define two tools: one for analyzing how well a property matches the lead's criteria, and one for scoring overall buyer readiness.

First, let's define what a lead looks like. This is the data structure the agent will work with:

lead_scoring_agent.py
def create_sample_lead() -> dict:
    """Returns a realistic sample lead for testing."""
    return {
        "lead_id": "lead_001",
        "name": "Marcus and Jennifer Holloway",
        "contact_info": {
            "email": "[email protected]",
            "phone": "239-555-0147",
            "preferred_contact": "email"
        },
        "budget": {
            "min": 850000,
            "max": 1200000,
            "financing": "pre_approved",
            "down_payment_percent": 20
        },
        "timeline": {
            "urgency": "3_to_6_months",
            "flexibility": "moderate",
            "currently_renting": True,
            "lease_end": "2026-08-01"
        },
        "property_preferences": {
            "type": "single_family",
            "min_bedrooms": 4,
            "min_bathrooms": 3,
            "preferred_areas": ["Naples", "Bonita Springs", "Marco Island"],
            "must_haves": ["pool", "3_car_garage", "waterfront_or_golf"],
            "nice_to_haves": ["home_office", "guest_suite"]
        },
        "engagement": {
            "source": "website_inquiry",
            "pages_viewed": 14,
            "listings_saved": 6,
            "days_since_first_contact": 8,
            "response_rate": "high",
            "scheduled_showing": False
        }
    }

Now define the two tools. The tool definitions tell Claude what each function does and what parameters it expects. Claude will decide when to call them based on context.

lead_scoring_agent.py
TOOLS = [
    {
        "name": "analyze_property_match",
        "description": (
            "Analyzes how well the available inventory matches a lead's property "
            "preferences. Considers budget alignment, location preferences, property "
            "features, and current market availability in Southwest Florida. "
            "Returns a match score from 0-100 and specific match/gap details."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "The unique identifier for the lead"
                },
                "budget_max": {
                    "type": "number",
                    "description": "The lead's maximum budget in USD"
                },
                "preferred_areas": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of preferred geographic areas"
                },
                "must_have_features": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Non-negotiable property features"
                },
                "min_bedrooms": {
                    "type": "integer",
                    "description": "Minimum number of bedrooms required"
                }
            },
            "required": ["lead_id", "budget_max", "preferred_areas", "must_have_features", "min_bedrooms"]
        }
    },
    {
        "name": "score_buyer_readiness",
        "description": (
            "Evaluates a lead's readiness to purchase based on financial qualification, "
            "timeline urgency, and engagement signals. Returns a readiness score from "
            "0-100 with a priority tier (Hot/Warm/Cold) and recommended next action."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "lead_id": {
                    "type": "string",
                    "description": "The unique identifier for the lead"
                },
                "financing_status": {
                    "type": "string",
                    "enum": ["pre_approved", "pre_qualified", "cash", "not_started"],
                    "description": "Current financing status of the buyer"
                },
                "timeline_urgency": {
                    "type": "string",
                    "enum": ["immediate", "1_to_3_months", "3_to_6_months", "6_plus_months"],
                    "description": "How urgently the lead needs to purchase"
                },
                "engagement_score_inputs": {
                    "type": "object",
                    "description": "Engagement metrics from CRM or website",
                    "properties": {
                        "pages_viewed": {"type": "integer"},
                        "listings_saved": {"type": "integer"},
                        "days_since_first_contact": {"type": "integer"},
                        "response_rate": {"type": "string"},
                        "scheduled_showing": {"type": "boolean"}
                    },
                    "required": ["pages_viewed", "listings_saved", "days_since_first_contact",
                                 "response_rate", "scheduled_showing"]
                }
            },
            "required": ["lead_id", "financing_status", "timeline_urgency", "engagement_score_inputs"]
        }
    }
]

Step 3: Create the Lead Scoring Agent with Tool Use

Now we build the actual tool execution functions and the agent class. The tool functions simulate what would be real database or MLS lookups in a production system. The agent class wraps everything and manages the conversation with Claude.

lead_scoring_agent.py
def execute_analyze_property_match(params: dict) -> dict:
    """
    Simulates a property match analysis against current inventory.
    In production, this would query your MLS feed or property database.
    """
    budget_max = params["budget_max"]
    areas = params["preferred_areas"]
    must_haves = params["must_have_features"]
    min_beds = params["min_bedrooms"]

    # Simulate inventory check for Naples/Southwest Florida market
    inventory_available = budget_max >= 750000  # Realistic floor for the market
    area_coverage = sum(1 for a in areas if a in ["Naples", "Bonita Springs", "Marco Island", "Estero"])
    area_score = min((area_coverage / len(areas)) * 100, 100) if areas else 0

    # Score based on how many must-haves are findable in current inventory
    findable_features = {"pool", "3_car_garage", "waterfront_or_golf", "home_office", "guest_suite"}
    feature_hits = sum(1 for f in must_haves if f in findable_features)
    feature_score = (feature_hits / len(must_haves)) * 100 if must_haves else 0

    bed_score = 100 if min_beds <= 5 else 60  # 5+ bed homes are less common

    # Weighted average match score
    match_score = round((area_score * 0.35) + (feature_score * 0.40) + (bed_score * 0.25))

    return {
        "match_score": match_score,
        "inventory_available": inventory_available,
        "matching_listings_estimated": max(0, int((match_score / 100) * 12)),
        "area_coverage_score": round(area_score),
        "feature_availability_score": round(feature_score),
        "notes": (
            f"Strong inventory alignment in {', '.join(areas[:2])}. "
            f"{feature_hits} of {len(must_haves)} must-have features readily available. "
            f"Budget at ${budget_max:,} is well-positioned for this market segment."
        )
    }


def execute_score_buyer_readiness(params: dict) -> dict:
    """
    Scores buyer readiness based on financing, timeline, and engagement data.
    Returns a tier classification and recommended next action.
    """
    financing = params["financing_status"]
    timeline = params["timeline_urgency"]
    eng = params["engagement_score_inputs"]

    # Financing scores
    financing_map = {"pre_approved": 100, "cash": 100, "pre_qualified": 65, "not_started": 20}
    finance_score = financing_map.get(financing, 20)

    # Timeline urgency scores
    timeline_map = {"immediate": 100, "1_to_3_months": 85, "3_to_6_months": 65, "6_plus_months": 35}
    timeline_score = timeline_map.get(timeline, 35)

    # Engagement scoring
    engagement_points = 0
    engagement_points += min(eng["pages_viewed"] * 3, 30)       # Up to 30 pts
    engagement_points += min(eng["listings_saved"] * 5, 25)     # Up to 25 pts
    recency = max(0, 30 - eng["days_since_first_contact"])       # Fresh leads score higher
    engagement_points += recency
    response_map = {"high": 15, "medium": 8, "low": 2}
    engagement_points += response_map.get(eng["response_rate"], 2)
    if eng["scheduled_showing"]:
        engagement_points += 20
    engagement_score = min(engagement_points, 100)

    # Composite readiness score (weighted)
    readiness_score = round(
        (finance_score * 0.40) +
        (timeline_score * 0.30) +
        (engagement_score * 0.30)
    )

    # Tier classification
    if readiness_score >= 75:
        tier = "Hot"
        next_action = "Call within 2 hours. Offer to schedule a showing this week."
    elif readiness_score >= 50:
        tier = "Warm"
        next_action = "Send curated listing package within 24 hours. Follow up in 3 days."
    else:
        tier = "Cold"
        next_action = "Add to nurture email sequence. Check back in 30 days."

    return {
        "readiness_score": readiness_score,
        "priority_tier": tier,
        "finance_score": finance_score,
        "timeline_score": timeline_score,
        "engagement_score": round(engagement_score),
        "recommended_next_action": next_action
    }


# Map tool names to their execution functions
TOOL_EXECUTORS = {
    "analyze_property_match": execute_analyze_property_match,
    "score_buyer_readiness": execute_score_buyer_readiness
}


class LeadScoringAgent:
    """
    An AI agent that scores real estate leads using Claude and tool use.
    Initialize with an Anthropic client and a lead data dictionary.
    """

    def __init__(self, client: anthropic.Anthropic, model: str = MODEL):
        self.client = client
        self.model = model
        self.system_prompt = """You are an expert real estate lead qualification specialist for a 
Southwest Florida luxury real estate agency. Your job is to thoroughly analyze incoming leads 
and produce a complete qualification report.

When given lead data, you MUST:
1. Call analyze_property_match to evaluate inventory alignment
2. Call score_buyer_readiness to evaluate purchase intent and financial readiness  
3. Synthesize both tool results into a final qualification summary

Always call both tools before writing your final summary. Be specific and actionable."""

    def score_lead(self, lead_data: dict) -> str:
        """
        Run the full scoring pipeline for a single lead.
        Returns the agent's final qualification report as a string.
        """
        # Format the lead data as the initial user message
        user_message = f"""Please fully qualify this incoming real estate lead and provide a complete scoring report.

Lead Data:
{json.dumps(lead_data, indent=2)}

Analyze their property match against our current Southwest Florida inventory and score their 
buyer readiness, then give me a final qualification summary with your recommendation."""

        messages = [{"role": "user", "content": user_message}]

        print(f"\n[Agent] Starting lead scoring for: {lead_data.get('name', 'Unknown')}")

        # Run the agentic loop (defined in Step 4)
        return self._run_agent_loop(messages)

Step 4: Implement the Agent Run Loop

This is the core of how the agent works. The loop keeps sending messages to Claude, checks if it wants to use a tool, executes that tool, and feeds the result back — until Claude has everything it needs to write the final report.

lead_scoring_agent.py
    def _run_agent_loop(self, messages: list) -> str:
        """
        The agentic loop: send messages, handle tool calls, repeat until done.
        Claude will stop the loop naturally when it returns end_turn stop reason.
        """
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=self.system_prompt,
                tools=TOOLS,
                messages=messages
            )

            print(f"[Agent] Stop reason: {response.stop_reason}")

            # If Claude is done (no more tool calls), return the final text
            if response.stop_reason == "end_turn":
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text
                return "No final response generated."

            # Process tool use blocks
            if response.stop_reason == "tool_use":
                # Add Claude's response (including tool call) to message history
                messages.append({
                    "role": "assistant",
                    "content": response.content
                })

                # Build the tool results list to send back
                tool_results = []

                for block in response.content:
                    if block.type == "tool_use":
                        tool_name = block.name
                        tool_input = block.input
                        tool_use_id = block.id

                        print(f"[Agent] Calling tool: {tool_name}")
                        print(f"[Agent] Tool input: {json.dumps(tool_input, indent=2)}")

                        # Execute the tool and capture the result
                        if tool_name in TOOL_EXECUTORS:
                            result = TOOL_EXECUTORS[tool_name](tool_input)
                        else:
                            result = {"error": f"Unknown tool: {tool_name}"}

                        print(f"[Agent] Tool result: {json.dumps(result, indent=2)}")

                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": tool_use_id,
                            "content": json.dumps(result)
                        })

                # Send all tool results back to Claude in a single user turn
                messages.append({
                    "role": "user",
                    "content": tool_results
                })

            else:
                # Unexpected stop reason — break to avoid infinite loop
                print(f"[Agent] Unexpected stop reason: {response.stop_reason}")
                break

        return "Agent loop ended unexpectedly."


# ── Entry point ────────────────────────────────────────────────────────────────

def main():
    agent = LeadScoringAgent(client=client)
    lead = create_sample_lead()
    report = agent.score_lead(lead)

    print("\n" + "="*60)
    print("LEAD QUALIFICATION REPORT")
    print("="*60)
    print(report)


if __name__ == "__main__":
    main()

Run it from the terminal with:

terminal
export ANTHROPIC_API_KEY="your-key-here"
python lead_scoring_agent.py

Here's the kind of output you'll see:

sample output
[Agent] Starting lead scoring for: Marcus and Jennifer Holloway
[Agent] Stop reason: tool_use
[Agent] Calling tool: analyze_property_match
[Agent] Tool input: {
  "lead_id": "lead_001",
  "budget_max": 1200000,
  "preferred_areas": ["Naples", "Bonita Springs", "Marco Island"],
  "must_have_features": ["pool", "3_car_garage", "waterfront_or_golf"],
  "min_bedrooms": 4
}
[Agent] Tool result: {
  "match_score": 88,
  "inventory_available": true,
  "matching_listings_estimated": 10,
  "area_coverage_score": 100,
  "feature_availability_score": 100,
  "notes": "Strong inventory alignment in Naples, Bonita Springs. 3 of 3 must-have features readily available. Budget at $1,200,000 is well-positioned for this market segment."
}
[Agent] Calling tool: score_buyer_readiness
[Agent] Tool input: {
  "lead_id": "lead_001",
  "financing_status": "pre_approved",
  "timeline_urgency": "3_to_6_months",
  "engagement_score_inputs": {
    "pages_viewed": 14,
    "listings_saved": 6,
    "days_since_first_contact": 8,
    "response_rate": "high",
    "scheduled_showing": false
  }
}
[Agent] Tool result: {
  "readiness_score": 80,
  "priority_tier": "Hot",
  "finance_score": 100,
  "timeline_score": 65,
  "engagement_score": 84,
  "recommended_next_action": "Call within 2 hours. Offer to schedule a showing this week."
}
[Agent] Stop reason: end_turn

============================================================
LEAD QUALIFICATION REPORT
============================================================
## Lead Qualification Report — Marcus and Jennifer Holloway

**Overall Assessment: HOT LEAD — Priority Action Required**

---

### Property Match Score: 88/100
Our current Southwest Florida inventory is an excellent fit for the Holloways.
All three of their must-have features (pool, 3-car garage, waterfront/golf) are
readily available in their preferred markets. With an estimated 10 matching
listings in Naples, Bonita Springs, and Marco Island at their $850K–$1.2M budget,
we have strong options to present immediately.

### Buyer Readiness Score: 80/100 — HOT
- **Financing:** Pre-approved at 20% down — they can close when the right home appears
- **Timeline:** 3–6 month window with a hard lease-end date of August 1, 2026 — creates
  natural urgency in the next 60–90 days
- **Engagement:** 14 pages viewed, 6 listings saved in just 8 days — this is a motivated,
  active buyer showing serious research behavior

### Recommended Next Action
**Call within 2 hours.** Reference the listings they saved and offer to schedule
a showing block this week. Prioritize waterfront and golf-community listings
in the $950K–$1.15M range. Their lease deadline makes them highly closeable
before summer.