@veyralabs/skills 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/bin/cli.js +3 -2
- package/commands/venture-analyst.md +16 -0
- package/install.sh +1 -0
- package/package.json +6 -2
- package/skills/venture-suite/venture-analyst/SKILL.md +449 -0
- package/skills/venture-suite/venture-analyst/references/blue-ocean.md +130 -0
- package/skills/venture-suite/venture-analyst/references/customer-dev.md +147 -0
- package/skills/venture-suite/venture-analyst/references/founder-traps.md +191 -0
- package/skills/venture-suite/venture-analyst/references/lean-startup.md +123 -0
- package/skills/venture-suite/venture-analyst/references/mom-test.md +146 -0
- package/skills/venture-suite/venture-analyst/references/traction.md +154 -0
- package/skills/venture-suite/venture-analyst/scripts/enhance_detect.py +172 -0
- package/skills/venture-suite/venture-analyst/scripts/experiments.py +228 -0
- package/skills/venture-suite/venture-analyst/scripts/scraper.py +194 -0
- package/skills/venture-suite/venture-analyst/scripts/sources.py +288 -0
- package/skills/venture-suite/venture-analyst/templates/experiment-spec.md +119 -0
- package/skills/venture-suite/venture-analyst/templates/verdict.md +240 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Traction Reference
|
|
2
|
+
|
|
3
|
+
Gabriel Weinberg & Justin Mares, "Traction" (2014).
|
|
4
|
+
Framework for finding customer acquisition channels that actually work.
|
|
5
|
+
|
|
6
|
+
## Core insight
|
|
7
|
+
|
|
8
|
+
Most startups fail not because of product problems but traction problems. Building product and finding traction should happen in parallel, not sequentially.
|
|
9
|
+
|
|
10
|
+
Half your time should be spent on traction, not product — even pre-launch.
|
|
11
|
+
|
|
12
|
+
## The 19 traction channels
|
|
13
|
+
|
|
14
|
+
1. **Viral marketing** - product spreads through existing users (Dropbox, Slack)
|
|
15
|
+
2. **PR** - traditional press coverage
|
|
16
|
+
3. **Unconventional PR** - stunts, giveaways, memorable campaigns
|
|
17
|
+
4. **Search engine marketing (SEM)** - paid search (Google Ads)
|
|
18
|
+
5. **Social and display ads** - Meta, LinkedIn, programmatic
|
|
19
|
+
6. **Offline ads** - billboards, radio, TV, print
|
|
20
|
+
7. **Search engine optimization (SEO)** - organic search
|
|
21
|
+
8. **Content marketing** - blog, YouTube, podcast as acquisition
|
|
22
|
+
9. **Email marketing** - list building and outbound sequences
|
|
23
|
+
10. **Engineering as marketing** - free tools, calculators, embeds that drive signups
|
|
24
|
+
11. **Targeting blogs** - get covered by influential niche blogs
|
|
25
|
+
12. **Business development** - partnerships with complementary products
|
|
26
|
+
13. **Sales** - direct outbound sales, inside sales teams
|
|
27
|
+
14. **Affiliate programs** - revenue share for referrals
|
|
28
|
+
15. **Existing platforms** - App Store, Shopify App Store, Chrome extensions, marketplace distribution
|
|
29
|
+
16. **Trade shows** - industry events, conferences
|
|
30
|
+
17. **Offline events** - meetups, workshops, demos
|
|
31
|
+
18. **Speaking engagements** - conferences, podcasts, webinars
|
|
32
|
+
19. **Community building** - Slack groups, forums, ambassador programs
|
|
33
|
+
|
|
34
|
+
## Bullseye Framework
|
|
35
|
+
|
|
36
|
+
Don't try all 19 channels. Use the Bullseye to find the one that works.
|
|
37
|
+
|
|
38
|
+
### Step 1 — Brainstorm (outer ring)
|
|
39
|
+
For each of the 19 channels, answer:
|
|
40
|
+
- How could we use this channel?
|
|
41
|
+
- What would success look like?
|
|
42
|
+
- How could we test it cheaply?
|
|
43
|
+
|
|
44
|
+
Produces 19 rough hypotheses.
|
|
45
|
+
|
|
46
|
+
### Step 2 — Rank (middle ring)
|
|
47
|
+
Score each channel on 3 criteria (1-3):
|
|
48
|
+
- Probability of working
|
|
49
|
+
- Volume potential (enough customers to matter)
|
|
50
|
+
- Cost per acquisition (time + money)
|
|
51
|
+
|
|
52
|
+
Pick the top 6 channels. These are your "promising" channels.
|
|
53
|
+
|
|
54
|
+
### Step 3 — Test (inner ring — the bullseye)
|
|
55
|
+
Pick the 3 channels with highest combined score. Run cheap, fast tests on each simultaneously.
|
|
56
|
+
|
|
57
|
+
Each test:
|
|
58
|
+
- Fixed budget (~€200-500)
|
|
59
|
+
- Fixed time (2-4 weeks)
|
|
60
|
+
- One metric: cost per lead, or signups, or revenue
|
|
61
|
+
|
|
62
|
+
### Step 4 — Focus
|
|
63
|
+
One test will outperform the others. Focus 100% on that channel. Optimize it until it no longer scales.
|
|
64
|
+
|
|
65
|
+
When the channel plateaus, go back to Step 2.
|
|
66
|
+
|
|
67
|
+
## Channel selection by stage
|
|
68
|
+
|
|
69
|
+
Different channels dominate at different stages:
|
|
70
|
+
|
|
71
|
+
| Stage | Best channels |
|
|
72
|
+
|-------|--------------|
|
|
73
|
+
| Pre-PMF | Direct sales, outreach, niche blogs, communities |
|
|
74
|
+
| Early growth | Content, SEO, partnerships, email |
|
|
75
|
+
| Scaling | Paid acquisition (once LTV:CAC proven), viral, affiliates |
|
|
76
|
+
| Dominant | Brand, PR, events |
|
|
77
|
+
|
|
78
|
+
**Pre-PMF rule:** channels that require scale to work (SEO, paid ads, viral) are wrong for early stage. You don't have enough data to optimize them. Use channels where you control the targeting.
|
|
79
|
+
|
|
80
|
+
## Channel-market fit
|
|
81
|
+
|
|
82
|
+
Some channels only work for specific markets:
|
|
83
|
+
|
|
84
|
+
| Channel | Works for |
|
|
85
|
+
|---------|-----------|
|
|
86
|
+
| Cold outbound | B2B with clear ICP, high LTV |
|
|
87
|
+
| Content/SEO | Markets that Google actively |
|
|
88
|
+
| Product-led viral | Consumer, collaboration tools |
|
|
89
|
+
| Community | Niche markets with strong identity |
|
|
90
|
+
| Paid social | B2C with wide audience |
|
|
91
|
+
| Trade shows | B2B with offline buying decisions |
|
|
92
|
+
| Partnerships | Products with complementary distribution |
|
|
93
|
+
|
|
94
|
+
Picking the wrong channel for your market wastes 6 months.
|
|
95
|
+
|
|
96
|
+
## Cost benchmarks (rough, 2024)
|
|
97
|
+
|
|
98
|
+
| Channel | Metric | B2C benchmark | B2B benchmark |
|
|
99
|
+
|---------|--------|--------------|--------------|
|
|
100
|
+
| Facebook/Instagram ads | CPL | €3-8 | €15-30 |
|
|
101
|
+
| Google Search | CPL | €5-15 | €20-60 |
|
|
102
|
+
| LinkedIn ads | CPL | - | €40-100 |
|
|
103
|
+
| Cold email | CPL | - | €20-80 (time cost) |
|
|
104
|
+
| Content/SEO | CPL | €5-20 (long term) | €15-50 (long term) |
|
|
105
|
+
| Referral/viral | CPL | €1-5 | €5-20 |
|
|
106
|
+
|
|
107
|
+
LTV must be at least 3x CAC for paid channels to be viable.
|
|
108
|
+
|
|
109
|
+
## Traction metrics by channel
|
|
110
|
+
|
|
111
|
+
**Viral / product-led:**
|
|
112
|
+
- K-factor = (invites per user) × (invite conversion rate). Target K > 0.5 for meaningful viral effect
|
|
113
|
+
- Time to viral loop: how fast does one user create another?
|
|
114
|
+
|
|
115
|
+
**Content / SEO:**
|
|
116
|
+
- Organic sessions per month
|
|
117
|
+
- Keyword rankings for high-intent terms
|
|
118
|
+
- Time to rank: SEO takes 3-6 months minimum to show results
|
|
119
|
+
|
|
120
|
+
**Email:**
|
|
121
|
+
- List growth rate per week
|
|
122
|
+
- Open rate: >30% is good for cold; >50% for warm
|
|
123
|
+
- Click-through: >5% is strong
|
|
124
|
+
|
|
125
|
+
**Paid acquisition:**
|
|
126
|
+
- CPL and CAC (cost per acquired customer)
|
|
127
|
+
- Payback period: how many months to recoup CAC?
|
|
128
|
+
- ROAS (return on ad spend)
|
|
129
|
+
|
|
130
|
+
**Sales:**
|
|
131
|
+
- Leads per week
|
|
132
|
+
- Conversion rate (leads to paying)
|
|
133
|
+
- Sales cycle length
|
|
134
|
+
- Average contract value
|
|
135
|
+
|
|
136
|
+
## Applying to venture analysis
|
|
137
|
+
|
|
138
|
+
In Phase 3 (validation), recommend channels based on:
|
|
139
|
+
|
|
140
|
+
1. Market type (B2B vs B2C)
|
|
141
|
+
2. LTV estimate (high LTV = more channels viable)
|
|
142
|
+
3. Available budget
|
|
143
|
+
4. Target customer location (online? offline? specific communities?)
|
|
144
|
+
|
|
145
|
+
For zero-budget startups, the viable channels are:
|
|
146
|
+
- Direct outreach (cold email/DM) — time only
|
|
147
|
+
- Niche community posting (Reddit, specific Slack/Discord groups) — time only
|
|
148
|
+
- Engineering as marketing — build a free tool that attracts ICPs
|
|
149
|
+
- Content on a specific topic ICP searches for
|
|
150
|
+
|
|
151
|
+
For startups with €100-500/month budget:
|
|
152
|
+
- Fake door test (landing page + small paid traffic)
|
|
153
|
+
- LinkedIn outreach with Sales Navigator trial
|
|
154
|
+
- Sponsored newsletter in a niche (often €100-300 per send)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Level 2 auto-detection.
|
|
3
|
+
Silently detects available enhancements. Never asks the user for config.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def detect_level() -> dict:
|
|
12
|
+
"""
|
|
13
|
+
Detect what's available. Returns a capability map.
|
|
14
|
+
Called once at session start — results cached for the session.
|
|
15
|
+
"""
|
|
16
|
+
return {
|
|
17
|
+
"docker": _has_docker(),
|
|
18
|
+
"searxng": _has_searxng(),
|
|
19
|
+
"veyrascrape_mcp": _has_env("VEYRASCRAPE_API_KEY"),
|
|
20
|
+
"github_token": _has_env("GITHUB_TOKEN"),
|
|
21
|
+
"exa_key": _has_env("EXA_API_KEY"),
|
|
22
|
+
"tavily_key": _has_env("TAVILY_API_KEY"),
|
|
23
|
+
"groq_key": _has_env("GROQ_API_KEY"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _has_env(key: str) -> bool:
|
|
28
|
+
val = os.environ.get(key, "").strip()
|
|
29
|
+
return bool(val)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_docker() -> bool:
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["docker", "info"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
timeout=4,
|
|
38
|
+
)
|
|
39
|
+
return result.returncode == 0
|
|
40
|
+
except Exception:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _has_searxng(ports: list[int] = None) -> bool:
|
|
45
|
+
if ports is None:
|
|
46
|
+
ports = [8080, 8888, 4000]
|
|
47
|
+
for port in ports:
|
|
48
|
+
try:
|
|
49
|
+
r = requests.get(
|
|
50
|
+
f"http://localhost:{port}/search",
|
|
51
|
+
params={"q": "test", "format": "json"},
|
|
52
|
+
timeout=2,
|
|
53
|
+
)
|
|
54
|
+
if r.status_code == 200:
|
|
55
|
+
return True
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_searxng() -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Launch SearXNG via Docker if available and not already running.
|
|
64
|
+
Returns status dict.
|
|
65
|
+
"""
|
|
66
|
+
if _has_searxng():
|
|
67
|
+
return {"available": True, "url": "http://localhost:8080", "launched": False}
|
|
68
|
+
|
|
69
|
+
if not _has_docker():
|
|
70
|
+
return {"available": False, "reason": "docker_not_found"}
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
subprocess.run(
|
|
74
|
+
[
|
|
75
|
+
"docker", "run", "-d",
|
|
76
|
+
"--name", "venture-searxng",
|
|
77
|
+
"-p", "8080:8080",
|
|
78
|
+
"searxng/searxng",
|
|
79
|
+
],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
timeout=45,
|
|
82
|
+
)
|
|
83
|
+
# give it time to start
|
|
84
|
+
for _ in range(6):
|
|
85
|
+
time.sleep(2)
|
|
86
|
+
if _has_searxng():
|
|
87
|
+
return {"available": True, "url": "http://localhost:8080", "launched": True}
|
|
88
|
+
return {"available": False, "reason": "searxng_start_timeout"}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return {"available": False, "reason": str(e)}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def search_searxng(query: str, url: str = "http://localhost:8080", limit: int = 10) -> list[dict]:
|
|
94
|
+
"""Search via local SearXNG instance."""
|
|
95
|
+
try:
|
|
96
|
+
r = requests.get(
|
|
97
|
+
f"{url}/search",
|
|
98
|
+
params={"q": query, "format": "json", "pageno": 1},
|
|
99
|
+
timeout=10,
|
|
100
|
+
)
|
|
101
|
+
results = r.json().get("results", [])[:limit]
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
"source": "searxng",
|
|
105
|
+
"title": res.get("title", ""),
|
|
106
|
+
"url": res.get("url", ""),
|
|
107
|
+
"text": res.get("content", ""),
|
|
108
|
+
"engine": res.get("engine", ""),
|
|
109
|
+
}
|
|
110
|
+
for res in results
|
|
111
|
+
]
|
|
112
|
+
except Exception:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def search_with_exa(query: str, api_key: str, limit: int = 10) -> list[dict]:
|
|
117
|
+
"""Semantic search via Exa API (optional Level 3 enhancement)."""
|
|
118
|
+
try:
|
|
119
|
+
import exa_py
|
|
120
|
+
exa = exa_py.Exa(api_key=api_key)
|
|
121
|
+
results = exa.search(query, num_results=limit, use_autoprompt=True)
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
"source": "exa",
|
|
125
|
+
"title": r.title or "",
|
|
126
|
+
"url": r.url,
|
|
127
|
+
"text": r.text or "",
|
|
128
|
+
}
|
|
129
|
+
for r in results.results
|
|
130
|
+
]
|
|
131
|
+
except Exception:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def best_search(query: str, env: dict, limit: int = 10) -> list[dict]:
|
|
136
|
+
"""
|
|
137
|
+
Use the best available search method based on detected environment.
|
|
138
|
+
Priority: Exa > SearXNG > Tavily > ddgs
|
|
139
|
+
"""
|
|
140
|
+
# Level 3: Exa
|
|
141
|
+
if env.get("exa_key"):
|
|
142
|
+
results = search_with_exa(query, os.environ["EXA_API_KEY"], limit)
|
|
143
|
+
if results:
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
# Level 2: SearXNG
|
|
147
|
+
if env.get("searxng"):
|
|
148
|
+
results = search_searxng(query, limit=limit)
|
|
149
|
+
if results:
|
|
150
|
+
return results
|
|
151
|
+
|
|
152
|
+
# Level 2: Tavily
|
|
153
|
+
if env.get("tavily_key"):
|
|
154
|
+
try:
|
|
155
|
+
from tavily import TavilyClient
|
|
156
|
+
client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
|
|
157
|
+
response = client.search(query, max_results=limit)
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
"source": "tavily",
|
|
161
|
+
"title": r.get("title", ""),
|
|
162
|
+
"url": r.get("url", ""),
|
|
163
|
+
"text": r.get("content", ""),
|
|
164
|
+
}
|
|
165
|
+
for r in response.get("results", [])
|
|
166
|
+
]
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
# Level 1: ddgs (always available)
|
|
171
|
+
from scripts.sources import search_web
|
|
172
|
+
return search_web(query, limit)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation experiment generator.
|
|
3
|
+
Based on Lean Startup + Mom Test methodology.
|
|
4
|
+
Outputs concrete, actionable experiments — not analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
EXPERIMENTS = {
|
|
8
|
+
"cold_outreach": {
|
|
9
|
+
"name": "Cold Outreach Test",
|
|
10
|
+
"type": "discovery",
|
|
11
|
+
"cost": "0€",
|
|
12
|
+
"duration": "5-7 days",
|
|
13
|
+
"effort": "medium",
|
|
14
|
+
"goal": "Validate problem exists and find early adopters",
|
|
15
|
+
"process": [
|
|
16
|
+
"Identify 50 potential customers on LinkedIn, Reddit, or niche communities",
|
|
17
|
+
"Send short message: 'I'm researching [problem area] — would you spare 15 min?'",
|
|
18
|
+
"Do NOT pitch. Ask about their current process and pain.",
|
|
19
|
+
"Apply Mom Test: ask about past behavior, not future intentions",
|
|
20
|
+
],
|
|
21
|
+
"metric": "Response rate + calls booked",
|
|
22
|
+
"success_criteria": "≥10% response rate, ≥3 discovery calls completed",
|
|
23
|
+
"red_flags": [
|
|
24
|
+
"Everyone says 'great idea' but nobody wants a call",
|
|
25
|
+
"People say they'd pay but won't commit to a free beta",
|
|
26
|
+
"You have to explain the problem before they understand it",
|
|
27
|
+
],
|
|
28
|
+
"mom_test_questions": [
|
|
29
|
+
"Tell me how you currently handle [problem]. Walk me through your last time.",
|
|
30
|
+
"What's the most frustrating part of that process?",
|
|
31
|
+
"Have you tried other solutions? What happened?",
|
|
32
|
+
"How much time/money does this cost you per month right now?",
|
|
33
|
+
"What would you do if this problem disappeared tomorrow?",
|
|
34
|
+
],
|
|
35
|
+
"bad_questions_to_avoid": [
|
|
36
|
+
"Would you use a tool that does X?",
|
|
37
|
+
"Do you think this is a good idea?",
|
|
38
|
+
"Would you pay $X for this?",
|
|
39
|
+
"How much would you pay for this?",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"fake_door": {
|
|
44
|
+
"name": "Fake Door Test",
|
|
45
|
+
"type": "demand_signal",
|
|
46
|
+
"cost": "0-30€",
|
|
47
|
+
"duration": "7-14 days",
|
|
48
|
+
"effort": "low",
|
|
49
|
+
"goal": "Measure real demand before building anything",
|
|
50
|
+
"process": [
|
|
51
|
+
"Build a landing page (Carrd.co free, 30 min)",
|
|
52
|
+
"Describe the value proposition clearly — no features, only outcome",
|
|
53
|
+
"Add a CTA button: 'Join Waitlist' or 'Get Early Access'",
|
|
54
|
+
"When clicked: show 'Thanks, we'll notify you' — collect email",
|
|
55
|
+
"Drive traffic: 2-3 Reddit posts in relevant communities + LinkedIn",
|
|
56
|
+
"Track: page visits, CTA clicks, emails collected",
|
|
57
|
+
],
|
|
58
|
+
"metric": "CTA click-through rate (CTR)",
|
|
59
|
+
"success_criteria": "≥5% CTR from cold traffic, ≥50 emails in 14 days",
|
|
60
|
+
"tools": [
|
|
61
|
+
"Carrd.co — free landing pages",
|
|
62
|
+
"Tally.so — free forms with email collection",
|
|
63
|
+
"Google Analytics (free) — traffic tracking",
|
|
64
|
+
"Plausible.io — privacy-friendly alternative",
|
|
65
|
+
],
|
|
66
|
+
"headline_formula": "[Outcome they want] without [current pain]",
|
|
67
|
+
"subheadline_formula": "[Product name] helps [target customer] [achieve outcome] by [key mechanism]",
|
|
68
|
+
"red_flags": [
|
|
69
|
+
"CTR below 2% consistently",
|
|
70
|
+
"High visits but nobody clicks CTA",
|
|
71
|
+
"Traffic only from your own network — zero cold traffic",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"waitlist": {
|
|
76
|
+
"name": "Waitlist Campaign",
|
|
77
|
+
"type": "demand_signal",
|
|
78
|
+
"cost": "0€",
|
|
79
|
+
"duration": "14-30 days",
|
|
80
|
+
"effort": "medium",
|
|
81
|
+
"goal": "Build an audience before building the product",
|
|
82
|
+
"process": [
|
|
83
|
+
"Build landing with value proposition + email signup",
|
|
84
|
+
"Write 3-5 posts for relevant communities (Reddit, LinkedIn, Twitter)",
|
|
85
|
+
"Frame as 'I'm building X because Y — who's interested?'",
|
|
86
|
+
"Respond to every comment — this is customer research",
|
|
87
|
+
"Track: emails per day, most common questions, what resonates",
|
|
88
|
+
],
|
|
89
|
+
"metric": "Email signups per week",
|
|
90
|
+
"success_criteria": "100 organic signups in 30 days without paid ads",
|
|
91
|
+
"tools": [
|
|
92
|
+
"Beehiiv (free for <2500 subs) — email list",
|
|
93
|
+
"Carrd.co — landing",
|
|
94
|
+
"Reddit (organic, zero cost)",
|
|
95
|
+
"LinkedIn (organic, zero cost)",
|
|
96
|
+
],
|
|
97
|
+
"red_flags": [
|
|
98
|
+
"Only your friends/connections sign up",
|
|
99
|
+
"High unsubscribe rate on first email",
|
|
100
|
+
"Nobody shares it organically",
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
"concierge_mvp": {
|
|
105
|
+
"name": "Concierge MVP",
|
|
106
|
+
"type": "value_validation",
|
|
107
|
+
"cost": "0€",
|
|
108
|
+
"duration": "2-4 weeks",
|
|
109
|
+
"effort": "high",
|
|
110
|
+
"goal": "Deliver the core value manually to 3-5 real users",
|
|
111
|
+
"process": [
|
|
112
|
+
"Find 3-5 people with the problem (from outreach or waitlist)",
|
|
113
|
+
"Offer to do the service manually for free",
|
|
114
|
+
"Do it by hand: spreadsheets, email, calls, manual research",
|
|
115
|
+
"Observe where they get value, where they get frustrated",
|
|
116
|
+
"At the end: 'Would you pay X/month for this if it was automated?'",
|
|
117
|
+
],
|
|
118
|
+
"metric": "Willingness to pay (real commitment, not hypothetical)",
|
|
119
|
+
"success_criteria": "≥3/5 users say yes to paying target price, 1+ pre-orders",
|
|
120
|
+
"key_question": "If this service disappeared tomorrow, what would you do?",
|
|
121
|
+
"what_to_track": [
|
|
122
|
+
"Which parts of the manual process they value most",
|
|
123
|
+
"Which parts they don't care about",
|
|
124
|
+
"How often they'd use it",
|
|
125
|
+
"What would make them stop using it",
|
|
126
|
+
"Who else they'd recommend it to",
|
|
127
|
+
],
|
|
128
|
+
"red_flags": [
|
|
129
|
+
"'I'd use it but can't commit right now'",
|
|
130
|
+
"They use it but never refer anyone",
|
|
131
|
+
"The manual process takes so long it's not viable to automate",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
"smoke_test_ads": {
|
|
136
|
+
"name": "Paid Smoke Test",
|
|
137
|
+
"type": "demand_signal",
|
|
138
|
+
"cost": "30-100€",
|
|
139
|
+
"duration": "7 days",
|
|
140
|
+
"effort": "medium",
|
|
141
|
+
"goal": "Measure demand with cold paid traffic — no bias from network",
|
|
142
|
+
"process": [
|
|
143
|
+
"Set up landing page with single CTA",
|
|
144
|
+
"Run €30-50 in Meta or Google Ads to target audience",
|
|
145
|
+
"B2C: Facebook/Instagram interest targeting",
|
|
146
|
+
"B2B: LinkedIn job title targeting (more expensive, ~€5-10/click)",
|
|
147
|
+
"Measure CPL (cost per lead) — this is your true demand signal",
|
|
148
|
+
],
|
|
149
|
+
"metric": "Cost Per Lead (CPL)",
|
|
150
|
+
"success_criteria": "B2C CPL < 5€, B2B CPL < 40€",
|
|
151
|
+
"tools": [
|
|
152
|
+
"Meta Ads Manager (€30 minimum budget)",
|
|
153
|
+
"Google Ads (€10/day minimum)",
|
|
154
|
+
"LinkedIn Ads (most expensive — B2B only, €15-50/click)",
|
|
155
|
+
],
|
|
156
|
+
"red_flags": [
|
|
157
|
+
"CPL 5x higher than benchmark — market doesn't respond to messaging",
|
|
158
|
+
"Clicks but no conversions — landing page or offer problem",
|
|
159
|
+
"Can't target audience precisely — unclear ICP",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_experiments(
|
|
166
|
+
idea: str,
|
|
167
|
+
target_customer: str,
|
|
168
|
+
market_type: str = "b2c",
|
|
169
|
+
competition_level: str = "medium",
|
|
170
|
+
budget: str = "zero",
|
|
171
|
+
) -> list[dict]:
|
|
172
|
+
"""
|
|
173
|
+
Generate 3 prioritized validation experiments.
|
|
174
|
+
Always starts with the cheapest, highest-signal experiments first.
|
|
175
|
+
"""
|
|
176
|
+
selected = []
|
|
177
|
+
|
|
178
|
+
# Always: Cold outreach first (zero cost, real signal)
|
|
179
|
+
exp = EXPERIMENTS["cold_outreach"].copy()
|
|
180
|
+
exp["priority"] = 1
|
|
181
|
+
exp["customized_for"] = idea
|
|
182
|
+
exp["target"] = target_customer
|
|
183
|
+
selected.append(exp)
|
|
184
|
+
|
|
185
|
+
# Always: Fake door (low cost, measurable)
|
|
186
|
+
exp = EXPERIMENTS["fake_door"].copy()
|
|
187
|
+
exp["priority"] = 2
|
|
188
|
+
exp["customized_for"] = idea
|
|
189
|
+
selected.append(exp)
|
|
190
|
+
|
|
191
|
+
# B2B with budget: concierge MVP
|
|
192
|
+
# B2C with budget: waitlist
|
|
193
|
+
if market_type.lower() == "b2b" or competition_level == "high":
|
|
194
|
+
exp = EXPERIMENTS["concierge_mvp"].copy()
|
|
195
|
+
exp["priority"] = 3
|
|
196
|
+
exp["customized_for"] = idea
|
|
197
|
+
exp["target"] = target_customer
|
|
198
|
+
selected.append(exp)
|
|
199
|
+
else:
|
|
200
|
+
exp = EXPERIMENTS["waitlist"].copy()
|
|
201
|
+
exp["priority"] = 3
|
|
202
|
+
exp["customized_for"] = idea
|
|
203
|
+
selected.append(exp)
|
|
204
|
+
|
|
205
|
+
return selected
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def format_experiment_output(experiments: list[dict], idea: str) -> str:
|
|
209
|
+
"""Format experiments as markdown output."""
|
|
210
|
+
lines = [f"# Validation Experiments — {idea}\n"]
|
|
211
|
+
lines.append("Start with Experiment 1. Only move to the next if the previous fails.\n")
|
|
212
|
+
|
|
213
|
+
for exp in experiments:
|
|
214
|
+
lines.append(f"## Experiment {exp['priority']}: {exp['name']}")
|
|
215
|
+
lines.append(f"**Cost:** {exp['cost']} | **Duration:** {exp['duration']} | **Effort:** {exp['effort']}")
|
|
216
|
+
lines.append(f"\n**Goal:** {exp['goal']}\n")
|
|
217
|
+
lines.append("**Process:**")
|
|
218
|
+
for step in exp.get("process", []):
|
|
219
|
+
lines.append(f"- {step}")
|
|
220
|
+
lines.append(f"\n**Metric:** {exp['metric']}")
|
|
221
|
+
lines.append(f"**Success:** {exp['success_criteria']}")
|
|
222
|
+
if exp.get("red_flags"):
|
|
223
|
+
lines.append("\n**Red flags to watch:**")
|
|
224
|
+
for flag in exp["red_flags"]:
|
|
225
|
+
lines.append(f"- {flag}")
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
return "\n".join(lines)
|