Cohort LTV Analyzer
Build monthly acquisition cohorts from an order export and report repeat rate, cumulative revenue and contribution-margin LTV per customer by month since first order, CAC payback per cohort, and which cohorts are tracking below a…
At a glance
Cohort LTV Analyzer is an AI skill that you can use in Claude Code, Codex, or type.com.
- Updated
See an example
Example output from a sample run. Company names and figures are sample data.
Cohort LTV and CAC payback: Kindling Coffee Co., cohorts 2026-01 to 2026-08 (as of 2026-09-01)
Only the 2026-01, 2026-02 and 2026-03 cohorts have paid back CAC. CAC climbed from 21.00 to 49.00 per customer while the three newest cohorts sit at 9.23 to 9.69 of contribution margin per customer. 2026-04 is -2.72 short of CAC at M3 and not paid back through M4.
| Cohort | Repeat % | CM/cust | CAC | CM − CAC | Payback |
|---|---|---|---|---|---|
| 2026-01 | 28.3% | 30.83 | 21.00 | 9.83 | M0 |
| 2026-02 | 39.7% | 33.97 | 22.50 | 11.47 | M0 |
| 2026-03 | 34.7% | 27.48 | 24.00 | 3.48 | M1 |
| 2026-04 | 30.5% | 23.78 | 26.50 | -2.72 | none thru M4 |
| 2026-05 | 30.0% | 28.11 | 29.00 | -0.89 | none thru M3 |
| 2026-06 | 15.2% | 9.69 | 41.00 | -31.31 | none thru M2 (immature) |
| 2026-07 | 13.3% | 9.23 | 46.00 | -36.77 | none thru M1 (immature) |
| 2026-08 | 0.0% | 9.61 | 49.00 | -39.40 | none thru M0 (immature) |
Repeat % and CM/cust are cumulative through M3, or through the last fully elapsed month for immature cohorts. Contribution margin deducts COGS, shipping cost, payment fees (2.9% + 0.30 per order) and 3.25 fulfillment per order. Marketing spend is compared as CAC, not deducted.
Cohorts trending below reference (2026-01: CM/cust 30.83 at M3, repeat 28.3%)
- 2026-03: CM/cust 27.48 (-10.9%) despite a higher repeat rate of 34.7%.
- 2026-04: CM/cust 23.78 (-22.9%), repeat 30.5%.
- Immature cohorts (2026-06 to 2026-08) are excluded from the comparison until they have lived through M3.
- With finance/revenue_definitions.json in the working folder, the skill switches to finance's timezone, FX and refund-date rules and lists every excluded test, cancelled, voided, replacement, zero-value and internal order.
Inputs: order export (927 orders, 730 customers, 2026-01-01 to 2026-08-31), per-cohort marketing spend file, M3 horizon, 3.25 fulfillment cost per order.
Produced by running bash examples/run.sh on the sample data bundled with this skill.
How to use Cohort LTV Analyzer
- type.com: Choose Use Skill to add Cohort LTV Analyzer to your type.com workspace.
- Claude Code: Download the skill and extract it into
~/.claude/skills/cohort-ltv-analyzer/(or.claude/skills/cohort-ltv-analyzer/in a project). Then ask Claude to use Cohort LTV Analyzer. - Codex: Download the skill and extract it into
~/.agents/skills/cohort-ltv-analyzer/(or.agents/skills/cohort-ltv-analyzer/in a repository). Then ask Codex to use Cohort LTV Analyzer.
AI agents can read this page as Markdown or fetch the raw SKILL.md.
Frequently asked questions
What is Cohort LTV Analyzer?
Cohort LTV Analyzer is an AI skill that you can use in Claude Code, Codex, or type.com.
What does Cohort LTV Analyzer do?
Build monthly acquisition cohorts from an order export and report repeat rate, cumulative revenue and contribution-margin LTV per customer by month since first order, CAC payback per cohort, and which cohorts are tracking below a…
Can I use Cohort LTV Analyzer in Claude Code or Codex?
Yes. Cohort LTV Analyzer is a standard Agent Skills folder with a SKILL.md file. Download it and extract it into ~/.claude/skills/ for Claude Code or ~/.agents/skills/ for Codex.
Who created Cohort LTV Analyzer?
The type.com team created Cohort LTV Analyzer. The type.com team reviews every skill before it is published in the Skills Library.
Related guide
Browse the technical files
--- name: cohort-ltv-analyzer description: Build monthly acquisition cohorts from an order export and report repeat rate, cumulative revenue and contribution-margin LTV per customer by month since first order, CAC payback per cohort, and which cohorts are tracking below a reference cohort at the same age. --- # Cohort LTV Analyzer Answers the question a blended LTV number cannot: *are the customers we are buying this quarter worth what the customers we bought in January were worth, at the same age?* It assigns every customer to the month of their first order, then measures each cohort at equal ages (M0, M1, M2 …) so a three-month-old cohort is never unfairly compared with an eight-month-old one. ## Before you run This skill ships scripts and sample data alongside this SKILL.md. Before running any command: 1. **Get the files.** Make sure the skill's other files (`scripts/`, `examples/` and anything else listed with this skill) are in your working folder at the same relative paths. Some environments load only SKILL.md; if yours did, fetch each file from this skill's published files and write it to the matching path. In Type, read them with the skill-file tools. Anywhere else, the Type Skills Library API lists every file with its path, content and `sha256`: GET `https://api.type.com/api/public/library/skills` and take the entry with slug `cohort-ltv-analyzer`. 2. **Check the copies are exact.** Compare each file's size in bytes, not characters (and its hash, where your tools report one), with the published version before running. A copy written out from the published file is fine once its byte size and hash match; never run a script you summarised or reconstructed from memory. 3. **Run from the skill's folder**, calling interpreters explicitly: `python3 scripts/…` and `bash examples/run.sh`. 4. **Try the sample first.** If the skill ships `examples/run.sh` and `examples/expected_output.txt`, run `bash examples/run.sh`; its output should match the expected file exactly. If it doesn't, stop and report the first differing line rather than running on real data. ## When to use this - CAC is rising and you need to know whether LTV is rising with it. - A finance or board question about payback period on paid acquisition. - After a channel shift (a discount-heavy campaign, a new marketplace, an influencer push) to see whether the new customers repeat like the old ones. - Before raising acquisition budget: which cohorts pay back, and by when. - Subscription and replenishment brands checking whether retention is decaying. ## Gathering the input Two local files, plus finance's definitions file if the business has one. The script never touches the network. **0. Finance's revenue definitions** (`finance/revenue_definitions.json`, optional). Finance owns this file; see `REVENUE_DEFINITIONS.md`. When it is in the working folder (or passed with `--definitions`), the skill takes the reporting timezone, reporting currency and FX rates file, net sales flags, refund date basis and excluded-order rules from it and prints its version at the top of the report. When it is absent, the first lines of the output say so and list the skill's own defaults; the numbers are exactly what earlier versions produced. Ask whether finance has this file before running, and never write one on finance's behalf. **1. Order export** (required). From the user's connected integrations: - **Shopify**: Orders export, or a report with one row per order. Use *net* product revenue (after discounts, before tax and shipping). Without a definitions file, exclude test and cancelled orders yourself. With one, keep them and include the `tags`, `financial_status` and `cancelled` columns so the skill excludes them under finance's rules and reports what it removed. Keep the `Created at` timestamp with its UTC offset and, if refunds should land when they were issued, a `refund_date` per refunded order. - **A data warehouse / GA4 BigQuery export / an ERP**: one row per order with the columns in `DATA_CONTRACT.md`. - **A 3PL or ERP** can supply `cogs`, `shipping_cost`, and fulfillment cost per order if the storefront export lacks them. The critical field is `customer_id` and it must be **stable across orders**. If guest checkouts create a new id per order, every customer looks single-purchase and the whole analysis is wrong — stitch on hashed email first and say so in the writeup. **2. Marketing spend by month** (optional but strongly recommended). Columns `cohort_month,marketing_spend`. Pull acquisition spend per calendar month from the ad platforms (Meta, Google Ads), Klaviyo/paid email if it is an acquisition channel, and any agency retainer allocated to acquisition. CAC per cohort is then `spend in that month / new customers acquired in that month`. If spend is unavailable, pass a single blended `--cac`. ## Running it ```bash python3 scripts/cohort_ltv.py \ --orders /path/to/orders.csv \ --spend /path/to/spend.csv \ --as-of 2026-09-01 \ --horizon 3 \ --fulfillment-cost 3.25 ``` Key arguments: - `--as-of YYYY-MM-DD` (required) is the only "today". A cohort month index is only shown once that month has fully elapsed as of this date, so partial months never masquerade as complete ones. - `--horizon N` is the cohort age at which cohorts are compared. Default 3 (a 90-day payback convention). Use 6 or 12 for considered-purchase brands. - `--cogs-pct N` is required only when the export has no `cogs` column. - `--payment-fee-pct` / `--payment-fee-fixed` default to 2.9% + 0.30, the common US card rate. Override for a different processor, or supply a `payment_fee` column and the flags are ignored. - `--restock-recovery-pct` defaults to 0: refunded goods are assumed unsellable. Set it to the real resale rate if returns go back into stock. - `--reference-cohort YYYY-MM` overrides the default reference (the oldest mature cohort). - `--tolerance-pct` (default 10) is how far below the reference a cohort must fall before it is flagged. - `--definitions PATH` points at finance's revenue definitions. Default: `finance/revenue_definitions.json` in the working folder, used if it exists. An explicit path that does not exist is an error. - `--json` emits the same analysis, including both triangles, as JSON, with `definitions` and `excludedOrders` keys first. Bad input exits 2 naming the file, row, and column. ## Reading and presenting the output First comes either **FINANCE DEFINITIONS** (file, version, owner, and how each rule was applied or why it could not be) or a **NOTE** that no definitions file was found, listing the defaults in use. Quote the version in the writeup; if it is the NOTE, say the figures are not on finance's definitions. Then five blocks, in this order (plus **Excluded orders** after the assumptions when a definitions file is in use: count and net_revenue per rule, and any rule that could not be checked because its column is missing): 1. **Assumptions** — every margin input, printed. Read it first; if the COGS or fee assumption is wrong, every number below is wrong. Put this in the writeup too, so nobody re-litigates the math later. 2. **Cohort summary** — customers, orders, AOV, repeat rate, revenue per customer, contribution margin per customer, CAC, and payback month. `obs` is the cohort's last fully elapsed month; rows marked `(immature)` have not reached the horizon and are excluded from comparison. A payback of `M0, lost M2` means the cohort reached CAC in M0 but later refunds (for example under a refund-date basis) or loss-making orders pulled it back below, and it is still below at its last observed month. 3. **Cumulative revenue per customer triangle** — the top line. Rising from left to right within a row is repeat purchasing. 4. **Cumulative contribution margin per customer triangle** — the same shape after product, shipping, fee, and refund costs. This is the number CAC has to be compared against, not revenue. 5. **Payback against CAC**, then **cohorts trending below reference**. How to present it: - Lead with payback: "January and February cohorts pay back in month 0; the June onward cohorts have not paid back and are at CAC $41–49 against $13 of contribution margin so far." - Distinguish a **mix problem** from a **retention problem**. If a recent cohort's M0 margin per customer is down, the first order got smaller or more discounted. If M0 holds but M1–M3 flatten, customers stopped coming back. The triangle tells you which; say which. - Immature cohorts are early signal, not a verdict. Say "M0 margin is down 38% versus the reference cohort" rather than "the June cohort is unprofitable" when June has only lived one month. - Convert to a decision: pause or reprice the channel that produced the weak cohort, or extend the acceptable payback window if cash allows. ## Limits - **Contribution margin, not net profit.** Overheads, salaries, software, warehousing rent, and agency fees are not modelled. A cohort that "pays back" covers its own variable costs and its acquisition cost, nothing more. - **CAC is blended per month**, spend divided by new customers acquired that month. It does not deduplicate organic from paid, does not model view-through or lagged attribution, and assigns none of the spend to repeat purchases of earlier cohorts. In a brand with heavy organic acquisition, real paid CAC is higher than this figure. - **No forecasting.** There is no curve fitting, no projected LTV, no survival model. Every cell is observed history. A cohort with no future months shown has no future months of data, not zero future value. - **Calendar months only.** Not weekly cohorts, not 30-day windows, not first-touch-channel cohorts. A customer's cohort is the month of their first order in the file — if the export does not go back far enough, genuinely old customers get mis-assigned to the first month of the file. Check the earliest order date before trusting the oldest cohort. - **One currency.** Without a definitions file, convert multi-currency exports before running. With one, rows in another currency are converted at the order date's rate from finance's rates file (the whole row, refund included: FX movement between sale and refund is not modelled). A missing rate stops the run naming the row. Spend, `--cac` and the cost flags must already be in the reporting currency. - **Refund timing follows finance.** Without a definitions file (or with `refunds.date_basis: order_date`), refunds are netted in the month of the original order. With `refund_date`, the refund and its cost recovery land in the month-since-first-order bucket of the refund date, so cumulative figures can fall in later months. `refunds.late_refund_days` is not used. - **Timezones.** Without a reporting timezone, `order_date` is read as its first 10 characters. With one, timestamps must carry a UTC offset; a timestamp without one stops the run rather than being guessed. - **Net sales flags change the revenue figures only.** Contribution margin is always net of discounts and refunds and includes shipping charged. Tax and gift-card flags cannot be applied (no such columns); the report says so. - **Excluded orders** are only as good as the `tags`, `financial_status` and `cancelled` columns. A rule whose column is missing is reported as not checked, and those orders must still be pre-filtered. An excluded order never makes its customer a customer: if a customer's only orders are excluded, they are in no cohort. - It cannot see subscriptions, exchanges, gift cards, or store credit as distinct events; they are only whatever the order export says they are.