This repository contains the current working prototype of the Seedance lead generation engine: an LLM-powered qualification and prioritization pipeline designed to turn noisy outbound signals into a ranked, auditable lead list.
The long-term vision is bigger than lead scoring. The goal is to build a reusable "Claude brain" for outbound: a context-aware system that understands Seedance's ICP, product, sales messaging, target personas, buying signals, and outreach style, then helps SDRs and AEs decide who to target and how to engage them.
Today, the repo implements the first practical layer of that vision:
- ingest lead data from JSON or Trigify-style CSV exports
- normalize and lightly enrich each lead
- run a two-stage LLM qualification workflow
- apply deterministic Python routing
- output CSV queues plus JSON artifacts for review and audit
This document serves as both the current README and the working PRD for what has been built so far.
The business problem is straightforward: outbound agencies are not producing enough value, pipeline pressure is high, and manual lead review is slow, inconsistent, and hard to scale. Antti's direction is to build an internal AI-assisted outbound engine that improves lead quality fast, while laying the foundation for a broader sales operator over time.
The current system focuses on the shortest path to value:
- use existing lead exports and Trigify data
- enrich them with lightweight external context where possible
- have Claude extract structured evidence first, then score fit second
- route each lead into
qualified,review, ordisqualified - preserve artifacts so the team can debug false positives and improve the rubric quickly
The strategic direction is to move from prompt engineering to context engineering. Instead of writing one-off prompts, the system should reason from a centralized body of business context:
- product knowledge
- ICP definitions
- target personas
- use cases
- disqualification rules
- brand voice and sales messaging
- historical outreach examples
- market and vertical priorities
In the short term, success means producing a better ranked lead list than today's manual or agency-led process. In the long term, success means each AE starting the day with:
- a ranked account list
- context on why each account matters now
- draft outreach aligned with company messaging
- signals such as hiring, funding, leadership changes, and strategic initiatives
Build a Seedance "Claude brain" that acts as a context-aware outbound operator for lead qualification, account prioritization, and eventually personalized outreach support.
Current outbound work has three major problems:
- too much manual reading of weak signals
- inconsistent qualification quality across people and days
- no durable memory of why a lead was accepted or rejected
The system should replace ad hoc judgment with a repeatable, inspectable workflow.
This project is intended to evolve from a lead qualification engine into a broader sales operations assistant that can support:
- account research
- buying signal detection
- strategic initiative detection
- daily account prioritization
- outreach drafting
- repetitive AE and SDR workflows
Claude is expected to handle reasoning and personalization. Tools such as Clay or other enrichment systems can later supply structured firmographic, contact, and intent data.
- batch lead ingestion from
.jsonand.csv - Trigify-style CSV parsing into a normalized lead schema
- lead normalization and deduplication
- lightweight enrichment
- two-stage Claude reasoning pipeline
- Python-side score validation and routing
- CSV outputs for action queues
- per-lead JSON artifacts for auditability
- domain knowledge encoded in
skills/markdown files and prompt templates
- CRM write-back
- automated email or LinkedIn sending
- real-time ingestion or webhooks
- a dashboard or review UI
- learning from reviewer feedback automatically
- a full AE operator workflow
- production infrastructure for large-scale always-on outbound
This is a strong v1 prototype / pilot system, not yet a full production-grade outbound platform.
It already proves the architecture and gives the team something real to evaluate. It still needs data, workflow, integration, and reliability hardening before it should be treated as production-ready.
- Antti and SDR leadership, who define ICP, qualification logic, and success criteria
- SDRs, who need a better first-pass qualified lead queue
- AEs, who eventually need ranked accounts plus context and draft messaging
- Engineering / ops, who need a deterministic system that can be tuned safely
The implemented system looks like this:
Lead input (JSON or CSV)
↓
Normalization
↓
Enrichment
↓
Claude Stage 1: Evidence Extraction
↓
Claude Stage 2: Qualification Decision
↓
Python Decision Router
↓
CSV outputs + JSON artifacts
Claude is the reasoning engine. Python is the control plane.
Claude does not own workflow state, retries, routing, file output, or score integrity. Claude only returns structured JSON. Python orchestrates the pipeline and enforces deterministic behavior where possible.
lead_generation_engine/
├── main.py
├── config.py
├── requirements.txt
├── README.md
├── SOLUTION_DESIGN.md
├── PRD_REVIEW.md
├── normalization/
│ ├── csv_loader.py
│ └── normalizer.py
├── claude/
│ ├── prompts.py
│ ├── stage1_evidence.py
│ └── stage2_decision.py
├── tools/
│ ├── enrichment.py
│ └── web_enrichment.py
├── router/
│ └── decision_router.py
├── output/
│ ├── csv_writer.py
│ └── artifacts/
└── skills/
├── disqualification-rules/
├── lead-qualification/
├── seedance-icp/
└── trigify-signal-scoring/
The CLI accepts either:
- JSON arrays of lead objects
- Trigify-style CSV exports
The code auto-detects the file type from the extension.
The CSV loader maps rows into the internal lead format. Based on the current sample in 10 gen leads.csv, the engine expects columns such as:
NAMEJOB TITLECOMPANYINDUSTRYCOMPANY SIZEICP SCORECOMPANY URLLINKEDIN PROFILELOCATIONHQ COMPANY LOCATIONINTERACTIONPOST URL
Because the CSV does not contain one canonical signal body, signal_text is synthesized from available fields, especially:
- who the person is
- what interaction happened
- the Trigify qualification note
- company context such as industry, size, and location
The normalization layer:
- validates required fields using Pydantic
- strips and cleans text
- normalizes domains
- standardizes LinkedIn URLs
- normalizes titles
- deduplicates by
lead_id - skips invalid leads instead of crashing the full batch
The enrichment layer currently combines:
- stubbed company profile lookup
- stubbed recent signal lookup
- local role seniority heuristic
- best-effort website scraping
- best-effort LinkedIn profile scraping
Important caveat: the structured company and recent-signal enrichment functions are still stubs, but web enrichment is live and best-effort.
Current enrichment behavior:
get_company_profile()returns an empty structured objectget_recent_signals()returns an empty structured objectget_role_seniority()classifies titles intosenior,mid,junior, orunknownscrape_company_website()fetches and summarizes homepage contentscrape_linkedin_profile()tries to recover public profile text and tenure clues
Stage 1 does not score leads. It extracts structured evidence.
It returns:
segmentcandidate_use_casesignal_strengthobserved_evidenceinferred_evidenceunknownshard_disqualifiers
This separation is important. The engine explicitly distinguishes:
- observed facts
- inferred conclusions
- unknown data
That keeps the system more auditable and reduces the risk that confident inferences get treated like facts.
Stage 2 receives only the Stage 1 output, not the raw lead.
It returns:
decisionscoreconfidence- five sub-scores
- qualification and disqualification reasons
- reasoning summary
- suggested use case
Python then:
- forces
lead_idconsistency - clamps sub-scores to configured maxima
- re-sums the total score
- corrects the decision if it does not match thresholds
The router applies the final authoritative rules:
- hard disqualifiers always route to
disqualified signal_strength == "low"always routes todisqualified- otherwise threshold-based routing applies
score >= 70 -> qualified
60 <= score < 70 -> review
score < 60 -> disqualified
Each run writes:
output/qualified_leads.csvoutput/review_leads.csvoutput/disqualified_leads.csvoutput/run.log
Each processed lead also gets three JSON artifacts:
{lead_id}_input.json{lead_id}_evidence.json{lead_id}_decision.json
This is one of the most useful parts of the implementation because it creates an audit trail for review, debugging, and scoring calibration.
The current business logic is based on the Antti discussion and encoded in both prompts and skills/ reference files.
- film studios and film production companies
- short drama / short-form vertical video producers
- AI tools companies building with video generation
- video-focused creative agencies
- high-volume content creator businesses and influencer studios
- advertising teams with video at scale
- adjacent marketing teams where video production is a real bottleneck
Highest priority:
- Founder / Co-founder
- CEO / Managing Director
- Head of Production
- Head of Content
- VP / Director of Creative
- CTO / CPO for AI-video product companies
Strong champions / influencers:
- Creative Director
- Senior Producer
- Lead Editor
- AI / ML lead
- Senior Motion Designer
Low-value or disqualifying contacts:
- interns
- junior roles
- coordinators
- assistants
- analysts without real buying power
Antti's signal priority is reflected in the prompt design:
- posts are strongest
- comments are stronger than likes
- likes are still meaningful for the right ICP and persona
- low signal should only mean weak evidence plus poor fit, not merely a passive interaction
The system is designed to notice signals such as:
- hiring for AI, video, or creative roles
- new executives or leadership changes
- funding events
- strategic expansion
- product launches
One important rule from Antti is explicitly encoded: a newly appointed senior decision-maker can be a high-value target because they are under pressure to make changes.
The current implemented weights in config.py are:
| Factor | Weight | What It Measures |
|---|---|---|
| Segment Fit | 30 | how closely the company matches the priority verticals |
| Buyer Fit | 25 | how much decision power or influence the person likely has |
| Pain Fit | 20 | how strongly the signal suggests speed, scale, cost, or workflow pain |
| Workflow Fit | 15 | whether the company/person has a relevant video, AI, or creative workflow and how strong the engagement signal is |
| Commercial Fit | 10 | whether the company appears viable in size, stage, and momentum |
Current thresholds:
score >= 70 -> qualified
60 <= score < 70 -> review
score < 60 -> disqualified
Hard overrides:
- any hard disqualifier -> disqualified
- low signal strength -> disqualified
This weighting reflects the latest code and prompt calibration, which now puts the highest emphasis on:
- vertical fit
- buyer quality
- pain
- workflow and signal quality
- commercial viability
The system intentionally splits reasoning into two steps.
"What do we actually know?"
"Given what we know, how should we score this lead?"
This separation improves:
- auditability
- prompt clarity
- scoring consistency
- debugging when lead quality is wrong
If a lead is overqualified, the team can inspect whether the mistake came from:
- bad evidence extraction
- incorrect business logic
- wrong scoring weights
- weak source data
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCopy .env.example to .env, then choose the model/provider you want:
cp .env.example .envExample .env for Claude:
LLM_MODEL=anthropic/claude-haiku-4-5-20251001
LLM_API_KEY=sk-ant-...
LLM_PROVIDER=
LLM_API_BASE=
LLM_TIMEOUT_SECONDS=30Example .env for BytePlus ModelArk / Dola Seed 2.0 Pro:
LLM_MODEL=seed-2-0-pro-260328
LLM_API_KEY=your-ark-api-key
LLM_PROVIDER=openai
LLM_API_BASE=https://ark.ap-southeast.bytepluses.com/api/v3
LLM_TIMEOUT_SECONDS=30The app reads these values at startup, so switching providers is a .env change instead of a code change.
python3 main.py --input "10 gen leads.csv" --output-dir ./output --concurrency 10python3 main.py --input ./leads.json --output-dir ./output --concurrency 10python3 main.py --input <file> [options]
--input, -i required input file (.json or .csv)
--output-dir, -o output directory for CSVs and artifacts
--concurrency, -c concurrent lead processing, allowed range 5-20
--verbose, -v enable debug logging
The FastAPI web UI lets you upload CSVs, inspect current and historical runs, and deep-link to a run by run_id in the URL.
Without a database:
python3 ui/server.pyOpen:
http://localhost:8080
With a database:
- Set
DATABASE_URLbefore starting the server - Results persist across restarts and historical runs stay available in the UI
Example:
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/seedance
python3 ui/server.pyThis is the easiest full local setup if you want both the web app and PostgreSQL.
docker compose up --buildOpen:
http://localhost:8080
What Docker Compose provides:
appservice for the web UI and pipeline runnerdbservice using PostgreSQL 16- persisted Docker volumes for database state and run files
To stop:
docker compose downTo remove volumes too:
docker compose down -vThe recommended production shape is:
- one app container running
ui/server.py - one managed PostgreSQL instance
- environment variables injected by the hosting platform
Required environment variables:
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/seedance
LLM_MODEL=seed-2-0-pro-260328
LLM_API_KEY=your-api-key
LLM_PROVIDER=openai
LLM_API_BASE=https://ark.ap-southeast.bytepluses.com/api/v3
LLM_TIMEOUT_SECONDS=30Build the image:
docker build -t lead-generation-engine-v1 .Run the image manually against managed PostgreSQL:
docker run -d \
--name lead-engine \
-p 8080:8080 \
-e DATABASE_URL='postgresql://USER:PASSWORD@HOST:5432/seedance' \
-e LLM_MODEL='seed-2-0-pro-260328' \
-e LLM_API_KEY='your-api-key' \
-e LLM_PROVIDER='openai' \
-e LLM_API_BASE='https://ark.ap-southeast.bytepluses.com/api/v3' \
-e LLM_TIMEOUT_SECONDS='30' \
lead-generation-engine-v1Suitable cloud targets include:
- AWS ECS or Fargate with RDS PostgreSQL
- GCP Cloud Run with Cloud SQL PostgreSQL
- Render, Railway, or Fly.io with managed PostgreSQL
- a VM with Docker plus managed PostgreSQL
Cloud deployment checklist:
- provision PostgreSQL and set
DATABASE_URL - store API keys in the platform secret manager
- expose port
8080 - verify
GET /api/runsafter deployment - upload a sample CSV and confirm runs persist after container restart
For JSON input, each lead should follow this shape:
{
"lead_id": "unique-string",
"person_name": "Full Name",
"title": "VP of Creative",
"company_name": "Example Studio",
"company_domain": "examplestudio.com",
"linkedin_url": "https://linkedin.com/in/example",
"signal_text": "Raw signal or qualification note",
"signal_type": "linkedin_post",
"signal_timestamp": "2026-04-17T10:00:00Z"
}The qualified CSV captures:
- core lead identity
- decision
- score
- confidence
- segment
- suggested use case
- reasoning summary
The review CSV captures:
- core lead identity
- score and confidence
- missing data derived from Stage 1 unknowns
- reasoning summary
The disqualified CSV captures:
- core lead identity
- score and confidence
- disqualification reasons
- reasoning summary
Artifacts are the real source of truth for debugging. They preserve:
- what entered the pipeline
- what Claude extracted
- what Claude scored after Python validation
The current repo already contains a structured knowledge layer in skills/:
skills/seedance-icp/SKILL.mdskills/lead-qualification/SKILL.mdskills/trigify-signal-scoring/SKILL.mdskills/disqualification-rules/SKILL.md
These files are important because they represent the beginning of the broader context-engineering approach. They are not just implementation notes. They are the raw ingredients for the future Claude brain knowledge layer.
Right now they are effectively source material for the prompts. In a later version they can become dynamically loaded context modules.
The agreed pilot shape from the Antti discussion is:
- take the latest 100 leads
- have Antti manually identify the top 10
- run all 100 through the engine
- compare model ranking and routing against Antti's judgment
- refine ICP, disqualifiers, scoring, and signal interpretation based on the gaps
This is the right first validation step because it tests usefulness against real business judgment instead of only technical correctness.
The engine architecture is in place, but production-level outbound requires more than a working batch script.
- final ICP, persona, and anti-ICP calibration using real company data
- clearer definition of success metrics for qualification precision and ranking quality
- consistent human review loop for false positives and false negatives
- brand-voice and outreach-context assets for next-step messaging generation
- real company and people enrichment APIs
- reliable signal ingestion beyond current CSV exports
- stronger coverage for company size, funding, hiring, and time-in-role
- better handling of missing or partial data
- CRM integration
- review workflow for SDRs
- account-level ranking rather than only per-lead routing
- outreach generation after qualification
- broader automated test coverage
- monitoring and run-level metrics
- stronger error handling around external enrichments
- secrets and environment hardening
- deployment path beyond local CLI usage
For now, the most important next step is not a giant feature list. It is a disciplined iteration loop with Antti.
- collaborate with Antti every day
- show iterative progress based on his inputs
- review where the engine matched or missed his judgment
- refine ICP rules, scoring, signal interpretation, and disqualifiers continuously
- run the 100-lead pilot and benchmark against Antti's top 10
- tighten prompts and weights based on pilot misses
- replace placeholder business assumptions with actual Seedance targeting logic
- add stronger enrichment for company, person, and momentum signals
- add account-ranking and next-best-action outputs
- generate draft outreach once qualification quality is strong enough
- integrate with CRM and AE workflows
- move from lead-level qualification to daily prioritized account lists
- build reusable context modules for product knowledge, messaging, and historical emails
- pair Claude reasoning with structured enrichment from Clay or similar tools
This depends on access to data, speed of decision-making, and how quickly Antti can review outputs daily. Assuming one focused builder, daily feedback from Antti, and no major procurement blockers, a realistic estimate is:
Estimated time: 1-2 weeks
Focus:
- calibrate ICP and scoring against real leads
- run the 100-lead benchmark
- improve ranking precision
- replace placeholder assumptions with Seedance-specific logic
Estimated time: 1-2 weeks
Focus:
- integrate reliable enrichment sources
- improve data completeness
- add clearer review-loop outputs
- define operational metrics and acceptance thresholds
Estimated time: 2-4 weeks
Focus:
- CRM integration
- outreach generation support
- account-level prioritization
- deployment, monitoring, and operational hardening
- production-ready pilot for internal SDR use:
3-4 weeks - stronger production-level lead engine with integrations and operational guardrails:
5-8 weeks - broader "AE operator" foundation beyond lead qualification: additional phased work after that
This estimate assumes the team keeps the current daily iteration rhythm with Antti and avoids waiting for a perfectly polished v1 before testing with real leads.
The current repository is the first useful building block, not the end state.
The broader roadmap is:
- move from prompt engineering to context engineering
- centralize company knowledge so Claude reasons from the same source of truth every time
- combine Claude for reasoning with structured data systems for signal collection
- evolve from lead qualification into a daily outbound operating system for SDRs and AEs
If this project works, the next version should not just answer "is this lead good?"
It should answer:
- which accounts matter most today
- why they matter now
- who to contact
- what signal triggered the recommendation
- what message should be sent first
That is the real "Claude brain" vision this repo is starting to implement.