"""CRM Price Index (October 2026): compute every figure the study quotes. Reads blog/content/data/crm_price_index_2026_10.json and prints the tables. No network access; the JSON is the only input. python scripts/studies/crm_price_index.py # all tables python scripts/studies/crm_price_index.py --json # computed results as JSON """ import json import math import os import statistics import sys ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) DATA = os.path.join(ROOT, "blog", "content", "data", "crm_price_index_2026_10.json") BILLINGS = ("monthly", "annual") TIERS = ("entry_tier", "growth_tier") def load(path=DATA): with open(path) as fh: return json.load(fh) def billable_seats(vendor, users): buckets = vendor.get("seat_buckets") if not buckets: return users for size in buckets: if users <= size: return size return users def plan_cost(vendor, plan, users, billing): """Monthly cost of one plan for `users` people, or None if not offered.""" price = plan.get(billing) if price is None: return None cap = plan.get("max_users") if cap is not None and users > cap: return None kind = plan["pricing"] if kind == "flat": return float(price) if kind == "per_seat": return float(price) * billable_seats(vendor, users) if kind == "base_plus_seats": extra = plan["extra_user_" + billing] return float(price) + max(0, users - plan["included_users"]) * extra raise ValueError(f"unknown pricing {kind}") def tier_cost(vendor, tier, users, billing): """Cheapest plan in the tier that serves `users`: (plan name, cost) or (None, None).""" best = (None, None) for name in vendor[tier]: cost = plan_cost(vendor, vendor["plans"][name], users, billing) if cost is not None and (best[1] is None or cost < best[1]): best = (name, cost) return best def annual_discount(plan): if plan.get("discount_unknown"): return None if plan.get("monthly") in (None, 0) or plan.get("annual") is None: return None return round((plan["monthly"] - plan["annual"]) / plan["monthly"] * 100, 1) def compute(data): sizes = data["team_sizes"] out = {"cells": [], "summary": {}, "discounts": [], "crossovers": [], "free_plans": []} for v in data["vendors"]: for tier in TIERS: for billing in BILLINGS: for n in sizes: plan, cost = tier_cost(v, tier, n, billing) out["cells"].append({ "vendor": v["name"], "tier": tier, "billing": billing, "users": n, "plan": plan, "monthly_cost": None if cost is None else round(cost, 2), "per_user": None if cost is None else round(cost / n, 2), }) for name, plan in v["plans"].items(): d = annual_discount(plan) if d is not None: out["discounts"].append({"vendor": v["name"], "plan": name, "pct": d}) if v.get("free_plan"): out["free_plans"].append({"vendor": v["name"], **v["free_plan"]}) for tier in TIERS: for billing in BILLINGS: for n in sizes: rows = [c for c in out["cells"] if c["tier"] == tier and c["billing"] == billing and c["users"] == n and c["monthly_cost"] is not None] rows.sort(key=lambda c: c["monthly_cost"]) costs = [c["monthly_cost"] for c in rows] others = [c for c in rows if c["vendor"] != "Scalry"] unavailable = sorted(c["vendor"] for c in out["cells"] if c["tier"] == tier and c["billing"] == billing and c["users"] == n and c["monthly_cost"] is None) out["summary"][f"{tier}|{billing}|{n}"] = { "priced": len(rows), "cheapest": (rows[0]["vendor"], costs[0]) if rows else None, "median": round(statistics.median(costs), 2) if rows else None, "most_expensive": (rows[-1]["vendor"], costs[-1]) if rows else None, "unavailable": unavailable, "cheapest_excl_scalry": (others[0]["vendor"], others[0]["monthly_cost"]) if others else None, "median_excl_scalry": round(statistics.median([c["monthly_cost"] for c in others]), 2) if others else None, } # Growth premium: growth-tier cost / entry-tier cost for 3 users, annual billing. out["growth_premium"] = [] for v in data["vendors"]: _, entry = tier_cost(v, "entry_tier", 3, "annual") _, growth = tier_cost(v, "growth_tier", 3, "annual") out["growth_premium"].append({"vendor": v["name"], "model": v["billing_model"], "ratio": round(growth / entry, 2)}) # Crossover: the smallest team at which each per-seat growth tier costs # more per month than each Scalry plan. scalry = next(v for v in data["vendors"] if v["name"] == "Scalry") for v in data["vendors"]: if v["billing_model"] != "per_seat": continue for billing in BILLINGS: for sname in ("Starter", "Pro", "Scale"): flat = scalry["plans"][sname][billing] cross = None for n in range(1, 201): _, cost = tier_cost(v, "growth_tier", n, billing) if cost is not None and cost > flat: cross = n break out["crossovers"].append({"vendor": v["name"], "billing": billing, "scalry_plan": sname, "scalry_price": flat, "users": cross}) return out def money(x): if x is None: return "n/a" return f"${x:,.0f}" if float(x).is_integer() else f"${x:,.2f}" def print_tables(data, res): sizes = data["team_sizes"] print(f"{data['study']} - compiled {data['compiled']} - {len(data['vendors'])} vendors\n") for tier in TIERS: for billing in BILLINGS: print(f"== {tier.replace('_', ' ')} / {billing} billing (USD per month) ==") print(f"{'Vendor':22}" + "".join(f"{str(n) + ' user(s)':>22}" for n in sizes)) for v in data["vendors"]: line = f"{v['name']:22}" for n in sizes: c = next(c for c in res["cells"] if c["vendor"] == v["name"] and c["tier"] == tier and c["billing"] == billing and c["users"] == n) cell = "n/a" if c["plan"] is None else f"{money(c['monthly_cost'])} {c['plan'][:9]}" line += f"{cell:>22}" print(line) for n in sizes: s = res["summary"][f"{tier}|{billing}|{n}"] print(f" {n:>2} users: priced {s['priced']}, cheapest {s['cheapest'][0]} " f"{money(s['cheapest'][1])}, median {money(s['median'])}, most expensive " f"{s['most_expensive'][0]} {money(s['most_expensive'][1])}" + (f", not available: {', '.join(s['unavailable'])}" if s["unavailable"] else "")) print(f" excluding Scalry: cheapest {s['cheapest_excl_scalry'][0]} " f"{money(s['cheapest_excl_scalry'][1])}, median {money(s['median_excl_scalry'])}") print() print("== Cost per user per month, growth tier, annual billing ==") for v in data["vendors"]: cells = [c for c in res["cells"] if c["vendor"] == v["name"] and c["tier"] == "growth_tier" and c["billing"] == "annual"] print(f"{v['name']:22}" + "".join(f"{money(c['per_user']):>12}" for c in cells)) print() print("== Growth tier / entry tier, 3 users, annual billing ==") for g in res["growth_premium"]: print(f"{g['vendor']:22}{g['ratio']:>6}x") seat = [g["ratio"] for g in res["growth_premium"] if g["model"] == "per_seat"] print(f" per-seat vendors: median {statistics.median(seat)}x, range {min(seat)}x to {max(seat)}x\n") print("== Annual-billing discount by plan ==") for d in sorted(res["discounts"], key=lambda d: -d["pct"]): print(f"{d['vendor']:22}{d['plan']:14}{d['pct']:>6}%") pcts = [d["pct"] for d in res["discounts"]] print(f" median {statistics.median(pcts)}%, range {min(pcts)}% to {max(pcts)}%, " f"{len(pcts)} plans\n") print("== Free plans ==") for f in res["free_plans"]: print(f"{f['vendor']:22} up to {f['max_users']} user(s): {f['note']}") print() print("== Crossover: smallest team where a per-seat growth tier costs more than Scalry ==") for c in res["crossovers"]: print(f"{c['vendor']:22}{c['billing']:9} vs Scalry {c['scalry_plan']:8}" f"({money(c['scalry_price'])}): {c['users']} users") print() print("== Crossover medians (per-seat vendors) ==") for billing in BILLINGS: for sname in ("Starter", "Pro", "Scale"): xs = [c["users"] for c in res["crossovers"] if c["billing"] == billing and c["scalry_plan"] == sname] print(f" {billing:8} vs Scalry {sname:8}: median {statistics.median(xs)} users, " f"range {min(xs)} to {max(xs)}") print() print("== Excluded ==") for e in data["excluded"]: print(f"{e['name']}: {e['reason']}") def main(argv): data = load() res = compute(data) if "--json" in argv: print(json.dumps(res, indent=2)) else: print_tables(data, res) if __name__ == "__main__": main(sys.argv[1:])