An AI engine that reads every inbound lead the moment it lands, scores it on BANT (Budget · Authority · Need · Timeline), and routes it — turning a 42-hour, manual qualification process into one that responds to the best buyers in minutes.
This repository is a self-contained, end-to-end demonstration of an AI lead-scoring system: synthetic data → an AI scoring engine → a routing layer → an interactive executive dashboard. It runs offline for free (deterministic rules pass) or against the Claude API for real model-driven scoring.
It models a fictional company, Cadence Workflow — a ~120-person Series B B2B SaaS selling project-management software per-seat — where 3 SDRs and 4 AEs can’t reach 400 monthly leads in time, so the best buyers go cold before anyone calls.
▶ Live demos — once GitHub Pages is enabled (Settings → Pages → Deploy from a branch → main / /docs):
| Demo | Link |
|---|---|
| 🛠️ Velocity Console — the interactive working tool (queue, routing, live scorer) | https://aceymagallanes.github.io/velocity-engine/console.html · how to use |
| 📊 Executive dashboard — the narrative walkthrough | https://aceymagallanes.github.io/velocity-engine/ |
Inbound qualification is usually manual and outnumbered: leads arrive around the clock, the team works business hours, and the highest-intent buyers wait in a queue. Velocity removes the wait.
The point the data makes is uncomfortable and deliberate: under the as-is process, 16 of 64 high-priority leads — buyers with budget, authority, and a deadline — were never contacted at all.
Modeled on the synthetic dataset in this repo (400 leads, calibrated to a realistic broken baseline):
| Metric | As-is (manual) | Velocity (routed) | Delta |
|---|---|---|---|
| Average first response | ~42 hours | ~13 hours | ~3× faster |
| Response to high-priority leads | ~28 hours | 5 minutes | ~330× faster |
| Leads never contacted | 28% | 0% (all routed) | −28 pts |
| Priority tiers (emerged from messages, not labeled) | — | 16% High · 32% Medium · 52% Low | — |
The “to-be” SLAs are conservative routing targets (High 5 min, Medium 60 min, Low 24 h). The “as-is” baseline is calibrated to a ~2,520-minute (≈42 h) mean with a long tail and ~28% never contacted — see Data dictionary.
Beyond the narrative dashboard, the repo ships a functional tool: docs/console.html — a live lead-triage workbench that runs entirely in the browser (no backend, no login).
src/score_leads.py to run client-side — scores and tiers it in real time. This is the proof the engine genuinely reads and reasons over the text, not a canned result.Try it live: https://aceymagallanes.github.io/velocity-engine/console.html
The system is a small, transparent pipeline. The engine in the middle is reusable; the edges are yours — your channels flow in, your tools take the action out.
flowchart LR
subgraph SRC["Lead sources — your systems"]
A1[Web forms]
A2[CRM / inbox]
A3[Ads · chat]
end
SRC --> ING["Ingest API<br/>webhook / REST"]
ING --> NRM["Normalize<br/>→ one lead schema"]
NRM --> COREVelocity Core<br/>Claude · BANT scoring
CORE --> RT["Route<br/>tier · SLA"]
RT --> ACT["Act — your systems<br/>CRM · Slack · email · calendar"]
ACT -. "won / lost outcomes" .-> CORE
CORE --> DB[("JSON data layer")]
classDef engine fill:#0D6B4F,stroke:#094D3A,color:#fff;
classDef yours fill:#475569,stroke:#0A1D37,color:#fff;
class ING,NRM,CORE,RT,DB engine;
class A1,A2,A3,ACT yours;
Stages
| Stage | Owner | What it does |
|---|---|---|
| Lead sources | You | Every inbound channel — forms, CRM, shared inbox, ads, demo bookings, chat |
| Ingest API | Engine | Captures each new lead via webhook / REST the moment it’s created |
| Normalize | Engine | Maps messy inputs to one schema: {source, role, company_size, region, message, timestamp} |
| Velocity Core | Engine | Claude reads the message and returns a structured BANT score + reason |
| Route | Engine | Tier → action and response-time SLA |
| Act | You | CRM update, Slack/Teams alert, email/SMS, calendar booking (API / MCP) |
| Data layer | Engine | Plain JSON state; outcomes feed back to refine the rubric |
The data layer is intentionally just JSON (data/leads.json → data/leads_scored.json) so the system drops into any stack without a migration.
Each lead takes the same path: assemble a request with a fixed BANT rubric and a strict output schema, send it to Claude, and get back a guaranteed-valid structured object that’s tiered and routed.
flowchart TD
L["Lead message + context"] --> P["Assemble request<br/>system: BANT rubric (cached)<br/>output: JSON Schema (structured)"]
P --> C["Claude Opus 4.8<br/>structured output"]
C --> S["BANT scores 0–3 each<br/>+ one-line reason"]
S --> T{"Total / 12"}
T -->|"≥ 8"| H["HIGH → AE in 5 min"]
T -->|"3–7"| M["MEDIUM → SDR in 60 min"]
T -->|"0–2"| Lo["LOW → nurture in 24 h"]
classDef hi fill:#0D6B4F,stroke:#094D3A,color:#fff;
classDef md fill:#D4AF37,stroke:#9a7d1e,color:#0A1D37;
classDef lo fill:#9FB0AE,stroke:#475569,color:#0A1D37;
class H hi; class M md; class Lo lo;
Request to Claude (the system prompt holds a fixed rubric and is prompt-cached; the user message is the lead’s own words):
system: BANT rubric (fixed) — how to score Budget/Authority/Need/Timeline 0–3
input: lead.stated_need + {source, role, company_size, region}
output: JSON Schema (structured outputs) → guaranteed valid object
Structured response (validated against a Pydantic model / JSON Schema):
{
"budget": 3,
"authority": 3,
"need": 3,
"timeline": 3,
"reason": "Approved budget for 120 seats, Q3 deadline, VP-level buyer."
}
→ total 12/12 → HIGH → route to an AE in 5 minutes
src/score_leads.py) mirrors the same BANT scoring with keyword heuristics — perfect for offline demos, CI tests, and zero-cost runs. The AI pass (src/score_leads_ai.py) is a drop-in upgrade producing the same schema.flowchart LR
G["generate_leads.py<br/>synthetic dataset"] --> J[("leads.json")]
J --> R["score_leads.py<br/>rules pass (free, offline)"]
J --> AI["score_leads_ai.py<br/>Claude API pass"]
R --> JS[("leads_scored.json")]
AI --> JS
JS --> B["build_dashboard.py"]
B --> H[["docs/index.html<br/>interactive dashboard"]]
Both scorers write the same leads_scored.json shape, so the dashboard renders either one unchanged. Run the rules pass to see it instantly for free, then swap in the AI pass for real model scoring (the dashboard even shows the model’s per-lead reasoning).
| Layer | Technology |
|---|---|
| Core service | Python 3 (standard library for the rules pass — no deps required) |
| AI scoring | Anthropic Claude API · Claude Opus 4.8 (quality) / Claude Haiku 4.5 (cost mode) |
| Output contract | Structured Outputs — Pydantic models + JSON Schema |
| Throughput | Concurrent scoring (ThreadPoolExecutor), prompt caching of the rubric |
| Integration (reference) | n8n / webhooks in; CRM, Slack, email, calendar out via API / MCP |
| Data layer | Plain JSON (leads.json → leads_scored.json) |
| Presentation | Single self-contained HTML dashboard — inline SVG/CSS/JS, zero external dependencies, brand-themed |
velocity-engine/
├── README.md
├── LICENSE
├── requirements.txt # only needed for the Claude API pass
├── .gitignore
├── src/
│ ├── generate_leads.py # synthetic, calibrated 400-lead dataset
│ ├── score_leads.py # RULES pass — deterministic BANT, free & offline
│ ├── score_leads_ai.py # AI pass — Claude API + structured outputs
│ ├── build_dashboard.py # renders the executive dashboard
│ └── build_console.py # renders the working Console + how-to guide
├── data/
│ ├── leads.json # raw synthetic leads
│ └── leads_scored.json # leads + BANT scores + tiers + SLAs
└── docs/ # GitHub Pages-ready
├── index.html # executive dashboard (the narrative)
├── console.html # Velocity Console (the working tool)
└── guide.html # how to use the console
All commands run from the repository root.
python src/generate_leads.py # writes data/leads.json
python src/score_leads.py # writes data/leads_scored.json
python src/build_dashboard.py # writes docs/index.html (executive dashboard)
python src/build_console.py # writes docs/console.html + docs/guide.html (working tool)
# open them
open docs/console.html # the working Console (macOS; 'start' on Windows, 'xdg-open' on Linux)
open docs/index.html # the executive dashboard
pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-...
python src/score_leads_ai.py --limit 5 # cheap smoke test first (~free)
python src/score_leads_ai.py # score all 400 (~$0.90 Opus / ~$0.18 Haiku)
python src/build_dashboard.py # rebuild the dashboard with AI scores
open docs/index.html
Useful flags on the AI pass: --model claude-haiku-4-5 (cheaper), --limit N (test subset), --concurrency N.
A single, self-contained docs/index.html (no build step, no CDN — renders offline). It’s a scrollytelling executive narrative:
Host it free: push this repo, then enable GitHub Pages → Deploy from branch → /docs.
data/leads.json — array of 400 lead objects under { "_meta": {...}, "leads": [...] }.
| Field | Type | Description |
|---|---|---|
lead_id |
string | L0001–L0400 |
created_at |
string | ISO 8601 timestamp, spread across one calendar month (incl. evenings/weekends) |
source |
enum | demo_request · free_trial · content_webinar · paid_ad · referral |
company_size_band |
enum | solo · small · mid · large · enterprise |
contact_role |
enum | junior · manager · director · vp · c_level · owner |
region |
string | Country / region |
stated_need |
string | The lead’s own 1–2 sentence message (the signal that gets scored) |
as_is_first_response_minutes |
int | null | Minutes to first reply under the manual process; null if never contacted |
as_is_contacted |
bool | Whether the lead was ever contacted |
After scoring, leads_scored.json adds: bant {budget, authority, need, timeline}, bant_total (0–12), priority (high/medium/low), to_be_first_response_minutes, and — on the AI pass — bant_reason.
Calibration targets (so readers know the data is modeled, not real):
The engine is the reusable core; you bring the edges. Typical rollout is ~3 weeks with no rip-and-replace:
| Phase | What happens | You walk away with |
|---|---|---|
| Week 1 — Connect & map | Wire up channels (webhooks / native), map fields to the schema, import 3–6 months of history | Every lead in one clean stream |
| Week 2 — Calibrate | Define your BANT rubric in plain English; score your history; tune tiers against known won/lost | A model validated on your outcomes |
| Week 3 — Route & go live | Wire actions into CRM/Slack/calendar, set SLAs, shadow-run, then switch on | Hot leads to the right rep in minutes |
| Ongoing | Weekly review; outcomes feed back to refine the rubric | Compounding accuracy, near-zero manual effort |
generate_leads.py uses a fixed random seed).Built by Acey Magallanes as part of AceLiora AI — an AI automation studio helping SMEs turn manual, leaky processes into fast, measurable, automated systems.
Accelerate Change. Sustain Excellence.
MIT © 2026 Acey Magallanes