mcpField
P
propensity-scoring-realtime

propensity-scoring-realtime

github.com/louisblytheMarketing & growth190402 installsUpdated 40w ago
Repository

About propensity-scoring-realtime

A skill for sales-bot developers on updating prospect scores as interactions unfold. It describes firmographic, engagement, and conversation signals, score decay, and actions driven by current scores.

Key features

  1. 01Set a baseline from firmographics
  2. 02Update scores from engagement and conversations
  3. 03Decay scores over time and inactivity
  4. 04Route prospects by score
  5. 05Review score timelines and distributions

Use cases

  1. 01Prioritize leads as new conversation signals arrive
  2. 02Adjust follow-up by score tier
  3. 03Spot prospects whose engagement has declined
  4. 04Check how scores relate to sales outcomes

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/louisblythe/sales-skills --skill propensity-scoring-realtime

After installing, type/propensity-scoring-realtimein Claude Code to confirm it loaded.

Details

From the project README

Propensity Scoring in Real-Time

You are an expert in building sales bots that dynamically update lead scores as conversations progress. Your goal is to help developers create systems that re-evaluate prospect quality with every interaction.

Why Real-Time Scoring Matters

Static Scoring Limitations
Traditional lead scoring:
- Scored once at creation
- Based on firmographics only
- Doesn't reflect engagement
- Score stale by first conversation

Lead created at score 75 (based on company size).
Three weeks later, still 75, even after:
- Ignored 5 emails
- Bounced from website
- Never responded

Should be much lower.
Real-Time Scoring
Dynamic scoring:
- Updates with every signal
- Reflects actual engagement
- Predicts current likelihood
- Enables smart prioritization

Lead starts at 75.
After engagement signals:
- Opened 3 emails: +10
- Clicked pricing: +15
- Asked about timeline: +20
- Mentioned budget: +25

New score: 145 (high priority)

Scoring Components

Firmographic Baseline
Starting score based on fit:

Company size:
- <10 employees: +10
- 10-50: +20
- 51-200: +30
- 201-1000: +40
- 1000+: +50

Industry fit:
- Ideal industry: +30
- Good fit: +20
- Neutral: +10
- Poor fit: 0

Title/Role:
- Decision maker: +30
- Influencer: +20
- User: +10
- Unknown: +5
Engagement Signals
Real-time adjustments:

Email engagement:
- Opened: +5 per open
- Clicked: +10 per click
- Replied: +25
- Positive reply: +40

Website behavior:
- Page visit: +3
- Pricing page: +15
- Demo page: +20
- Multiple pages: +5 per page
- Return visit: +10

Content engagement:
- Downloaded: +15
- Video watched: +10 per 25% completion
- Webinar registered: +20
- Webinar attended: +35
Conversation Signals
From bot conversations:

Questions asked:
- Feature question: +5
- Pricing question: +20
- Timeline question: +25
- Implementation: +20
- Competitor mention: +15

Buying signals:
- "We need": +15
- "When can we": +20
- "Who else uses": +10
- "What's the process": +25
- Budget mentioned: +30

Objection signals:
- Price objection: -5 (but engaged)
- Timing objection: -10
- "Not interested": -30
- Unsubscribe request: -50

Real-Time Implementation

Score Calculation Engine
class PropensityScorer:
    def __init__(self, prospect):
        self.prospect = prospect
        self.base_score = self.calculate_base_score()
        self.engagement_score = 0
        self.conversation_score = 0
        self.decay_factor = 0

    def calculate_base_score(self):
        score = 0
        score += COMPANY_SIZE_SCORES.get(self.prospect.company_size_bucket, 10)
        score += INDUSTRY_SCORES.get(self.prospect.industry, 10)
        score += TITLE_SCORES.get(self.prospect.title_level, 5)
        return score

    def update_on_event(self, event):
        """Called in real-time when events occur"""

        # Get score delta for event type
        delta = EVENT_SCORES.get(event.type, 0)

        # Apply context multipliers
        if event.recency < timedelta(hours=1):
            delta *= 1.5  # Recent activity bonus
        if event.context.get("high_intent_page"):
            delta *= 1.3

        # Update appropriate component
        if event.category == "engagement":
            self.engagement_score += delta
        elif event.category == "conversation":
            self.conversation_score += delta

        # Apply decay to old scores
        self.apply_decay()

        # Emit score update event
        self.emit_score_change()

    def get_current_score(self):
        return (
            self.base_score +
            self.engagement_score +
            self.conversation_score -
            self.decay_factor
        )

    def get_score_tier(self):
        score = self.get_current_score()
        if score >= 100:
            return "hot"
        elif score >= 60:
            return "warm"
        elif score >= 30:
            return "cool"
        else:
            return "cold"
Event Processing Pipeline
class ScoreEventProcessor:
    def __init__(self):
        self.scorers = {}

    def process_event(self, event):
        prospect_id = event.prospect_id

        # Get or create scorer
        if prospect_id not in self.scorers:
            prospect = load_prospect(prospect_id)
            self.scorers[prospect_id] = PropensityScorer(prospect)

        scorer = self.scorers[prospect_id]

        # Update score
        old_score = scorer.get_current_score()
        scorer.update_on_event(event)
        new_score = scorer.get_current_score()

        # Check for tier changes
        old_tier = get_tier(old_score)
        new_tier = get_tier(new_score)

        if new_tier != old_tier:
            self.handle_tier_change(prospect_id, old_tier, new_tier)

        # Persist score
        update_prospect_score(prospect_id, new_score, new_tier)

        return {
            "prospect_id": prospect_id,
            "old_score": old_score,
            "new_score": new_score,
            "tier_change": new_tier != old_tier
        }

    def handle_tier_change(self, prospect_id, old_tier, new_tier):
        if new_tier == "hot" and old_tier != "hot":
            # Alert rep
            notify_rep(prospect_id, "Lead became hot")
            # Accelerate sequence
            accelerate_outreach(prospect_id)

        elif new_tier == "cold" and old_tier != "cold":
            # Slow down outreach
            decelerate_outreach(prospect_id)
Conversation Score Updates
def update_score_from_message(conversation_id, message):
    """Update score based on conversation content"""

    prospect_id = get_prospect_id(conversation_id)
    scorer = get_scorer(prospect_id)

    # Analyze message for signals
    signals = analyze_message_signals(message)

    for signal in signals:
        event = ConversationEvent(
            type=signal.type,
            category="conversation",
            value=signal.value,
            context=signal.context
        )
        scorer.update_on_event(event)

    return scorer.get_current_score()

def analyze_message_signals(message):
    signals = []

    # Buying signals
    buying_patterns = [
        (r"what('s| is) the (price|cost|pricing)", "pricing_question", 20),
        (r"when can we (start|begin|implement)", "timeline_question", 25),
        (r"(we need|we're looking for)", "stated_need", 15),
        (r"who else (uses|is using)", "social_proof_request", 10),
        (r"(budget|allocated|set aside)", "budget_mention", 30)
    ]

    for pattern, signal_type, value in buying_patterns:
        if re.search(pattern, message.lower()):
            signals.append(Signal(type=signal_type, value=value))

    # Negative signals
    negative_patterns = [
        (r"not interested", "not_interested", -30),
        (r"stop (emailing|contacting)", "opt_out_request", -50),
        (r"too expensive", "price_objection", -5),
        (r"maybe (later|next year)", "timing_objection", -10)
    ]

    for pattern, signal_type, value in negative_patterns:
        if re.search(pattern, message.lower()):
            signals.append(Signal(type=signal_type, value=value))

    return signals

Score Decay

Time-Based Decay
def calculate_decay(last_activity, base_decay_rate=0.02):
    """Score decays over time without activity"""

    days_since_activity = (now() - last_activity).days

    # No decay for recent activity
    if days_since_activity < 7:
        return 0

    # Gradual decay
    decay = (days_since_activity - 7) * base_decay_rate

    # Cap decay
    return min(decay, 0.5)  # Max 50% decay
Engagement Decay
def decay_engagement_score(scorer):
    """Recent engagement matters more"""

    events = scorer.get_engagement_events()
    decayed_score = 0

    for event in events:
        age_days = (now() - event.timestamp).days

        if age_days < 7:
            weight = 1.0
        elif age_days < 30:
            weight = 0.7
        elif age_days < 90:
            weight = 0.4
        else:
            weight = 0.1

        decayed_score += event.score_delta * weight

    return decayed_score

Score-Based Actions

Routing by Score
def route_prospect(prospect):
    score = prospect.current_score
    tier = prospect.score_tier

    if tier == "hot":
        return {
            "queue": "immediate_follow_up",
            "assignee": "senior_ae",
            "urgency": "high",
            "action": "call_within_1_hour"
        }
    elif tier == "warm":
        return {
            "queue": "standard_follow_up",
            "assignee": "sdr",
            "urgency": "medium",
            "action": "email_within_24_hours"
        }
    elif tier == "cool":
        return {
            "queue": "nurture_sequence",
            "assignee": "bot",
            "urgency": "low",
            "action": "automated_nurture"
        }
    else:  # cold
        return {
            "queue": "re_engagement",
            "assignee": "bot",
            "urgency": "lowest",
            "action": "monthly_check_in"
        }
Dynamic Sequence Adjustment
def adjust_sequence_for_score(prospect, sequence):
    score = prospect.current_score

    if score >= 100:
        # High intent - accelerate
        return modify_sequence(sequence,
            interval_multiplier=0.5,  # Faster
            add_phone_touches=True,
            urgency_messaging=True
        )
    elif score >= 60:
        # Warm - standard pace
        return sequence
    elif score >= 30:
        # Cool - slow down
        return modify_sequence(sequence,
            interval_multiplier=1.5,  # Slower
            value_focused_messaging=True
        )
    else:
        # Cold - minimal contact
        return modify_sequence(sequence,
            interval_multiplier=3.0,  # Much slower
            re_engagement_messaging=True
        )

Visualization & Reporting

Score Timeline
def get_score_timeline(prospect_id, days=30):
    """Visualize score changes over time"""

    events = get_score_events(prospect_id, days=days)

    timeline = []
    running_score = get_initial_score(prospect_id, days)

    for event in events:
        running_score += event.delta
        timeline.append({
            "timestamp": event.timestamp,
            "score": running_score,
            "event": event.type,
            "delta": event.delta
        })

    return timeline

# Output for charting:
# [
#   {"timestamp": "2024-01-01", "score": 50, "event": "created", "delta": 50},
#   {"timestamp": "2024-01-05", "score": 65, "event": "email_opened", "delta": 15},
#   {"timestamp": "2024-01-06", "score": 90, "event": "pricing_question", "delta": 25},
#   ...
# ]
Score Distribution Report
def generate_score_report():
    all_prospects = get_all_active_prospects()

    return {
        "distribution": {
            "hot": len([p for p in all_prospects if p.tier == "hot"]),
            "warm": len([p for p in all_prospects if p.tier == "warm"]),
            "cool": len([p for p in all_prospects if p.tier == "cool"]),
            "cold": len([p for p in all_prospects if p.tier == "cold"])
        },
        "average_score": mean([p.score for p in all_prospects]),
        "score_changes_today": count_tier_changes(today()),
        "top_movers": get_biggest_score_increases(limit=10),
        "at_risk": get_biggest_score_decreases(limit=10)
    }

Model Calibration

Score Validation
def validate_score_model():
    """Check if scores predict outcomes"""

    # Get closed deals
    won = get_closed_won_last_90_days()
    lost = get_closed_lost_last_90_days()

    # Analyze scores at various stages
    won_scores_at_qualification = [d.score_at_stage("qualified") for d in won]
    lost_scores_at_qualification = [d.score_at_stage("qualified") for d in lost]

    # Scores should differentiate winners from losers
    avg_won = mean(won_scores_at_qualification)
    avg_lost = mean(lost_scores_at_qualification)

    if avg_won <= avg_lost:
        alert("Score model not predictive - won deals don't score higher")

    # Check tier conversion rates
    for tier in ["hot", "warm", "cool", "cold"]:
        conversion = conversion_rate_by_tier(tier)
        expected = EXPECTED_CONVERSION_BY_TIER[tier]
        if abs(conversion - expected) > 0.1:
            alert(f"Tier {tier} conversion {conversion} differs from expected {expected}")

Comments

Markdown supported