# 🎯 Calibrated Lead Scorer - B2B Scores That Actually Spread (`that_red_bird/calibrated-lead-scorer`) Actor

🎯 Score B2B leads with a weighted, fully configurable rubric, no LLM calls. ✅ Fixes the classic AI-scorer failure mode where every lead lands at a confident-sounding 70-80: a percentile-rank calibration pass stretches the batch across the full 0-100 range.

- **URL**: https://apify.com/that\_red\_bird/calibrated-lead-scorer.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Calibrated Lead Scorer

Score B2B leads for sales prioritisation with a transparent, fully configurable rubric —
**no LLM calls, no network requests, nothing non-deterministic.** Run it twice on the same input
and you get byte-identical scores back.

### The problem this actor exists to fix

Point an LLM at a list of leads and ask it to score them 0-100 and you get back a wall of
confident-sounding reasoning and a distribution that bunches almost every lead at **70-80**. The
model is not wrong about any individual lead, but the *scores are useless for ranking* — if 40 of
your 50 leads are all "78", you have not prioritised anything. That bunching is not a prompting
problem; it is what happens when a system produces a plausible-sounding absolute judgement instead
of a genuinely comparative one.

This actor separates the two problems on purpose:

1. **RAW score (0-100)** — an explicit, weighted rubric over firmographics (employee count,
   revenue), fit signals (industry/country/job-title match against your ICP), contact quality
   (corporate vs. free email, has a website), and intent signals (tech stack, hiring, funding).
   Every weight is a plain integer you control. Each lead gets a full explanation: which features
   fired, what value was read from the input, and how many points (positive or negative, around a
   neutral 50 baseline) that feature contributed.
2. **CALIBRATION** — a percentile-rank transform applied across the whole batch. Whatever spread
   genuinely exists in the raw scores gets stretched across the full 0-100 range: the
   highest-raw-scoring lead in the batch becomes 100, the lowest becomes 0, and everything else is
   placed by rank, with ties sharing the average rank of their group. The output `calibratedScore`
   is the number to sort and tier by; `rawScore` is kept alongside it so you can see what the
   rubric actually thought before ranking.

### Input

Pass `leads` inline and/or `sourceDatasetIds`. All feature weights, ICP fields and tier cutoffs
have sensible defaults — the minimum viable run is just `{ "leads": [...] }`.

```json
{
  "leads": [{ "employeeCount": 220, "industry": "SaaS", "country": "United States", "email": "vp@acme.com", "jobTitle": "VP of Sales", "isHiring": true }],
  "icpIndustries": ["SaaS", "Industrial Automation"],
  "icpCountries": ["United States", "Canada"],
  "tierACutoff": 90, "tierBCutoff": 70, "tierCCutoff": 40
}
```

### Output

One row per lead (`type: "lead"`) with `tier` (A/B/C/D), `calibratedScore`, `rawScore`,
`featuresUsed`, and — unless disabled — an `explanation` array of
`{ feature, label, value, points, reason }`. Malformed input entries (null, arrays, non-objects)
are never silently dropped: skip them from scoring and, when `includeSkipped` is true, emit a
`type: "skipped"` row with the reason. The key-value store's `SUMMARY` reports the raw and
calibrated score distributions (min/max/mean/stdev) so you can sanity-check the spread yourself.

### Honest limitations — read before you trust the ranking

- **Calibration is relative, not absolute.** A `calibratedScore` of 80 means "ranks better than
  80% of this batch", not "this lead is worth 80 points of pipeline". Run the same lead in a batch
  of much stronger leads and its calibrated score will drop, even though nothing about the lead
  itself changed.
- **Needs a reasonably sized, varied batch.** With fewer than ~10-20 leads, or a batch where every
  lead is genuinely similar, there is little real spread to calibrate — the transform cannot
  invent variance that is not present in the raw scores, and with a single lead it passes the raw
  score through unchanged.
- **Garbage in, garbage out on feature data.** A lead missing every optional field (no industry,
  no email, no job title) simply has fewer features contribute to its raw score and lands near the
  neutral 50 baseline — this is not the same as "this is a bad lead", just "this lead has thin
  data".
- **No semantic understanding.** There is no LLM here. Industry/country/job-title matching against
  your ICP is exact-or-fuzzy string similarity (shared with `entity-resolver`'s matching engine),
  not an understanding of what the company actually does. "FinTech" and "Financial Technology"
  will match well; a genuinely novel industry description that shares no tokens with your ICP list
  will not, even if a human would recognise the fit instantly.
- **The rubric encodes assumptions, not universal truth.** Default weights favour larger
  companies, senior titles and corporate email — reasonable defaults for a typical enterprise B2B
  motion, wrong for a PLG/SMB motion. Every weight is configurable; the defaults are a starting
  point, not a verdict.

# Actor input Schema

## `leads` (type: `array`):

The leads to score, as an array of flat objects. Combine freely with sourceDatasetIds. Recognised fields (all optional): employeeCount, revenue, industry, country, website, email, jobTitle, techStack, isHiring, recentFunding/fundingStage.

## `sourceDatasetIds` (type: `array`):

Apify dataset IDs to pull additional leads from. Rows are appended to "leads" before scoring.

## `weightCompanySize` (type: `integer`):

How much employee-count fit against the ICP size range (or a sensible default of 10-1000) contributes to the raw score. Set to 0 to disable this feature entirely.

## `weightRevenue` (type: `integer`):

How much reported annual revenue (log-scaled, saturating around $100M+) contributes to the raw score.

## `weightIndustryMatch` (type: `integer`):

How much a fuzzy match between the lead's industry and icpIndustries contributes. Has no effect unless icpIndustries is set.

## `weightCountryMatch` (type: `integer`):

How much a fuzzy match between the lead's country and icpCountries contributes. Has no effect unless icpCountries is set.

## `weightJobTitleMatch` (type: `integer`):

How much a fuzzy match between the lead's job title and icpJobTitles contributes. Has no effect unless icpJobTitles is set. Separate from weightSeniority below.

## `weightSeniority` (type: `integer`):

How much the parsed seniority tier of the job title (C-level / VP / Director / Manager / individual contributor) contributes to the raw score.

## `weightCorporateEmail` (type: `integer`):

How much having a corporate (non-free) email domain contributes, with extra credit when the email domain matches the lead's own website domain.

## `weightHasWebsite` (type: `integer`):

How much having a usable website/domain field contributes. Unlike most features this is scored even when absent (a missing website is treated as a real negative signal, not missing data).

## `weightTechMaturity` (type: `integer`):

How much the size and B2B-relevance of the lead's techStack/technologies list contributes.

## `weightHiringSignal` (type: `integer`):

How much an isHiring/hiring boolean field contributes.

## `weightFundingSignal` (type: `integer`):

How much a recentFunding boolean, or a non-empty fundingStage field, contributes.

## `icpIndustries` (type: `array`):

Ideal Customer Profile industries, e.g. \["SaaS", "Industrial Automation"]. Leads with a matching (fuzzy or exact) industry get a boosted raw score before calibration. Leave empty to skip this feature.

## `icpCountries` (type: `array`):

Ideal Customer Profile countries, e.g. \["United States", "Canada"]. Leave empty to skip this feature.

## `icpJobTitles` (type: `array`):

Ideal Customer Profile job titles, e.g. \["VP of Engineering", "Head of Sales"]. Matched by fuzzy string similarity, separately from the seniority-tier feature. Leave empty to skip this feature.

## `icpMinEmployees` (type: `integer`):

Lower bound of the target company size range. Leave at 0 together with icpMaxEmployees to fall back to a generic 10-1000 default range.

## `icpMaxEmployees` (type: `integer`):

Upper bound of the target company size range. Set to 0 with icpMinEmployees > 0 to mean "no upper bound".

## `tierACutoff` (type: `integer`):

Leads with a calibrated score at or above this get tier "A". Must be the highest of the three cutoffs; malformed configs are clamped rather than rejected.

## `tierBCutoff` (type: `integer`):

Leads with a calibrated score at or above this (and below the tier A cutoff) get tier "B".

## `tierCCutoff` (type: `integer`):

Leads with a calibrated score at or above this (and below the tier B cutoff) get tier "C". Everything below this is tier "D".

## `includeExplanations` (type: `boolean`):

Attach the feature-by-feature point breakdown (feature name, value, points +/-, reason) to every scored lead.

## `includeSkipped` (type: `boolean`):

Emit a row (type: "skipped") for every input entry that was not a scoreable flat object, e.g. null or an array, instead of silently dropping it.

## Actor input object example

```json
{
  "leads": [
    {
      "companyName": "Northgate Robotics",
      "employeeCount": 220,
      "revenue": 18000000,
      "industry": "Industrial Automation",
      "country": "United States",
      "website": "northgaterobotics.com",
      "email": "j.rivera@northgaterobotics.com",
      "jobTitle": "VP of Engineering",
      "techStack": [
        "Salesforce",
        "AWS",
        "Jira"
      ],
      "isHiring": true,
      "recentFunding": true
    },
    {
      "companyName": "Cassie's Cupcakes",
      "employeeCount": 3,
      "revenue": 90000,
      "industry": "Food & Beverage",
      "country": "United States",
      "email": "cassie.baker@gmail.com",
      "jobTitle": "Owner",
      "isHiring": false
    }
  ],
  "weightCompanySize": 12,
  "weightRevenue": 8,
  "weightIndustryMatch": 15,
  "weightCountryMatch": 8,
  "weightJobTitleMatch": 10,
  "weightSeniority": 15,
  "weightCorporateEmail": 10,
  "weightHasWebsite": 6,
  "weightTechMaturity": 8,
  "weightHiringSignal": 8,
  "weightFundingSignal": 8,
  "icpMinEmployees": 0,
  "icpMaxEmployees": 0,
  "tierACutoff": 90,
  "tierBCutoff": 70,
  "tierCCutoff": 40,
  "includeExplanations": true,
  "includeSkipped": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `downloadCsv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `count` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "leads": [
        {
            "companyName": "Northgate Robotics",
            "employeeCount": 220,
            "revenue": 18000000,
            "industry": "Industrial Automation",
            "country": "United States",
            "website": "northgaterobotics.com",
            "email": "j.rivera@northgaterobotics.com",
            "jobTitle": "VP of Engineering",
            "techStack": [
                "Salesforce",
                "AWS",
                "Jira"
            ],
            "isHiring": true,
            "recentFunding": true
        },
        {
            "companyName": "Cassie's Cupcakes",
            "employeeCount": 3,
            "revenue": 90000,
            "industry": "Food & Beverage",
            "country": "United States",
            "email": "cassie.baker@gmail.com",
            "jobTitle": "Owner",
            "isHiring": false
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/calibrated-lead-scorer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "leads": [
        {
            "companyName": "Northgate Robotics",
            "employeeCount": 220,
            "revenue": 18000000,
            "industry": "Industrial Automation",
            "country": "United States",
            "website": "northgaterobotics.com",
            "email": "j.rivera@northgaterobotics.com",
            "jobTitle": "VP of Engineering",
            "techStack": [
                "Salesforce",
                "AWS",
                "Jira",
            ],
            "isHiring": True,
            "recentFunding": True,
        },
        {
            "companyName": "Cassie's Cupcakes",
            "employeeCount": 3,
            "revenue": 90000,
            "industry": "Food & Beverage",
            "country": "United States",
            "email": "cassie.baker@gmail.com",
            "jobTitle": "Owner",
            "isHiring": False,
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/calibrated-lead-scorer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "leads": [
    {
      "companyName": "Northgate Robotics",
      "employeeCount": 220,
      "revenue": 18000000,
      "industry": "Industrial Automation",
      "country": "United States",
      "website": "northgaterobotics.com",
      "email": "j.rivera@northgaterobotics.com",
      "jobTitle": "VP of Engineering",
      "techStack": [
        "Salesforce",
        "AWS",
        "Jira"
      ],
      "isHiring": true,
      "recentFunding": true
    },
    {
      "companyName": "Cassie'\''s Cupcakes",
      "employeeCount": 3,
      "revenue": 90000,
      "industry": "Food & Beverage",
      "country": "United States",
      "email": "cassie.baker@gmail.com",
      "jobTitle": "Owner",
      "isHiring": false
    }
  ]
}' |
apify call that_red_bird/calibrated-lead-scorer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/calibrated-lead-scorer"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/powx7ZmpWcZp0otzo/builds/xROSRedQUcFzGfB5N/openapi.json
