A five-component model. Per-ICP weights. Hard disqualifiers. The fit score is the gate that decides whether a lead is worth real research.
lead.webAudit — channels, traffic, ecommerce maturity, content recencylead.socialResearch — follower counts, posting cadencelead.companySize — employees, revenue tier where knownicp.weights — per-component weights (sums to 1.0)icp.disqualifiers[] — hard rules (e.g. is_agency)lead.fitScore — 0–100 weighted compositelead.scoreBreakdown — per-component subscoreslead.disqualifiers[] — names of fired hard-ruleslead.qualificationTier — A/B/C tier for sortinglead.qualifiedAt — timestamp + prompt_id back-linkdevtools · editable per packEach lead receives a subscore on five components. Digital maturity measures whether the lead has the website, channel presence, and content cadence to recognize the product. Channel dependency measures whether their distribution is concentrated enough that the pitch lands. Product fit measures alignment between observed pains and your solution templates. Company size measures whether the deal is worth the touch. Accessibility measures whether you can actually reach a decision maker.
Weights are per-pack, not per-product. The devtools pack weights infrastructure complexity heavily because the pitch only works if they're already running multi-cluster Kubernetes. The clinical pack weights accessibility higher because the buyer is often a clinic admin who answers their own email. You edit the weights once when you install or fork a pack, and the score model recomputes on every qualification run.
Hard disqualifiers run before scoring and override it. If a lead matches is_agency or below_min_traffic or on_global_blocklist, the score is calculated for diagnostics but the lead is tagged disqualified and never enters the Pitch Brief stage. That's the gate that keeps your enrichment bill, your LLM bill, and your reviewer time pointed at leads that can convert.
--- system --- You are a B2B fit-scoring assistant. Score the lead on five components, each 0..100. NEVER guess a subscore if the required input is missing — return null and add the field name to missing_required. --- inputs --- lead: domain: {{lead.domain}} webAudit: {{lead.webAudit | json}} socialResearch:{{lead.socialResearch | json}} companySize: {{lead.companySize | json}} icp: id: {{icp.id}} # devtools weights: {{icp.weights | json}} disqualifiers: {{icp.disqualifiers | json}} --- output schema --- { "subscores": { "digitalMaturity": number | null, "channelDependency": number | null, "productFit": number | null, "companySize": number | null, "accessibility": number | null }, "disqualifiers_fired": string[] # names from icp.disqualifiers only, "missing_required": string[], "rationale": string } --- guards --- - composite = sum(subscore * weight) only if all subscores present. Otherwise composite = null and lead.qualificationTier = "pending". - If any disqualifier fires, lead.qualificationTier = "rejected" regardless of composite. # <!-- PLACEHOLDER — full prompt registry available in app -->
A model trained on one ICP overscores leads in a different one.
Per-ICP weight vault. Each ICP carries its own weights, disqualifiers, and threshold; nothing is shared by accident.
Web audit hasn't completed yet, so the model fills the gap with optimism.
Composite is null if any subscore is null. The lead stays in pending until the audit finishes.
Composite is high enough that an obviously-bad lead slips into outreach.
Hard disqualifiers always override composite. is_agency + score 95 = rejected, full stop.
Drop a CSV. Watch the five components compute live in the sandbox.