
Channel Economics
- 1 installs
- 23.8k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
Channel-economics is a Claude Code skill that computes fully-loaded cost-to-serve, three-lens ROI, and the optimal mix for direct versus partner sales channels.
About
Channel-economics is a skill for RevOps and commercial leaders that computes fully-loaded cost-to-serve and ROI for each sales channel. A developer or analyst uses it during a quarterly channel review to see whether direct or partner-led channels actually make money after CAC, discounts, support load, and overhead are loaded in. It runs three Python scripts to output per-channel verdicts, a sensitivity-tested channel-mix recommendation, and a diminishing-returns inflection point.
- Emits per-channel DOUBLE-DOWN / MAINTAIN / DEFUND / EXIT verdicts
- Three ROI lenses: cash year-1, LTV-adjusted, and marginal
- Runs three stdlib-only Python scripts on intake channel data
Channel Economics by the numbers
- 1 all-time installs (skills.sh)
- Ranked #909 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
channel-economics capabilities & compatibility
- Capabilities
- data analysis
- Use cases
- data analysis
- Pricing
- Free
What channel-economics says it does
The skill emits **per-channel verdicts** (DOUBLE-DOWN / MAINTAIN / DEFUND / EXIT), a **sensitivity-tested mix recommendation**, and **the diminishing-returns inflection point**.
All scripts: stdlib only.
npx skills add https://github.com/alirezarezvani/claude-skills --skill channel-economicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 23.8k |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
What it does
Compute fully-loaded cost-to-serve and ROI per sales channel to decide the optimal direct-vs-partner channel mix.
Who is it for?
Head of Commercial, RevOps, or VP Sales running a quarterly channel review with a mixed direct and partner pipeline.
Skip if: Designing partner tiers or revshare splits, SDR-to-AE routing, GAAP revenue recognition, or per-deal discount approval.
When should I use this skill?
Pipeline is 60/40 or 50/50 direct vs partner and nobody knows which channel is profitable after CAC and overhead.
What you get
Per-channel ROI verdicts, a sensitivity-tested mix recommendation, and the diminishing-returns inflection point.
- cost-to-serve report
- channel ROI report with verdicts
- constrained channel-mix recommendation
By the numbers
- 3 stdlib-only Python scripts
- 3 ROI lenses (cash / LTV / marginal)
- 5 industry profiles (saas/api/enterprise-software/marketplace/hardware)
Files
channel-economics
Purpose
Help Head of Commercial / RevOps / VP Sales answer three questions at the quarterly channel review:
1. What does each channel actually cost to serve, fully loaded? (direct headcount, channel manager attribution, partner discount, MDF, enablement time, support load, allocated overhead) 2. What is the ROI of each channel under three lenses? (cash ROI year-1, LTV-adjusted ROI, marginal ROI — next dollar of investment) 3. What is the optimal channel mix subject to our strategic constraints? (minimum direct floor, maximum partner concentration ceiling, sensitivity to CAC shifts)
The skill emits per-channel verdicts (DOUBLE-DOWN / MAINTAIN / DEFUND / EXIT), a sensitivity-tested mix recommendation, and the diminishing-returns inflection point. It does not pick the strategy — humans do, with the numbers loaded honestly for the first time.
When to use
- Quarterly channel review: pipeline is 60/40 or 50/50 direct vs partner and you don't actually know which one is profitable
- Considering hiring a channel manager — need to know if the channel can clear the loaded-cost bar
- Partner program ROI question from the board ("we spent $X on MDF — what did we get?")
- A segment is over-indexed to one channel and you suspect mix dogma is blocking the other
- About to expand into a new region and need to decide direct-first vs partner-first
- M&A diligence: target company claims "partner-led at 70% gross margin" — need to validate after loading
Do not use for:
- Designing partner tiers, joint GTM motion, revshare splits →
partnerships-architect - SDR-to-AE routing, lead scoring, MQL definitions →
business-growth/revenue-operations - Strategic CRO decisions ("should we hire a VP Sales?", comp plan design) →
c-level-advisor/cro-advisor - Quarterly close, GAAP revenue recognition, channel-level P&L for historical reporting →
finance/financial-analysis - Per-deal discount approval →
deal-desk - Pricing model design →
pricing-strategist
Workflow
Step 1 — Intake channel data
Fill assets/channel_data_template.md (≈ 20 min). Capture per channel: deal count TTM, ARR TTM, avg deal size, gross margin %, CAC, sales-cycle days, retention rate, expansion rate, partner discount %, all attributable costs (SDR / AE / SE / channel manager / CS / support / marketing / partner MDF / tooling / overhead allocation %).
The template surfaces the costs teams most often forget: partner enablement time, certification investment, channel-conflict resolution overhead, channel-manager headcount cost.
Step 2 — Compute cost-to-serve per channel
Run scripts/cost_to_serve_calculator.py --input channel.json --output markdown.
Output: fully-loaded cost-to-serve per deal AND per dollar of ARR, with direct costs broken out from allocated overhead, and a "true gross margin" line after channel-specific load. Flags double-counting and surfaces hidden costs.
Run once per channel. The "true gross margin" line is the input the next two scripts care about.
Step 3 — Compute ROI per channel under three lenses
Run scripts/channel_roi_analyzer.py --input roi.json --profile saas --output markdown.
Output: per channel, three ROI numbers (Cash year-1, LTV-adjusted, Marginal), the diminishing-returns inflection point, and a verdict: DOUBLE-DOWN / MAINTAIN / DEFUND / EXIT.
Verdict logic is deterministic and surfaced in the report. Humans can override; the skill won't.
Step 4 — Optimize channel mix subject to constraints
Run scripts/channel_mix_optimizer.py --input mix.json --profile saas --output markdown.
Output: recommended mix that maximizes effective ARR subject to constraints (min direct %, max partner concentration), plus a sensitivity table (what if direct CAC rises 20%? what if partner discount widens 5 points?).
Step 5 — Decide
Take the three reports into the quarterly channel review. The skill recommends; the human commits.
Scripts
scripts/cost_to_serve_calculator.py— fully-loaded cost-to-serve per deal AND per $ ARR, with hidden-cost surfacingscripts/channel_roi_analyzer.py— 3-lens ROI (Cash / LTV / Marginal) with verdicts and diminishing-returns inflectionscripts/channel_mix_optimizer.py— constrained mix optimizer with sensitivity scenarios
All scripts: stdlib only. --help, --sample, --input, --output work on all three. Industry tuning via --profile {saas,api,enterprise-software,marketplace,hardware} on the two analyzers.
Quick example
# Emits fully-loaded cost-to-serve per channel (direct vs partner-led) for the built-in sample channel data
cd commercial/skills/channel-economics && python3 scripts/cost_to_serve_calculator.py --sampleReferences
references/channel_economics_canon.md— Skok, Bessemer State of the Cloud, Tunguz, Pacific Crest / KeyBanc SaaS Survey, Ramanujam, Jay McBain (Canalys)references/cost_to_serve_canon.md— Kaplan & Cooper (ABC), Horngren, Jeremy Hope, IBM CTS case studies, McKinsey, Gartner, BCGreferences/channel_anti_patterns.md— Forrester, Tunguz, Hessling, HBR, SiriusDecisions, MIT Sloan, Gartner
Assumptions
- Channel economics is a forward-looking question. Historical channel P&L is finance's job; this skill loads forward economics for a decision.
- "Channel" means a coherent go-to-market motion (direct outbound, partner-led, marketplace, reseller, OEM). It does not mean a marketing source.
- Cost-to-serve requires honest overhead allocation. The script validates that overhead % is consistent across channels — false partner-margin lift from inconsistent allocation is the #1 anti-pattern.
- LTV inputs (retention, expansion) are per-channel, not pooled. Partner-sourced customers often retain differently than direct-sourced — this difference is usually the largest economic variable and the most ignored.
- Industry profiles (
--profile) tune defaults for benchmarks (e.g., SaaS direct CAC payback target ~12mo, enterprise ~18mo) — they don't override your numbers. - This is a decision-support skill. Output is verdicts and a recommended mix, never an automatic resource reallocation.
Anti-patterns
- Treating "influenced" deals as "sourced" deals. A partner that touched a deal your AE already had is not channel-sourced revenue. Loading this as partner revenue inflates partner ROI and inflates direct CAC simultaneously.
- Inconsistent overhead allocation. Allocating 25% overhead to direct deals and 5% to partner deals because "the partner handles the overhead" is false. The partner manager, partner program, MDF, certification, and conflict-resolution all live in your P&L.
- Ignoring enablement time as a cost. Every hour your AE spends co-selling with a partner is a direct cost charged to the partner channel — most teams forget to load it.
- MDF without ROI tracking. Market Development Funds disbursed without an attributable pipeline ROI are just a partner-discount extension. The skill flags MDF with no return.
- Channel-mix dogma. "We're a partner-first company" / "we don't sell direct" blocks profitable segments. Mix should follow the math, not the slogan.
- Computing channel ROI without retention differential. If partner-sourced customers churn 5 points higher than direct, ignoring it overstates partner LTV by 30-50%. Per-channel retention is mandatory input.
- No cost-attribution for channel-manager headcount. A $200k channel manager managing $4M of partner ARR is $50 of channel-manager cost per $1k ARR — material to the verdict.
- Confusing this skill with partnerships-architect. That skill designs the partner program. This skill tells you whether the program pays for itself.
Distinct from
- commercial/partnerships-architect — partner tier design, joint GTM motion, revshare splits, partner enablement. Partner program structure, not partner program economics. This skill consumes the program structure as input and emits the economic verdict.
- business-growth/revenue-operations — lead routing, SDR motion, MQL definition, pipeline operations. RevOps owns the funnel mechanics; this skill loads the channel-level economic outcome.
- c-level-advisor/cro-advisor — strategic CRO judgment: when to hire a VP Sales, comp plan philosophy, territory design, multi-year revenue strategy. CRO advisor consumes channel-economics output as one input among many.
- finance/financial-analysis — close-and-report on historical channel P&L per GAAP. This skill is forward-looking decision support; finance is historical record. Different time horizon, different audience, different output.
- commercial/deal-desk — per-deal discount approval. Operates daily; this skill operates quarterly.
- commercial/pricing-strategist — pricing model and tier design. Pricing is input; channel economics is what happens at that pricing across channels.
Forcing-question library (Matt Pocock grill discipline)
Walked one at a time by /cs:grill-commercial or the orchestrator. Recommended answer + canon citation per question. Never bundled.
1. "What's your fully-loaded cost-to-serve per channel — including channel-manager headcount, MDF, partner enablement time, and overhead allocation?" Recommended: load all four. Most teams load partner discount but forget the channel-manager headcount and the enablement time, inflating partner margin by 8-15 points. Canon: Kaplan & Cooper (HBR 1988) — Measure Costs Right: Make the Right Decisions. Activity-Based Costing was invented precisely because channel costs hide in overhead and distort margin comparisons.
2. "What is the retention differential between direct-sourced and partner-sourced customers?" Recommended: instrument per-channel retention BEFORE running channel ROI. A 5-point retention gap moves LTV by 30-50%. Canon: David Skok (For Entrepreneurs — SaaS Metrics 2.0). LTV = (ARPA × Gross Margin) / Churn. Channel-blind churn is the most common source of false channel ROI.
3. "What share of 'channel-sourced' pipeline did your team actually originate?" Recommended: if your AE already had the account, it's not channel-sourced — it's channel-influenced. Influence and source are different economic lines. Canon: SiriusDecisions / Forrester channel attribution research — confused source vs. influence is the #1 reason partner ROI is overstated industry-wide.
4. "What is the marginal ROI of the next dollar invested in partner program vs. direct sales?" Recommended: compute the diminishing-returns curve on both. Average ROI hides the fact that the next dollar might earn 0.3x while the average earns 2.1x. Canon: Tomasz Tunguz (Tomasz Tunguz blog — channel CAC analyses). Average ROI is a vanity metric; marginal ROI drives investment decisions.
5. "What's your MDF-to-attributable-pipeline ratio in the last 4 quarters?" Recommended: < 5:1 (every $1 of MDF should generate ≥ $5 of attributable pipeline within 2 quarters). Anything looser is partner-discount theatre. Canon: Jay McBain (Canalys) — State of the Channel research. MDF without attribution discipline is the most expensive form of channel subsidy.
6. "Is your channel-mix dogma blocking a profitable segment?" Recommended: surface the dogma ("we're partner-first", "we don't sell direct in SMB") explicitly. Mix should follow the segment math. Canon: MIT Sloan Management Review — When Channel Conflict Means Growth. Dogmatic single-channel strategies forfeit 15-25% of TAM in mid-market specifically.
7. "What overhead-allocation methodology are you applying — and is it consistent across direct and partner?" Recommended: same methodology, same denominator, both channels. Inconsistent allocation is the silent killer of channel-economics analysis. Canon: Charles Horngren (Cost Accounting: A Managerial Emphasis) — allocation consistency is the precondition for cross-segment margin comparison. Without it, every conclusion is contaminated.
Walk depth-first. Lock 1-3 before opening 4-7. After all 7 are answered, invoke cost_to_serve_calculator.py → channel_roi_analyzer.py → channel_mix_optimizer.py in sequence.
Channel Data Template
Fill this out in ~20 minutes. The three scripts in this skill all consume JSON; this template gives you the schema with annotations on what to put and why.
If you don't know a value, leave it `null` (or the explicit "$0 unknown") and note it — the scripts surface unknowns explicitly rather than silently substituting.
---
Intake checklist (before you fill anything)
- [ ] Define "channel" — a coherent go-to-market motion (e.g.,
direct,partner-led,marketplace,reseller,oem). NOT a marketing source. - [ ] Confirm allocation methodology is the same across all channels (revenue-share or activity-driver, not mixed)
- [ ] Confirm retention numbers are per-channel, not pooled
- [ ] Confirm "channel-sourced" deals meet the strict definition: partner originated the opportunity AND brought it unqualified
- [ ] Identify your industry profile:
saas | api | enterprise-software | marketplace | hardware
---
Template 1 — Input for cost_to_serve_calculator.py
Run once per channel.
{
"channel_name": "partner-led-EMEA",
"deal_volume": 80,
"gross_revenue": 4000000,
"costs": {
"sdr_attribution": 60000,
"ae_attribution": 240000,
"sales_engineer_attribution": 90000,
"channel_manager_attribution": 180000,
"customer_success_attribution": 120000,
"support_attribution": 70000,
"marketing_attribution": 50000,
"partner_discount": 600000,
"partner_MDF": 80000,
"partner_enablement_time": 40000,
"certification_investment": 20000,
"channel_conflict_overhead": 15000,
"tooling_attribution": 25000,
"overhead_allocation_pct": 15.0
}
}Field-by-field guidance
| Field | What to put |
|---|---|
channel_name | Coherent GTM motion. Examples: direct, partner-led, marketplace, reseller-NA, oem. Naming matters — the optimizer recognizes direct and partner substrings for constraint enforcement. |
deal_volume | Closed-won deal count, trailing-twelve-months (TTM). |
gross_revenue | ARR (or annualized contracted revenue) closed in same TTM window. |
sdr_attribution | Loaded cost of SDR time on this channel. If 30% of SDR team works on this channel, allocate 30% of total SDR loaded cost. |
ae_attribution | Same logic for AE time. |
sales_engineer_attribution | SE / solution architect time. Frequently underestimated for partner-led — includes partner technical enablement. |
channel_manager_attribution | Loaded cost of channel-manager headcount. Direct channel = $0; partner channel = full loaded cost of channel team allocated by channel. Do not leave $0 for partner channels — the script flags it. |
customer_success_attribution | CS team allocation. |
support_attribution | Tier-1 / tier-2 support allocation. Partner-sourced customers often escalate to vendor faster — instrument support tickets by channel. |
marketing_attribution | Demand-gen, content, events allocated to this channel. |
partner_discount | Total $ given up in partner discount/margin for the TTM. |
partner_MDF | Market Development Funds disbursed. |
partner_enablement_time | Loaded $ of YOUR team's time spent on partner enablement. Frequently $0 in practice; should not be. |
certification_investment | Partner certification programs, training events, ongoing enablement spend. |
channel_conflict_overhead | Time/cost spent resolving deal conflicts between direct and channel teams. Industry: 5-8% of channel-team time. |
tooling_attribution | CRM seats, PRM (Partner Relationship Management) tools, channel-specific tooling. |
overhead_allocation_pct | Shared overhead allocated to this channel, as % of channel revenue. Must be consistent across channels. |
---
Template 2 — Input for channel_roi_analyzer.py
Run once across all channels.
{
"profile": "saas",
"channels": [
{
"channel": "direct",
"investment_ttm": {
"programs": 200000,
"headcount_cost": 1600000,
"partner_program_cost": 0,
"mdf": 0,
"tooling": 80000,
"training": 60000
},
"returns_ttm": {
"new_arr": 3800000,
"expansion_arr": 900000,
"retained_arr_attributable": 2400000
}
},
{
"channel": "partner-led",
"investment_ttm": {
"programs": 150000,
"headcount_cost": 360000,
"partner_program_cost": 280000,
"mdf": 120000,
"tooling": 30000,
"training": 80000
},
"returns_ttm": {
"new_arr": 1400000,
"expansion_arr": 200000,
"retained_arr_attributable": 900000
}
}
]
}Field guidance
| Field | What to put |
|---|---|
profile | One of saas, api, enterprise-software, marketplace, hardware. Tunes LTV multiplier and marginal-decay alpha. |
investment_ttm.programs | One-time program spend (events, content, campaigns). |
investment_ttm.headcount_cost | Loaded headcount cost dedicated to this channel. |
investment_ttm.partner_program_cost | Partner-program operating cost (PRM tooling, partner-portal infra, partner-only marketing). Distinct from MDF. |
investment_ttm.mdf | Market Development Funds. |
investment_ttm.tooling | Channel-specific tools. |
investment_ttm.training | Internal training + partner training cost. |
returns_ttm.new_arr | New ARR sourced by this channel, TTM. Strict definition: channel originated AND qualified. |
returns_ttm.expansion_arr | Expansion ARR from customers sourced by this channel. |
returns_ttm.retained_arr_attributable | Renewed ARR from customers sourced by this channel. |
---
Template 3 — Input for channel_mix_optimizer.py
Run once across all channels with constraints.
{
"profile": "saas",
"channels": [
{
"name": "direct",
"deal_count_ttm": 120,
"arr_ttm": 6000000,
"avg_deal_size": 50000,
"gross_margin_pct": 75,
"cac": 18000,
"sales_cycle_days": 75,
"retention_rate": 0.92,
"expansion_rate": 1.18,
"partner_discount_pct": 0
},
{
"name": "partner-led",
"deal_count_ttm": 80,
"arr_ttm": 4000000,
"avg_deal_size": 50000,
"gross_margin_pct": 75,
"cac": 10000,
"sales_cycle_days": 90,
"retention_rate": 0.86,
"expansion_rate": 1.08,
"partner_discount_pct": 20
}
],
"constraints": {
"min_direct_pct": 30,
"max_partner_concentration_pct": 50
}
}Field guidance
| Field | What to put |
|---|---|
name | Channel name. Use direct / partner substrings for constraint enforcement to work. |
gross_margin_pct | Use the true gross margin from cost_to_serve_calculator.py output, not the headline number. |
cac | Fully loaded CAC. Includes the channel-specific costs from the cost-to-serve calculator. |
retention_rate | Per-channel retention rate, not pooled. Critical input. |
expansion_rate | Net expansion (1.0 = flat, 1.20 = 120% NRR). |
partner_discount_pct | The discount % given up at sale (0 for direct channels). |
constraints.min_direct_pct | Floor on direct-channel share (e.g., 30 = "at least 30% of investment must go to direct"). |
constraints.max_partner_concentration_pct | Ceiling on any single partner channel (e.g., 50 = "no single partner channel may exceed 50%"). |
---
After filling
1. Save each template as a JSON file (e.g., channel-cts-partner.json, channel-roi.json, channel-mix.json) 2. Run in sequence:
python scripts/cost_to_serve_calculator.py --input channel-cts-partner.json --output markdown > out-cts-partner.md
python scripts/channel_roi_analyzer.py --input channel-roi.json --profile saas --output markdown > out-roi.md
python scripts/channel_mix_optimizer.py --input channel-mix.json --profile saas --output markdown > out-mix.md3. Bring all three reports to the quarterly channel review.
Channel Anti-Patterns
The eight anti-patterns this skill is built to detect, with citations. Most channel-economics decisions fail because of these patterns, not because the math is wrong.
---
1. Channel-led deals from your own pipeline = direct cost + partner cut
Pattern: Your AE sources an account, qualifies it, runs discovery, scopes the solution — and then a partner gets attached at the contract stage for the partner cut. The deal closes, is reported as "channel-sourced", and the partner gets margin.
Why it kills: You paid full direct cost (AE time, SE time, marketing) AND gave away partner margin. The deal looks profitable as "channel-led" but is value-destroying in reality.
Detection: require first-touch attribution in CRM. If the first-touch is internal but the deal closes as channel-sourced, flag it.
Source: Forrester Research, The Channel-Influence vs. Channel-Source Gap, 2019. Industry data: 25-40% of "channel-sourced" deals are actually channel-influenced direct deals.
---
2. No overhead allocation = false partner-margin lift
Pattern: Partner channel reports 75% gross margin while direct reports 60%. Look closer: direct channel gets 25% overhead allocation; partner channel gets 5% "because the partner handles overhead." The partner does not, in fact, handle overhead — your channel manager, partner program, MDF, and certification are all in YOUR P&L.
Why it kills: Apparent partner-margin lift drives over-investment in partner program. When the executive team eventually does honest allocation, partner margin collapses 8-15 points.
Detection: validate overhead-% is consistent across channels. If partner overhead allocation is <50% of direct, flag for review.
Source: Tomasz Tunguz, The Hidden Costs of Channel Programs, tomtunguz.com analyses 2021-2023. See also Horngren on allocation consistency.
---
3. Ignoring enablement time as cost
Pattern: Your AE spends 4 hours/week on partner co-selling, your SE spends 6 hours/week on partner technical enablement, your CS team handles tier-2 support that partners offload. None of this is loaded into channel cost.
Why it kills: Partner enablement time is often 15-30% of total channel cost, completely unattributed. The channel looks far more efficient than it is.
Detection: cost_to_serve_calculator.py flags partner_enablement_time and certification_investment when left at $0.
Source: Jay McBain (Canalys), State of the Channel research; Joe Hessling, Partner Program ROI Studies (channeltivity.com). Industry data: time-tracked enablement attribution increases partner channel cost by 15-30% over naive accounting.
---
4. MDF without ROI tracking
Pattern: Market Development Funds disbursed to partners without an attributable pipeline ROI. Partners take the MDF, deliver an event or campaign of dubious value, and no pipeline is traceable to the spend.
Why it kills: MDF without attribution is just a partner discount in disguise — and undisciplined. Industry-median MDF-to-pipeline ratio is 3.5:1; best-in-class is >7:1. If yours is <3:1 (or untracked), you have an unbudgeted discount line.
Detection: require MDF requests to commit to attributable pipeline targets BEFORE disbursement. Reconcile quarterly.
Source: Jay McBain (Canalys), MDF discipline research. SiriusDecisions (now Forrester) MDF benchmarks: 60% of MDF spend has no attributable pipeline tracking at all.
---
5. Channel-mix dogma ("we don't sell direct") blocks profitable segments
Pattern: A founder or CRO has a strong belief — "we're a partner-first company", "we don't sell direct in SMB", "we never sell direct in EMEA" — that overrides the segment-level economics. Profitable segments get starved because the strategy slogan doesn't allow direct motion there.
Why it kills: Mix should follow the math. Industry data shows dogmatic single-channel strategies forfeit 15-25% of TAM in mid-market specifically.
Detection: force the explicit articulation of the dogma in the planning conversation. "What's the segment we DON'T sell into, and why?"
Source: MIT Sloan Management Review, When Channel Conflict Means Growth, Frazier & Lassar (1996, updated 2019). Also: HBR on channel-conflict mismanagement, Cespedes (2014).
---
6. Treating influenced as sourced
Pattern: Partner is involved somewhere in a deal cycle — sometimes only at signature — and the deal is reported as "channel-sourced." Influence and source get conflated.
Why it kills: Inflates partner contribution by 25-40%. Drives mis-allocation of channel investment. Channel-program ROI becomes uninterpretable.
Detection: require strict first-touch + qualified-source criteria. Channel-sourced = partner originated the opportunity AND brought it to your team unqualified.
Source: SiriusDecisions (now Forrester), Channel Attribution Models, 2018-2022 research. Single most-cited source-vs-influence taxonomy in B2B SaaS.
---
7. No cost-attribution for channel-manager headcount
Pattern: Channel manager salary ($150-$250k loaded) is bucketed under "G&A" or "Sales Overhead" rather than attributed to the channel they manage. The channel reports better economics because its biggest cost line is hidden.
Why it kills: A $200k channel manager managing $4M of partner ARR is $50 of channel-manager cost per $1k ARR — material to the channel verdict. Hiding it is the most common single-line distortion in channel economics.
Detection: cost_to_serve_calculator.py flags channel_manager_attribution at $0 as a hidden-cost line.
Source: Gartner, Service Delivery Cost Allocation in Multi-Channel Technology Vendors, 2022. McKinsey CTS research.
---
8. Channel ROI computed without retention differential
Pattern: Channel ROI calculation uses pooled retention assumption (e.g., 90% across all channels) when in fact partner-sourced customers retain at 84% and direct-sourced retain at 92%. LTV calculation is inflated for the partner channel.
Why it kills: A 5-point retention gap moves LTV by 30-50%. Most channel investment decisions are made on LTV, so the wrong retention assumption produces the wrong investment decision.
Detection: require per-channel retention as mandatory input. channel_mix_optimizer.py will not compute effective LTV without a per-channel retention number.
Source: David Skok (For Entrepreneurs — SaaS Metrics 2.0). LTV = (ARPA × Gross Margin) / Churn — channel-blind churn is the most common source of false channel ROI.
---
Bonus anti-pattern: the "we'll figure out attribution later" trap
Pattern: Channel program launches without an attribution model. Six quarters later, no one can answer "did this work?" because the data was never structured.
Why it kills: Attribution must be designed at program-launch, not retrofit. Retroactive attribution is always contested.
Detection: force the attribution model to be in writing BEFORE the channel program is launched.
Source: HBR, Why Channel Programs Fail (Cespedes, 2014). Also: Tomasz Tunguz on channel-trap analyses.
---
How this skill detects the anti-patterns
| Anti-pattern | Detection mechanism |
|---|---|
| 1. Channel-led from own pipeline | Forcing question #3 (influence vs. source) |
| 2. No overhead allocation | cost_to_serve_calculator.py warns on inconsistent overhead-% |
| 3. Ignoring enablement time | Hidden-cost flag on partner_enablement_time |
| 4. MDF without ROI | Forcing question #5 (MDF ratio) |
| 5. Mix dogma | Forcing question #6 |
| 6. Influenced as sourced | Forcing question #3 |
| 7. No channel-manager attribution | Hidden-cost flag on channel_manager_attribution |
| 8. No retention differential | Forcing question #2; mandatory per-channel input |
Channel Economics Canon
The authoritative reference set for direct-vs-partner economics, channel ROI computation, and channel-mix decision-making. Use this when validating the assumptions inside cost_to_serve_calculator.py, channel_roi_analyzer.py, and channel_mix_optimizer.py.
---
1. David Skok — For Entrepreneurs: SaaS Metrics 2.0
Skok's framework gives the LTV / CAC equation the industry treats as canonical:
- LTV = (ARPA × Gross Margin %) / Churn Rate
- LTV / CAC ≥ 3.0 is the floor for sustainable channel investment
- CAC Payback ≤ 12 months is the SaaS target (longer for enterprise)
The channel-economics application: per-channel LTV/CAC and per-channel payback, never pooled. Pooled metrics hide the fact that one channel is funding another.
Source: forentrepreneurs.com — SaaS Metrics 2.0 — A Guide to Measuring and Improving What Matters (2014, updated 2018).
---
2. Bessemer Venture Partners — State of the Cloud (annual)
BVP's annual benchmark report is the single most-cited source for channel mix and CAC benchmarks across public + private SaaS:
- Public SaaS gross margins cluster 70-80%; partner-led channels typically run 5-10pts lower after load
- Sales efficiency (Magic Number) ≥ 0.7 is the funding bar; channel inefficiency drags this below the bar fastest
- Partner-led companies that scale past $100M ARR almost universally have <40% partner concentration — single-partner risk dominates above this line
Source: Bessemer Venture Partners, State of the Cloud report series, 2014-2024 editions.
---
3. Tomasz Tunguz — Channel CAC analyses
Tunguz's blog has the most rigorous public series on channel CAC and the diminishing-returns curve specifically. Key findings replicated across cohorts:
- Marginal CAC rises non-linearly with investment scale. The first $1M in channel program returns ~3x; the next $1M returns ~1.5x; the next $1M often <1.0x.
- Average ROI is a vanity metric. Investment decisions must be made on marginal ROI.
- Channel programs that "work on paper" but fail in practice usually fail because the team funded them past the marginal-ROI inflection point without realizing it.
Source: tomtunguz.com — channel CAC posts including The Channel CAC Premium, Diminishing Returns in SaaS Sales.
---
4. Pacific Crest / KeyBanc Capital Markets — Annual SaaS Survey
The Pacific Crest survey (continued by KeyBanc) is the longest-running channel-economics benchmark — 350+ private SaaS companies surveyed annually since 2008. The channel-specific findings used in this skill:
- Median direct CAC payback: 14 months. Partner-led: 11 months (lower nominal but understates loaded cost).
- Channel-led companies with <70% true (loaded) gross margin in partner channel materially underperform direct-led peers on Rule of 40
- Mixed-motion companies (40-60% direct, balance partner) outperform single-motion peers on growth efficiency by ~15-20%
Source: KeyBanc Capital Markets, SaaS Survey annual report (most recent 2024).
---
5. Madhavan Ramanujam — Monetizing Innovation — channel chapter
Ramanujam's channel chapter introduces the "value-flow" framework:
- Every channel splits economic value between vendor, partner, and customer
- The partner-cut must be earned by partner-delivered value (lead gen, technical sale, implementation, support) — not granted by program-tier convention
- Channels where the partner-cut exceeds the value the partner delivers are economic transfers, not channel programs
Source: Madhavan Ramanujam and Georg Tacke, Monetizing Innovation (Wiley, 2016) — Chapter 8 on channel & pricing alignment.
---
6. Jay McBain (Canalys) — Channel research
McBain is the most-cited channel analyst working today. The Canalys research the skill draws on:
- MDF discipline. Industry median MDF-to-attributable-pipeline ratio is 3.5:1; best-in-class >7:1. Anything below 3:1 is undisciplined.
- Influence vs. source. Channel-influenced ≠ channel-sourced. Industry conflation overstates partner contribution by 25-40% on average.
- Channel-conflict overhead is a real and measurable cost; mature channel programs allocate 5-8% of channel-team time to conflict resolution and surface it as a P&L line.
Source: Canalys research notes by Jay McBain (formerly Forrester), 2020-2024 — see also McBain's LinkedIn newsletter Channel Insights.
---
7. KeyBanc + OpenView — Joint Channel Maturity Benchmark
Joint research between KeyBanc Capital Markets and OpenView Partners (2022-2024) establishing the channel maturity scale used in this skill's verdict logic:
- Stage 1 (Discovery): channel < 15% of revenue, <2x LTV/CAC — DEFUND or EXIT verdict
- Stage 2 (Scale): channel 15-35% of revenue, 2-3x LTV/CAC — MAINTAIN verdict
- Stage 3 (Optimization): channel 35-50% of revenue, 3-5x LTV/CAC — DOUBLE-DOWN verdict candidate
- Stage 4 (Mature): channel >50%, but check single-partner concentration — risk verdict
Source: OpenView Partners + KeyBanc Capital Markets, Channel Maturity Benchmark 2023.
---
How this skill uses the canon
- `channel_roi_analyzer.py` verdict thresholds derive from Skok (LTV/CAC ≥ 3.0 floor) and BVP cash-ROI target ranges
- `channel_mix_optimizer.py` payback targets per profile follow KeyBanc/Pacific Crest survey medians
- Diminishing-returns curve in the marginal-ROI computation traces directly to Tunguz's channel-CAC posts
- Influence-vs-source discipline in the forcing-question library comes from McBain (Canalys) and SiriusDecisions
When the user's data contradicts these benchmarks, the data wins — these are reference anchors, not rules.
Cost-to-Serve Canon
The authoritative reference set for fully-loaded cost-to-serve methodology. Use this when validating cost categories, allocation methodology, and the "hidden costs" cost_to_serve_calculator.py surfaces.
The core principle across every source below: without consistent overhead allocation, every cross-channel margin comparison is contaminated.
---
1. Robert Kaplan & Robin Cooper — Measure Costs Right: Make the Right Decisions (HBR, 1988)
The foundational paper for Activity-Based Costing (ABC). Kaplan & Cooper observed that traditional cost-allocation methods systematically distort channel and product margins:
- High-volume, low-complexity channels appear unprofitable under traditional allocation (they over-absorb overhead)
- Low-volume, high-complexity channels appear profitable (they under-absorb)
- The fix: allocate overhead by activity driver, not by revenue share
For channel economics: partner-led channels typically appear higher-margin under naïve allocation precisely because they're lower-volume + higher-complexity. ABC corrects this.
Source: Kaplan, R.S. & Cooper, R., Measure Costs Right: Make the Right Decisions, Harvard Business Review, September-October 1988.
---
2. Charles Horngren — Cost Accounting: A Managerial Emphasis
The canonical textbook (now in 16th edition, Pearson). The chapters this skill draws on:
- Chapter 14 (Cost allocation): the rule of allocation consistency — same methodology, same denominator, every comparable segment. Inconsistent allocation invalidates downstream comparison.
- Chapter 15 (Customer-profitability analysis): the channel-economics application — customer (and channel) profitability is a function of both revenue and fully-loaded cost-to-serve, never just gross margin.
The most common channel-economics error this textbook anchors: allocating overhead at 25% to direct and 5% to partner "because the partner handles the overhead." The partner does not, in fact, handle the channel manager, the partner program, the certification, the MDF, the conflict resolution — all of which sit in YOUR P&L.
Source: Horngren, Datar & Rajan, Cost Accounting: A Managerial Emphasis, 16th ed., Pearson.
---
3. Jeremy Hope — Beyond Budgeting + channel-allocation writings
Hope's Beyond Budgeting movement contributed the framework for rolling channel-cost allocation rather than annual fixed allocation. Key principle:
- Channel cost allocation must update at the same cadence as channel investment decisions (quarterly minimum)
- Annual fixed allocations lock in last year's channel mix and prevent learning
- Use rolling 4-quarter cost-to-serve for forward decisions
Source: Hope, J. & Fraser, R., Beyond Budgeting (Harvard Business School Press, 2003); BBRT (Beyond Budgeting Round Table) channel-allocation guidance papers.
---
4. IBM Cost-to-Serve transformation case studies
IBM Institute for Business Value has published a sequence of cost-to-serve transformation case studies (2010-2022). Findings replicated across cases:
- 5-15% of "gross margin" at large enterprises evaporates when partner-channel overhead is loaded honestly
- The single largest unattributed cost is technical-sale resource time (sales engineering / solution architects co-selling with partners)
- Companies that move from naive to ABC-style channel allocation typically defund 1-2 channels within 6 months — and grow the remaining channels faster
Source: IBM Institute for Business Value, Cost-to-Serve Transformation case study series.
---
5. McKinsey & Company — Cost-to-Serve research
McKinsey's go-to-market practice publishes regular CTS research. The findings this skill leans on:
- Customer-level CTS variance within a single channel is often 5-10x — meaning a channel-average CTS hides material per-customer variance
- The hidden-cost line items most teams omit, in order of impact: technical-sale time, channel-manager attribution, partner enablement time, certification investment, conflict-resolution overhead
- McKinsey's recommended cadence: refresh CTS quarterly minimum, annually at the customer level, continuously for top-decile accounts
Source: McKinsey & Company, Cost-to-Serve: Reducing complexity and increasing profitability (operations practice white papers).
---
6. Gartner — Service Delivery Cost research
Gartner's research on service-delivery cost allocation, particularly for technology vendors with mixed direct + partner motion:
- The service-delivery overhead (customer success, support, professional services) often differs by 30-50% between direct-sourced and partner-sourced customers
- Reasons: partner-sourced customers often arrive less qualified, requiring more onboarding; partner-sourced customers expand less, reducing CS leverage; partner-sourced customers escalate to vendor support faster because the partner offloads tier-2 support back
- Gartner's recommendation: instrument support-ticket-volume-per-customer by sourcing channel, not by customer size
Source: Gartner, Service Delivery Cost Allocation in Multi-Channel Technology Vendors research notes, 2021-2024.
---
7. Boston Consulting Group — Channel allocation methodology
BCG's channel-allocation methodology (from their TMT and software practices) introduces the dual-axis cost framework this skill implements:
- Direct costs: incurred specifically because of this channel (channel manager headcount, MDF, partner discount, certification spend)
- Allocated overhead: shared costs apportioned by activity driver (revenue share, deal count, or time-tracked attribution)
- The two must always be reported separately so executives can see the lever they control directly
This is the framework cost_to_serve_calculator.py enforces by breaking out direct cost lines from allocated overhead — and validating overhead-% consistency across channels.
Source: BCG, Channel Economics in Software & Subscription Businesses practitioner publications.
---
How this skill uses the canon
- Direct-cost line items in
cost_to_serve_calculator.pyfollow BCG's dual-axis framework - Hidden-cost surfacing (the
HIDDEN_COST_KEYSlist flagged when $0) follows McKinsey's most-forgotten-cost ranking - Allocation consistency validation (warns when partner channel has <5% overhead while direct has >20%) implements Horngren's allocation-consistency rule
- Per-channel retention differential (used in
channel_roi_analyzer.py) follows Gartner's service-delivery findings — channel-blind retention is the most common source of wrong channel ROI
#!/usr/bin/env python3
"""channel_mix_optimizer.py
Computes per-channel effective LTV, payback period, and efficiency ratio
(LTV/CAC), then recommends a channel mix that maximizes effective ARR
subject to constraints (min direct %, max partner concentration %).
Includes a sensitivity table: what happens if direct CAC rises 20%, partner
discount widens 5 points, or retention drops 3 points?
Stdlib-only. Deterministic. No external solver — uses a discrete grid search
over feasible mixes, which is sufficient for 2-6 channel problems.
Usage:
python channel_mix_optimizer.py --sample
python channel_mix_optimizer.py --input mix.json --profile saas --output markdown
"""
from __future__ import annotations
import argparse
import json
import sys
from typing import Any
# Industry profiles tune assumed gross-margin-to-monthly conversion and
# benchmark payback targets (months).
PROFILES = {
"saas": {"payback_target_months": 12, "ltv_cac_floor": 3.0},
"api": {"payback_target_months": 9, "ltv_cac_floor": 4.0},
"enterprise-software": {"payback_target_months": 18, "ltv_cac_floor": 3.0},
"marketplace": {"payback_target_months": 6, "ltv_cac_floor": 2.5},
"hardware": {"payback_target_months": 24, "ltv_cac_floor": 2.0},
}
def _num(v: Any, default: float = 0.0) -> float:
try:
return float(v)
except (TypeError, ValueError):
return default
def compute_channel_metrics(ch: dict, profile_cfg: dict) -> dict:
name = ch.get("name", "unnamed")
deal_count = _num(ch.get("deal_count_ttm"))
arr_ttm = _num(ch.get("arr_ttm"))
avg_deal = _num(ch.get("avg_deal_size"))
gm_pct = _num(ch.get("gross_margin_pct"), 70.0)
cac = _num(ch.get("cac"))
cycle_days = _num(ch.get("sales_cycle_days"), 60)
retention = _num(ch.get("retention_rate"), 0.85)
expansion = _num(ch.get("expansion_rate"), 1.05)
partner_discount = _num(ch.get("partner_discount_pct"), 0)
if avg_deal <= 0 or cac <= 0:
return {"name": name, "error": "avg_deal_size and cac must both be > 0"}
# Effective margin after partner discount
effective_margin_pct = gm_pct * (1.0 - partner_discount / 100.0)
# Effective LTV — geometric-series approximation:
# LTV = avg_deal * (effective_margin/100) * expansion / (1 - retention)
# If retention >= 1.0, cap denominator at 0.05 to avoid blowup (means
# "indefinite retention" — we don't reward unrealistically).
denom = max(1.0 - retention, 0.05)
effective_ltv = avg_deal * (effective_margin_pct / 100.0) * expansion / denom
# Payback period: months to recoup CAC at monthly gross margin
monthly_gross_margin = (avg_deal / 12.0) * (effective_margin_pct / 100.0)
payback_months = cac / monthly_gross_margin if monthly_gross_margin > 0 else float("inf")
# Efficiency ratio
ltv_cac = effective_ltv / cac if cac > 0 else 0.0
return {
"name": name,
"deal_count_ttm": deal_count,
"arr_ttm": arr_ttm,
"avg_deal_size": avg_deal,
"gross_margin_pct": gm_pct,
"effective_margin_pct": round(effective_margin_pct, 2),
"cac": cac,
"sales_cycle_days": cycle_days,
"retention_rate": retention,
"expansion_rate": expansion,
"partner_discount_pct": partner_discount,
"effective_ltv": round(effective_ltv, 2),
"payback_months": round(payback_months, 2),
"ltv_cac": round(ltv_cac, 2),
"meets_payback_target": payback_months <= profile_cfg["payback_target_months"],
"meets_ltv_cac_floor": ltv_cac >= profile_cfg["ltv_cac_floor"],
}
def _is_partner_channel(name: str) -> bool:
n = name.lower()
return any(tag in n for tag in ("partner", "reseller", "channel", "oem", "marketplace"))
def _is_direct_channel(name: str) -> bool:
return "direct" in name.lower() or "inside" in name.lower() or "outbound" in name.lower()
def optimize_mix(metrics: list, constraints: dict) -> dict:
"""Discrete grid search over channel-mix percentages (5% increments)."""
n = len(metrics)
if n == 0:
return {"error": "no channels provided"}
min_direct = _num(constraints.get("min_direct_pct"), 0)
max_partner_conc = _num(constraints.get("max_partner_concentration_pct"), 100)
# Score = effective_ltv / cac (use LTV/CAC as the per-$-CAC efficiency).
# We allocate a normalized 100 "investment units" across channels and maximize
# sum(units_i * ltv_cac_i) subject to constraints.
best_score = -1.0
best_mix = None
step = 5
# generate compositions of 100 over n channels in 5% steps
def gen(remaining: int, slots: int):
if slots == 1:
yield (remaining,)
return
for v in range(0, remaining + 1, step):
for tail in gen(remaining - v, slots - 1):
yield (v,) + tail
for mix in gen(100, n):
# constraint checks
direct_share = sum(mix[i] for i, m in enumerate(metrics) if _is_direct_channel(m["name"]))
partner_share_max = max(
(mix[i] for i, m in enumerate(metrics) if _is_partner_channel(m["name"])),
default=0,
)
if direct_share < min_direct:
continue
if partner_share_max > max_partner_conc:
continue
score = sum(mix[i] * metrics[i].get("ltv_cac", 0) for i in range(n))
if score > best_score:
best_score = score
best_mix = mix
if best_mix is None:
return {"error": "no feasible mix under given constraints"}
return {
"best_mix_pct": {metrics[i]["name"]: best_mix[i] for i in range(n)},
"score": round(best_score, 2),
}
def sensitivity_scenarios(channels: list, profile_cfg: dict, constraints: dict) -> list:
"""Re-run optimization under perturbed inputs."""
scenarios = []
def perturb(perturbation_fn, label: str):
perturbed = []
for c in channels:
cc = dict(c)
perturbation_fn(cc)
perturbed.append(cc)
ms = [compute_channel_metrics(c, profile_cfg) for c in perturbed]
ms = [m for m in ms if "error" not in m]
opt = optimize_mix(ms, constraints)
scenarios.append({"scenario": label, "mix": opt.get("best_mix_pct"), "note": opt.get("error")})
def bump_direct_cac(c):
if _is_direct_channel(c.get("name", "")):
c["cac"] = _num(c.get("cac")) * 1.20
def widen_partner_discount(c):
if _is_partner_channel(c.get("name", "")):
c["partner_discount_pct"] = _num(c.get("partner_discount_pct")) + 5
def drop_retention(c):
c["retention_rate"] = max(0.0, _num(c.get("retention_rate"), 0.85) - 0.03)
perturb(bump_direct_cac, "Direct CAC +20%")
perturb(widen_partner_discount, "Partner discount +5pts")
perturb(drop_retention, "All retention -3pts")
return scenarios
def render_markdown(report: dict, profile: str) -> str:
lines = [
f"# Channel Mix Optimization — profile: `{profile}`",
"",
"## Per-channel economics",
"| Channel | Avg deal | Eff margin | CAC | Payback (mo) | LTV | LTV/CAC | Meets bar? |",
"|---|---:|---:|---:|---:|---:|---:|---|",
]
for m in report["metrics"]:
if "error" in m:
lines.append(f"| {m['name']} | — | — | — | — | — | — | ERROR: {m['error']} |")
continue
bar = (
"PASS"
if m["meets_payback_target"] and m["meets_ltv_cac_floor"]
else ("PARTIAL" if m["meets_payback_target"] or m["meets_ltv_cac_floor"] else "FAIL")
)
lines.append(
f"| {m['name']} | ${m['avg_deal_size']:,.0f} | {m['effective_margin_pct']:.1f}% | "
f"${m['cac']:,.0f} | {m['payback_months']:.1f} | ${m['effective_ltv']:,.0f} | "
f"{m['ltv_cac']:.2f}x | {bar} |"
)
lines.append("")
if "best_mix" in report and report["best_mix"].get("best_mix_pct"):
lines += ["## Recommended mix (subject to constraints)", "| Channel | Recommended share |", "|---|---:|"]
for k, v in report["best_mix"]["best_mix_pct"].items():
lines.append(f"| {k} | {v}% |")
lines.append("")
elif "best_mix" in report and report["best_mix"].get("error"):
lines += [f"## Mix optimization", f"**{report['best_mix']['error']}**", ""]
if report.get("sensitivity"):
lines += ["## Sensitivity scenarios", "| Scenario | Recommended mix |", "|---|---|"]
for s in report["sensitivity"]:
if s.get("mix"):
mix_str = ", ".join(f"{k}: {v}%" for k, v in s["mix"].items())
lines.append(f"| {s['scenario']} | {mix_str} |")
else:
lines.append(f"| {s['scenario']} | {s.get('note') or 'no feasible mix'} |")
lines.append("")
lines += [
"## Notes",
f"- Profile `{profile}` payback target: "
f"{PROFILES[profile]['payback_target_months']} months; LTV/CAC floor: "
f"{PROFILES[profile]['ltv_cac_floor']:.1f}x.",
"- Optimizer maximizes effective-ARR-weighted LTV/CAC across channels, in 5% steps.",
"- Constraint floors / ceilings are HARD constraints — infeasible mixes are reported as errors.",
]
return "\n".join(lines)
SAMPLE = {
"profile": "saas",
"channels": [
{
"name": "direct",
"deal_count_ttm": 120,
"arr_ttm": 6_000_000,
"avg_deal_size": 50_000,
"gross_margin_pct": 75,
"cac": 18_000,
"sales_cycle_days": 75,
"retention_rate": 0.92,
"expansion_rate": 1.18,
"partner_discount_pct": 0,
},
{
"name": "partner-led",
"deal_count_ttm": 80,
"arr_ttm": 4_000_000,
"avg_deal_size": 50_000,
"gross_margin_pct": 75,
"cac": 10_000,
"sales_cycle_days": 90,
"retention_rate": 0.86,
"expansion_rate": 1.08,
"partner_discount_pct": 20,
},
{
"name": "marketplace",
"deal_count_ttm": 200,
"arr_ttm": 1_000_000,
"avg_deal_size": 5_000,
"gross_margin_pct": 70,
"cac": 1_500,
"sales_cycle_days": 14,
"retention_rate": 0.78,
"expansion_rate": 1.02,
"partner_discount_pct": 15,
},
],
"constraints": {"min_direct_pct": 30, "max_partner_concentration_pct": 50},
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--input")
ap.add_argument("--output", choices=["json", "markdown"], default="markdown")
ap.add_argument(
"--profile",
choices=list(PROFILES.keys()),
default="saas",
)
ap.add_argument("--sample", action="store_true")
args = ap.parse_args()
if args.sample:
payload = SAMPLE
elif args.input:
with open(args.input) as f:
payload = json.load(f)
else:
ap.print_help()
return 0
profile = payload.get("profile", args.profile)
if profile not in PROFILES:
print(f"Unknown profile: {profile}", file=sys.stderr)
return 2
profile_cfg = PROFILES[profile]
channels = payload.get("channels", [])
constraints = payload.get("constraints", {}) or {}
metrics = [compute_channel_metrics(c, profile_cfg) for c in channels]
valid_metrics = [m for m in metrics if "error" not in m]
best = optimize_mix(valid_metrics, constraints)
sens = sensitivity_scenarios(channels, profile_cfg, constraints) if channels else []
report = {"profile": profile, "metrics": metrics, "best_mix": best, "sensitivity": sens}
if args.output == "json":
print(json.dumps(report, indent=2))
else:
print(render_markdown(report, profile))
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""channel_roi_analyzer.py
Computes per-channel ROI under three lenses:
- Cash ROI (year-1 returns / cash invested)
- LTV ROI (returns * LTV multiplier / investment)
- Marginal ROI (next dollar of investment, diminishing-returns curve)
Emits a verdict per channel: DOUBLE-DOWN / MAINTAIN / DEFUND / EXIT, plus
the diminishing-returns inflection point.
Stdlib-only. Deterministic.
Usage:
python channel_roi_analyzer.py --sample
python channel_roi_analyzer.py --input roi.json --profile saas --output markdown
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from typing import Any
# ---- Industry profiles: LTV multiplier benchmark, marginal-decay shape ----
# LTV multiplier = expected LTV / year-1 ARR (post-retention + expansion). Profile
# values are conservative midpoints from public benchmarks.
# marginal_decay_alpha = exponent k in marginal_roi = avg_roi * exp(-k * scale_idx)
# higher k = faster diminishing returns.
PROFILES = {
"saas": {"ltv_multiplier": 3.5, "marginal_decay_alpha": 0.35, "cash_roi_target": 1.0},
"api": {"ltv_multiplier": 4.5, "marginal_decay_alpha": 0.30, "cash_roi_target": 0.8},
"enterprise-software": {
"ltv_multiplier": 5.0,
"marginal_decay_alpha": 0.25,
"cash_roi_target": 0.6,
},
"marketplace": {
"ltv_multiplier": 2.5,
"marginal_decay_alpha": 0.45,
"cash_roi_target": 1.2,
},
"hardware": {
"ltv_multiplier": 1.8,
"marginal_decay_alpha": 0.50,
"cash_roi_target": 1.5,
},
}
def _num(v: Any, default: float = 0.0) -> float:
try:
return float(v)
except (TypeError, ValueError):
return default
def compute_channel_roi(channel: dict, profile_cfg: dict) -> dict:
name = channel.get("channel", "unnamed")
inv = channel.get("investment_ttm", {}) or {}
ret = channel.get("returns_ttm", {}) or {}
invested = sum(
_num(inv.get(k))
for k in ("programs", "headcount_cost", "partner_program_cost", "mdf", "tooling", "training")
)
new_arr = _num(ret.get("new_arr"))
exp_arr = _num(ret.get("expansion_arr"))
retained_arr = _num(ret.get("retained_arr_attributable"))
returns_y1 = new_arr + exp_arr + retained_arr
if invested <= 0:
return {"channel": name, "error": "investment_ttm sum must be > 0"}
# Cash ROI (year-1)
cash_roi = returns_y1 / invested
# LTV ROI — apply profile multiplier to recurring portion (new + expansion). Retained
# is already recurring so we don't double-count.
ltv_returns = (new_arr + exp_arr) * profile_cfg["ltv_multiplier"] + retained_arr
ltv_roi = ltv_returns / invested
# Marginal ROI — diminishing returns. Model: marginal = avg * exp(-alpha * scale_idx)
# where scale_idx is log10(invested / 100k) clamped >= 0. Inflection = scale at which
# marginal_roi drops to 1.0 (a dollar in returns a dollar — no profit).
alpha = profile_cfg["marginal_decay_alpha"]
scale_idx = max(0.0, math.log10(max(invested, 1.0) / 100_000.0))
marginal_roi = cash_roi * math.exp(-alpha * scale_idx)
# Inflection: solve cash_roi * exp(-alpha * x) = 1.0 -> x = ln(cash_roi)/alpha
if cash_roi > 1.0:
inflection_scale = math.log(cash_roi) / alpha
inflection_invested = 100_000.0 * (10 ** inflection_scale)
else:
inflection_invested = invested # already past the inflection
# Verdict logic — deterministic
target = profile_cfg["cash_roi_target"]
if cash_roi >= target * 1.5 and ltv_roi >= 3.0 and marginal_roi >= 1.0:
verdict = "DOUBLE-DOWN"
rationale = (
"Cash ROI > 1.5x target, LTV ROI ≥ 3.0x, marginal ROI > 1.0 — "
"next dollar still earns positive return. Invest more."
)
elif cash_roi >= target and ltv_roi >= 2.0:
verdict = "MAINTAIN"
rationale = (
"Cash ROI meets target and LTV ROI ≥ 2.0x. Hold current investment; "
"monitor marginal ROI before increasing."
)
elif cash_roi >= target * 0.5 or ltv_roi >= 1.5:
verdict = "DEFUND"
rationale = (
"Sub-target cash ROI. LTV ROI may be supportive but not enough to justify "
"current spend. Cut investment 30-50% and reassess in 2 quarters."
)
else:
verdict = "EXIT"
rationale = (
"Both cash ROI and LTV ROI below floor. Channel is value-destroying at "
"current load. Exit or restructure the program."
)
return {
"channel": name,
"invested_ttm": round(invested, 2),
"returns_y1": round(returns_y1, 2),
"cash_roi": round(cash_roi, 3),
"ltv_roi": round(ltv_roi, 3),
"marginal_roi": round(marginal_roi, 3),
"inflection_invested": round(inflection_invested, 2),
"verdict": verdict,
"rationale": rationale,
"profile_target_cash_roi": target,
}
def render_markdown(results: list, profile: str) -> str:
lines = [
f"# Channel ROI Analysis — profile: `{profile}`",
"",
"## Per-channel verdicts",
"| Channel | Invested | Returns Y1 | Cash ROI | LTV ROI | Marginal ROI | Inflection | Verdict |",
"|---|---:|---:|---:|---:|---:|---:|---|",
]
for r in results:
if "error" in r:
lines.append(f"| {r['channel']} | — | — | — | — | — | — | ERROR: {r['error']} |")
continue
lines.append(
f"| {r['channel']} | ${r['invested_ttm']:,.0f} | ${r['returns_y1']:,.0f} | "
f"{r['cash_roi']:.2f}x | {r['ltv_roi']:.2f}x | {r['marginal_roi']:.2f}x | "
f"${r['inflection_invested']:,.0f} | **{r['verdict']}** |"
)
lines += ["", "## Verdict rationale"]
for r in results:
if "error" in r:
continue
lines += [f"### {r['channel']} — {r['verdict']}", r["rationale"], ""]
lines += [
"## Definitions",
"- **Cash ROI** = year-1 returns / cash invested. Profile target shown above.",
"- **LTV ROI** = (new+expansion ARR × LTV multiplier + retained ARR) / invested.",
"- **Marginal ROI** = ROI on the next dollar of investment, modeled via "
"`avg_roi × exp(-alpha × log10(invested / $100k))`. Profile-tuned alpha.",
"- **Inflection** = invested-$ level at which marginal ROI hits 1.0 (break-even on "
"the next dollar). Beyond this point, additional spend destroys value.",
]
return "\n".join(lines)
SAMPLE = {
"profile": "saas",
"channels": [
{
"channel": "direct",
"investment_ttm": {
"programs": 200_000,
"headcount_cost": 1_600_000,
"partner_program_cost": 0,
"mdf": 0,
"tooling": 80_000,
"training": 60_000,
},
"returns_ttm": {
"new_arr": 3_800_000,
"expansion_arr": 900_000,
"retained_arr_attributable": 2_400_000,
},
},
{
"channel": "partner-led",
"investment_ttm": {
"programs": 150_000,
"headcount_cost": 360_000,
"partner_program_cost": 280_000,
"mdf": 120_000,
"tooling": 30_000,
"training": 80_000,
},
"returns_ttm": {
"new_arr": 1_400_000,
"expansion_arr": 200_000,
"retained_arr_attributable": 900_000,
},
},
{
"channel": "marketplace",
"investment_ttm": {
"programs": 60_000,
"headcount_cost": 120_000,
"partner_program_cost": 0,
"mdf": 0,
"tooling": 40_000,
"training": 0,
},
"returns_ttm": {
"new_arr": 200_000,
"expansion_arr": 40_000,
"retained_arr_attributable": 80_000,
},
},
],
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--input", help="Path to JSON input file")
ap.add_argument("--output", choices=["json", "markdown"], default="markdown")
ap.add_argument(
"--profile",
choices=list(PROFILES.keys()),
default="saas",
help="Industry profile (tunes LTV multiplier + marginal-decay alpha)",
)
ap.add_argument("--sample", action="store_true")
args = ap.parse_args()
if args.sample:
payload = SAMPLE
elif args.input:
with open(args.input) as f:
payload = json.load(f)
else:
ap.print_help()
return 0
profile = payload.get("profile", args.profile)
if profile not in PROFILES:
print(f"Unknown profile: {profile}", file=sys.stderr)
return 2
profile_cfg = PROFILES[profile]
channels = payload.get("channels", [])
if not channels and "channel" in payload:
channels = [payload]
results = [compute_channel_roi(c, profile_cfg) for c in channels]
if args.output == "json":
print(json.dumps({"profile": profile, "results": results}, indent=2))
else:
print(render_markdown(results, profile))
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""cost_to_serve_calculator.py
Computes fully-loaded cost-to-serve per deal AND per dollar of ARR for a
single channel. Breaks out direct vs. allocated overhead. Surfaces "hidden"
costs the average team forgets (partner enablement time, certification
investment, channel-conflict overhead) by flagging line items left at $0.
Stdlib-only. Deterministic.
Usage:
python cost_to_serve_calculator.py --sample
python cost_to_serve_calculator.py --input channel.json --output markdown
"""
from __future__ import annotations
import argparse
import json
import sys
from typing import Any
# ---- Hidden-cost line items (most-forgotten) -----------------------------
HIDDEN_COST_KEYS = {
"partner_enablement_time": "Partner enablement time (AE/SE hours co-selling)",
"certification_investment": "Partner certification + training investment",
"channel_conflict_overhead": "Channel-conflict resolution overhead",
"channel_manager_attribution": "Channel manager headcount attribution",
}
# ---- Cost categories -----------------------------------------------------
DIRECT_COST_KEYS = [
"sdr_attribution",
"ae_attribution",
"sales_engineer_attribution",
"channel_manager_attribution",
"customer_success_attribution",
"support_attribution",
"marketing_attribution",
"partner_discount",
"partner_MDF",
"partner_enablement_time",
"certification_investment",
"channel_conflict_overhead",
"tooling_attribution",
]
def _num(v: Any, default: float = 0.0) -> float:
try:
return float(v)
except (TypeError, ValueError):
return default
def compute_cost_to_serve(payload: dict) -> dict:
channel_name = payload.get("channel_name", "unnamed-channel")
deal_volume = _num(payload.get("deal_volume"), 0)
gross_revenue = _num(payload.get("gross_revenue"), 0)
costs = payload.get("costs", {}) or {}
if deal_volume <= 0 or gross_revenue <= 0:
return {
"error": "deal_volume and gross_revenue must both be > 0",
"channel_name": channel_name,
}
# Direct costs (sum)
direct_total = 0.0
direct_breakdown = {}
for key in DIRECT_COST_KEYS:
v = _num(costs.get(key), 0)
direct_breakdown[key] = v
direct_total += v
# Allocated overhead — applied as % of gross revenue
overhead_pct = _num(costs.get("overhead_allocation_pct"), 0)
if overhead_pct < 0 or overhead_pct > 100:
return {
"error": f"overhead_allocation_pct must be 0..100, got {overhead_pct}",
"channel_name": channel_name,
}
overhead_total = gross_revenue * (overhead_pct / 100.0)
total_loaded_cost = direct_total + overhead_total
cost_per_deal = total_loaded_cost / deal_volume
cost_per_arr_dollar = total_loaded_cost / gross_revenue
true_gross_margin_pct = (1.0 - cost_per_arr_dollar) * 100.0
# Hidden-cost surfacing — flag any HIDDEN_COST_KEYS that are $0
hidden_flags = []
for k, label in HIDDEN_COST_KEYS.items():
if direct_breakdown.get(k, 0) == 0:
hidden_flags.append(
f"'{k}' is $0 — likely understated. {label} is the most-forgotten "
"channel cost in industry benchmarks."
)
# Double-counting validation
warnings = []
if (
direct_breakdown.get("partner_discount", 0) > 0
and direct_breakdown.get("partner_MDF", 0) > 0
and direct_breakdown.get("partner_MDF", 0) > direct_breakdown.get("partner_discount", 0)
):
warnings.append(
"MDF spend exceeds partner discount — verify MDF is not double-counted "
"as discount in your channel agreements."
)
if overhead_pct > 50:
warnings.append(
f"Overhead allocation of {overhead_pct:.1f}% is unusually high. "
"Verify denominator (revenue vs. gross profit) is consistent across channels."
)
if overhead_pct < 5 and "partner" in channel_name.lower():
warnings.append(
f"Partner channel overhead allocation of {overhead_pct:.1f}% is unusually low. "
"Channel manager, partner program, certification all live in YOUR P&L. "
"Inconsistent allocation is the #1 source of false partner-margin lift."
)
return {
"channel_name": channel_name,
"deal_volume": deal_volume,
"gross_revenue": gross_revenue,
"direct_breakdown": direct_breakdown,
"direct_total": round(direct_total, 2),
"overhead_allocation_pct": overhead_pct,
"overhead_total": round(overhead_total, 2),
"total_loaded_cost": round(total_loaded_cost, 2),
"cost_per_deal": round(cost_per_deal, 2),
"cost_per_arr_dollar": round(cost_per_arr_dollar, 4),
"true_gross_margin_pct": round(true_gross_margin_pct, 2),
"hidden_cost_flags": hidden_flags,
"warnings": warnings,
}
def render_markdown(r: dict) -> str:
if "error" in r:
return f"# Cost-to-Serve\n\n**ERROR**: {r['error']}\n"
lines = [
f"# Cost-to-Serve — {r['channel_name']}",
"",
"## Inputs",
f"- Deal volume (TTM): **{r['deal_volume']:,.0f}**",
f"- Gross revenue (TTM): **${r['gross_revenue']:,.0f}**",
f"- Overhead allocation: **{r['overhead_allocation_pct']:.1f}%**",
"",
"## Direct cost breakdown",
"| Line item | $ |",
"|---|---:|",
]
for k, v in r["direct_breakdown"].items():
lines.append(f"| {k} | {v:,.0f} |")
lines += [
f"| **Direct total** | **{r['direct_total']:,.0f}** |",
f"| Allocated overhead | {r['overhead_total']:,.0f} |",
f"| **Total loaded cost** | **{r['total_loaded_cost']:,.0f}** |",
"",
"## Result",
f"- Cost-to-serve **per deal**: **${r['cost_per_deal']:,.2f}**",
f"- Cost-to-serve **per $ ARR**: **${r['cost_per_arr_dollar']:.4f}**",
f"- **True gross margin** (after channel-specific load): **{r['true_gross_margin_pct']:.2f}%**",
"",
]
if r["hidden_cost_flags"]:
lines.append("## Hidden-cost flags")
for f in r["hidden_cost_flags"]:
lines.append(f"- {f}")
lines.append("")
if r["warnings"]:
lines.append("## Warnings")
for w in r["warnings"]:
lines.append(f"- {w}")
lines.append("")
return "\n".join(lines)
SAMPLE = {
"channel_name": "partner-led-EMEA",
"deal_volume": 80,
"gross_revenue": 4_000_000,
"costs": {
"sdr_attribution": 60_000,
"ae_attribution": 240_000,
"sales_engineer_attribution": 90_000,
"channel_manager_attribution": 180_000,
"customer_success_attribution": 120_000,
"support_attribution": 70_000,
"marketing_attribution": 50_000,
"partner_discount": 600_000,
"partner_MDF": 80_000,
"partner_enablement_time": 40_000,
"certification_investment": 20_000,
"channel_conflict_overhead": 15_000,
"tooling_attribution": 25_000,
"overhead_allocation_pct": 15.0,
},
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--input", help="Path to JSON input file")
ap.add_argument("--output", choices=["json", "markdown"], default="markdown")
ap.add_argument("--sample", action="store_true", help="Run with embedded sample")
args = ap.parse_args()
if args.sample:
payload = SAMPLE
elif args.input:
with open(args.input) as f:
payload = json.load(f)
else:
ap.print_help()
return 0
result = compute_cost_to_serve(payload)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(render_markdown(result))
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
FAQ
What does channel-economics output?
Fully-loaded cost-to-serve per channel, three-lens ROI (cash, LTV-adjusted, marginal), per-channel DOUBLE-DOWN/MAINTAIN/DEFUND/EXIT verdicts, and a sensitivity-tested mix recommendation.
Does it pick the strategy for me?
No. The skill recommends with deterministic verdict logic; humans commit the decision.