# Recipe: score inbound leads

> Pick what the sender wants, rate how well the company fits your ideal customer and check whether they ask for a call, then fill the CRM fields and book the call.

Every inbound form gets an intent, a fit score and a meeting flag as it arrives, and sales starts from filled CRM fields. On the same model version the same lead always gets the same score, so the ranking holds from one day to the next.

## The request

- **State.** The form as JSON and the page it came from. `intent` and `wants_meeting` point at `{{form.message}}`, while `fit` points at the whole `{{form}}` object so it reads the company size and country too. Leave out what the decision does not need, such as the email address.
- **`intent` is a `pick`** of five reasons to write. They are mutually exclusive, so one pick is better than five checks, and `spam` is the way out.
- **`fit` is a `rate`** in beta on four levels. The `criteria` define your ideal customer profile in one sentence: an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month. Change the sentence and the scale follows.
- **`wants_meeting` is a `check`**: one yes or no fact about the message.
- **`min_confidence`** on every question, so an unsure answer leaves its CRM field empty instead of guessing.

```json
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}
```

## Run it

Put a test key in `DEX_API_KEY` (see the [Quickstart](/docs/quickstart/#get-a-key-in-60-seconds)). Save the Python code as `lead_scoring.py` and run `python lead_scoring.py`. Save the TypeScript code as `lead-scoring.mts` and run `npx tsx lead-scoring.mts`: the code uses `await` at the top level, and the `.mts` ending makes the file an ES module. Install the SDKs from [Downloads](/docs/reference/sdks/#downloads).

```bash tab="curl"
curl https://api.thinqit.ai/v1/decide \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data-binary @- <<'DEX_REQUEST'
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}
DEX_REQUEST
```

```python tab="Python"
# Save as dex_request.py, then run: python dex_request.py
import json

from thinqit_dex import Client

client = Client()  # reads DEX_API_KEY from the environment

request = json.loads(r'''
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}
''')

decision = client.decide(
    request["state"],
    request["questions"],
)
for question_id, answer in decision.answers.items():
    print(question_id, answer)
```

```ts tab="TypeScript"
// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";

const client = new Client(); // reads DEX_API_KEY from the environment

// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}`);

const decision = await client.decide(request);
console.log(decision.answers);
```

## Expected output

```json
{
  "id": "req_01M3JE1VPZ9WWB18Z4F27NS6C9",
  "object": "decision",
  "created": 1790546341,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "intent": {
      "type": "pick",
      "choice": "buy",
      "probabilities": {
        "buy": 0.9433,
        "partner": 0.0159,
        "support": 0.0165,
        "job": 0.0116,
        "spam": 0.0127
      },
      "confidence": 0.9268,
      "abstained": false
    },
    "fit": {
      "type": "rate",
      "rating": 2.3044,
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "probabilities": [0.0127, 0.0362, 0.585, 0.3661],
      "confidence": 0.5997,
      "abstained": false
    },
    "wants_meeting": {
      "type": "check",
      "probability": 0.9458,
      "confidence": 0.8915,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 216,
    "state_tokens": 87,
    "question_tokens": 129,
    "allowance_tokens": 0,
    "paid_tokens": 0,
    "charge_micro_cents": 0,
    "unit_price_micro_cents": 0,
    "tier": "test"
  }
}
```

Captured from the live API on 2026-09-27 with a test key: model `dex-1.0.1`, calibration `cal-20260926-1`, `served_by: gpu`, 216 input tokens (87 for the state, 129 for the questions). A test key is charged nothing, so `tier` is `test` and the charge is 0. On this exact version the same request always returns these answers.

| Question | Type | Answer | Confidence | `min_confidence` | Abstained |
| --- | --- | --- | --- | --- | --- |
| `intent` | pick | `buy` (0.9433) | 0.9268 | 0.5 | no |
| `fit` | rate | rating 2.3044, most likely `Good` (0.585) | 0.5997 | 0.3 | no |
| `wants_meeting` | check | yes with probability 0.9458 | 0.8915 | 0.6 | no |

The sender wants to buy (0.9433) and asks for a call (0.9458). The fit rating of 2.3044 is closest to `Good` (0.585), with `Excellent` second at 0.3661. No answer abstained, so every CRM field gets a value.

## Act on it

`lead_id` and the functions `update_crm` and `book_call` stand for your own code. Each typed answer fills one CRM field: the pick's label, the nearest level of the rating (2.3044 rounds to 2, which is `Good`) and a yes or no for the meeting. Here all three are filled, and the lead wants to buy, fits well and asks for a call, so the code books it.

```python tab="Python"
crm = {}
intent = decision.pick("intent")
if not intent.abstained:
    crm["intent"] = intent.choice

fit = decision.rate("fit")
if not fit.abstained:
    crm["fit"] = fit.levels[round(fit.rating)]  # the nearest level

meeting = decision.check("wants_meeting")
if not meeting.abstained:
    crm["wants_meeting"] = meeting.probability >= 0.5

update_crm(lead_id, crm)  # an abstained answer leaves its field empty for sales
if crm.get("intent") == "buy" and crm.get("fit") in ("Good", "Excellent") and crm.get("wants_meeting"):
    book_call(lead_id)
```

```ts tab="TypeScript"
const { intent, fit, wants_meeting: meeting } = decision.answers;
const crm: Record<string, string | boolean> = {};

if (intent?.type === "pick" && !intent.abstained) crm.intent = intent.choice;
if (fit?.type === "rate" && !fit.abstained) crm.fit = fit.levels[Math.round(fit.rating)]; // the nearest level
if (meeting?.type === "check" && !meeting.abstained) crm.wants_meeting = meeting.probability >= 0.5;

updateCrm(leadId, crm); // an abstained answer leaves its field empty for sales
if (crm.intent === "buy" && (crm.fit === "Good" || crm.fit === "Excellent") && crm.wants_meeting === true) bookCall(leadId);
```

As in the [support recipe](/docs/cookbook/support-routing/#act-on-it), the TypeScript answers have the general `Answer` type, so the code narrows each one on `type`.

## Adapt it

- **Your profile, in one sentence.** Put what makes a good customer in `criteria`, in English, as facts a form can show: country, size, volume. Dex reads the form; it does not look the company up. When you have data from your own sources, pass it as state fields.
- **Rank, not only label.** Store `fit.rating` as a number from 0 to 3 next to the level name, and sort the sales queue on it.
- **Forms are written by strangers.** Text in the state can try to steer the answer. Add a guardrail check as in the [agent tool gate recipe](/docs/cookbook/agent-guardrails/) before a score triggers anything you cannot undo.
- **Tune on leads you already know.** Run past forms whose outcome you know with a test key, and move the floors and the booking rule until the booked calls are the ones sales wants. See [Abstention](/docs/concepts/abstention/#choosing-a-threshold).
