profitable 0.3.0 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f5435ab1dfe3d15abb2f92e6720a9ced3f3fb60474eaf85d407b85f8306632d
4
- data.tar.gz: adc0a485926ff124fcd19bbf8c3d48eed10ac850fa459fcc9f84d78a40cddc9f
3
+ metadata.gz: db964563468a5d97aba1c885e241da6c9938c01a4e5bc491448effdd492fcb82
4
+ data.tar.gz: d8cc7bc12503aaabf2572841632d3d83c629258aa8b7727233a72c6f48f2f5f1
5
5
  SHA512:
6
- metadata.gz: 1e8d5f63b3d11b0b146b9225da6f63b85353efff1f944cc59a1ad78107e75aab4daacbbed5c46f7896edbc401790fbf9001d9424ca0fdbbe70afb4c3b2d587a2
7
- data.tar.gz: f429f34975e2c7c48e0c16579ec675059c13d82e9c1634ebdc3e29d3a327be9ceec9662957da5a2214bc1a642aa9a23fb33be4919100b553bd3ad4112583a8c9
6
+ metadata.gz: bc7391cee28cb2e0b11c7e29583fb2651e4a35e34199f04c142322d2b7dad436f42b1523c3299b6f8a38988b376a753ef6ef7f5c5367213a262ea754b01148ef
7
+ data.tar.gz: '08d67a50ca5b2b9f20202b6fc77802e2abd8249aca8a670377080c2c37759c5175b4d886f8a0ebd7031eccf61e4eab730606491fd88d483008f46a0ccefb8da0'
data/.simplecov ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SimpleCov configuration file (auto-loaded before test suite)
4
+ # This keeps test_helper.rb clean and follows best practices
5
+
6
+ SimpleCov.start do
7
+ # Use SimpleFormatter for terminal-only output (no HTML generation)
8
+ formatter SimpleCov::Formatter::SimpleFormatter
9
+
10
+ # Track coverage for the lib directory (gem source code)
11
+ add_filter "/test/"
12
+
13
+ # Exclude Rails engine components that require integration testing
14
+ # These are tested via Appraisal with a full Rails app
15
+ add_filter "/lib/profitable/engine.rb"
16
+ add_filter "/app/"
17
+
18
+ # Exclude the main profitable.rb entry point - it loads the engine and
19
+ # defines the Profitable module. Our unit tests use a test-specific
20
+ # Profitable module (defined in test_helper.rb) to avoid Rails dependencies.
21
+ # The core logic is tested via the individual lib/profitable/*.rb files.
22
+ add_filter "/lib/profitable.rb"
23
+
24
+ # Track the lib directory (core gem logic)
25
+ track_files "lib/**/*.rb"
26
+
27
+ # Enable branch coverage for more detailed metrics
28
+ enable_coverage :branch
29
+
30
+ # Set minimum coverage threshold for the core calculation logic
31
+ # The Rails engine/controllers are tested separately via Appraisal
32
+ minimum_coverage line: 80, branch: 70
33
+
34
+ # Disambiguate parallel test runs
35
+ command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
36
+ end
37
+
38
+ # Print coverage summary to terminal after tests complete
39
+ SimpleCov.at_exit do
40
+ SimpleCov.result.format!
41
+ puts "\n" + "=" * 60
42
+ puts "COVERAGE SUMMARY"
43
+ puts "=" * 60
44
+ puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
45
+ puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
46
+ puts "=" * 60
47
+ end
data/AGENTS.md ADDED
@@ -0,0 +1,5 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
4
+
5
+ Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
data/Appraisals CHANGED
@@ -1,31 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Test minimum supported Rails version (with latest Pay)
4
+ appraise "rails-7.2" do
5
+ gem "rails", "~> 7.2.0"
6
+ gem "pay", "~> 11.0"
7
+ gem "stripe", "~> 18.0"
8
+ end
9
+
10
+ # Test latest Rails version (with latest Pay)
11
+ appraise "rails-8.1" do
12
+ gem "rails", "~> 8.1.0"
13
+ gem "pay", "~> 11.0"
14
+ gem "stripe", "~> 18.0"
15
+ end
16
+
3
17
  # Test against Pay 7.x (original minimum supported version)
4
18
  appraise "pay-7.3" do
5
19
  gem "pay", "~> 7.3.0"
6
20
  gem "stripe", "~> 12.0"
21
+ gem "rails", "~> 8.1.0"
7
22
  end
8
23
 
9
24
  # Test against Pay 8.x
10
25
  appraise "pay-8.3" do
11
26
  gem "pay", "~> 8.3.0"
12
27
  gem "stripe", "~> 13.0"
28
+ gem "rails", "~> 8.1.0"
13
29
  end
14
30
 
15
31
  # Test against Pay 9.x
16
32
  appraise "pay-9.0" do
17
33
  gem "pay", "~> 9.0.0"
18
34
  gem "stripe", "~> 13.0"
35
+ gem "rails", "~> 8.1.0"
19
36
  end
20
37
 
21
38
  # Test against Pay 10.x (newly supported version with object column)
22
39
  appraise "pay-10.0" do
23
40
  gem "pay", "~> 10.0.0"
24
41
  gem "stripe", "~> 15.0"
42
+ gem "rails", "~> 8.1.0"
25
43
  end
26
44
 
27
45
  # Test against Pay 11.x (latest version as of 2025)
28
46
  appraise "pay-11.0" do
29
47
  gem "pay", "~> 11.0"
30
48
  gem "stripe", "~> 18.0"
49
+ gem "rails", "~> 8.1.0"
31
50
  end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # `profitable`
2
2
 
3
+ ## [0.5.0] - 2026-03-19
4
+ - Add `Profitable.ttm_revenue` for trailing twelve-month revenue
5
+ - Add `Profitable.ttm` as a founder-friendly alias for `ttm_revenue`
6
+ - Add `Profitable.revenue_run_rate`, `estimated_arr_valuation`, `estimated_ttm_revenue_valuation`, and `estimated_revenue_run_rate_valuation`
7
+ - Make revenue metrics net of refunds when `amount_refunded` is present
8
+ - Make subscriber and MRR metrics distinguish between current billable subscriptions and historical period events
9
+ - Count `new_customers` from first monetization date rather than signup date
10
+ - Count `new_subscribers` / `new_mrr` from when a subscription becomes billable, not when a free trial starts
11
+ - Handle additional Pay status variants like `on_trial`, `cancelled`, and `deleted`
12
+ - Keep grace-period subscriptions billable until `ends_at`
13
+ - Exclude metered Stripe items from fixed run-rate MRR calculations
14
+ - Surface TTM revenue in the built-in dashboard
15
+ - Extract shared metric logic to `lib/profitable/metrics.rb` for cleaner architecture
16
+
17
+ ## [0.4.0] - 2026-02-10
18
+ - Add monthly summary (12mo) and daily summary (30d) tables to dashboard
19
+ - Add `period_data` method for efficient batch computation of period metrics
20
+ - Fix `new_mrr` counting incomplete/unpaid subscriptions (now only counts active)
21
+ - Fix `new_subscribers` not filtering out trialing/paused subscriptions
22
+ - DRY up period methods (churn, churned_customers, new_mrr, etc.) via `_in_period` delegation
23
+ - Optimize dashboard from ~176 to 38 queries (batch summary queries, precompute in controller)
24
+
3
25
  ## [0.3.0] - 2026-01-01
4
26
  - Add Pay v10+ support, comprehensive Minitest test suite, and 16 critical bugfixes re: wrong calculations
5
27
 
@@ -24,4 +46,4 @@
24
46
 
25
47
  ## [0.1.0] - 2024-08-29
26
48
 
27
- - Initial test release (not production ready)
49
+ - Initial test release (not production ready)
data/CLAUDE.md ADDED
@@ -0,0 +1,5 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # 💸 `profitable` - SaaS metrics for your Rails app
1
+ # 💸 `profitable` - MRR dashboard & SaaS metrics for your Rails app
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/profitable.svg)](https://badge.fury.io/rb/profitable)
3
+ [![Gem Version](https://badge.fury.io/rb/profitable.svg)](https://badge.fury.io/rb/profitable) [![Build Status](https://github.com/rameerez/profitable/workflows/Tests/badge.svg)](https://github.com/rameerez/profitable/actions)
4
4
 
5
- Calculate the MRR, ARR, churn, LTV, ARPU, total revenue & estimated valuation of your `pay`-powered Rails SaaS app, and display them in a simple dashboard.
5
+ > [!TIP]
6
+ > **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=profitable)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=profitable)!
7
+
8
+ `profitable` allows you to calculate the MRR, ARR, churn, LTV, ARPU, total revenue & estimated valuation of your `pay`-powered Rails SaaS app, and display them in a simple dashboard. It also provides handy methods you can use independently if you don't want the full dashboard.
6
9
 
7
10
  ![Profitable gem main dashboard](profitable.webp)
8
11
 
@@ -12,7 +15,7 @@ Calculate the MRR, ARR, churn, LTV, ARPU, total revenue & estimated valuation of
12
15
 
13
16
  Usually, you would look into your Stripe Dashboard or query the Stripe API to know your MRR / ARR / churn – but when you're using `pay`, you already have that data available and auto synced to your own database. So we can leverage it to make handy, composable ActiveRecord queries that you can reuse in any part of your Rails app (dashboards, internal pages, reports, status messages, etc.)
14
17
 
15
- Think doing something like: `"Your app is currently at $#{Profitable.mrr} MRR Estimated to be worth $#{Profitable.valuation_estimate("3x")} at a 3x valuation"`
18
+ Think doing something like: `"Current MRR: $#{Profitable.mrr}"` or `"Your app is worth $#{Profitable.valuation_estimate("3x")} at a 3x valuation"`
16
19
 
17
20
  ## Installation
18
21
 
@@ -25,6 +28,24 @@ Then run `bundle install`.
25
28
 
26
29
  Provided you have a valid [`pay`](https://github.com/pay-rails/pay) installation (`Pay::Customer`, `Pay::Subscription`, `Pay::Charge`, etc.) everything is already set up and you can just start using [`Profitable` methods](#main-profitable-methods) right away.
27
30
 
31
+ Current MRR processor coverage is verified for `stripe`, `braintree`, `paddle_billing`, and `paddle_classic`.
32
+
33
+ For Stripe, metered subscription items are intentionally excluded from fixed run-rate metrics like `mrr`, `arr`, `new_mrr`, and `churned_mrr`.
34
+
35
+ > [!IMPORTANT]
36
+ > `profitable` does **not** yet normalize MRR for every processor that `pay` supports.
37
+ > If a subscription comes from an unsupported processor such as `lemon_squeezy`, it will currently contribute `0` to processor-adapter-dependent metrics until an adapter is added.
38
+ >
39
+ > Verified processor-adapter coverage today:
40
+ > - `stripe`
41
+ > - `braintree`
42
+ > - `paddle_billing`
43
+ > - `paddle_classic`
44
+ >
45
+ > Metrics that depend on processor-specific subscription amount parsing include `mrr`, `arr`, `new_mrr`, `churned_mrr`, `mrr_growth`, `mrr_growth_rate`, `lifetime_value`, `time_to_next_mrr_milestone`, and MRR-derived fields in summaries.
46
+ >
47
+ > Metrics based primarily on `Pay::Charge` and generic subscription lifecycle fields are much more portable across processors, including `all_time_revenue`, `revenue_in_period`, `ttm_revenue`, `revenue_run_rate`, customer counts, subscriber counts, and churn calculations.
48
+
28
49
  ## Mount the `/profitable` dashboard
29
50
 
30
51
  `profitable` also provides a simple dashboard to see your main business metrics.
@@ -49,25 +70,31 @@ All methods return numbers that can be converted to a nicely-formatted, human-re
49
70
 
50
71
  ### Revenue metrics
51
72
 
52
- - `Profitable.mrr`: Monthly Recurring Revenue (MRR)
53
- - `Profitable.arr`: Annual Recurring Revenue (ARR)
54
- - `Profitable.all_time_revenue`: Total revenue since launch
55
- - `Profitable.revenue_in_period(in_the_last: 30.days)`: Total revenue (recurring and non-recurring) in the specified period
73
+ - `Profitable.mrr`: Monthly Recurring Revenue (MRR) from subscriptions that are billable right now
74
+ - `Profitable.arr`: Annual Recurring Revenue (ARR), calculated as current `mrr * 12`, not trailing revenue
75
+ - `Profitable.ttm`: Founder-friendly shorthand alias for `ttm_revenue`
76
+ - `Profitable.ttm_revenue`: Trailing twelve-month revenue, net of refunds when `amount_refunded` is present
77
+ - `Profitable.revenue_run_rate(in_the_last: 30.days)`: Recent revenue annualized (useful for TrustMRR-style revenue multiples)
78
+ - `Profitable.all_time_revenue`: Net revenue since launch
79
+ - `Profitable.revenue_in_period(in_the_last: 30.days)`: Net revenue (recurring and non-recurring) in the specified period
56
80
  - `Profitable.recurring_revenue_in_period(in_the_last: 30.days)`: Only recurring revenue in the specified period
57
81
  - `Profitable.recurring_revenue_percentage(in_the_last: 30.days)`: Percentage of revenue that is recurring in the specified period
58
- - `Profitable.new_mrr(in_the_last: 30.days)`: New MRR added in the specified period
82
+ - `Profitable.new_mrr(in_the_last: 30.days)`: Full MRR from subscriptions that first became billable in the specified period
59
83
  - `Profitable.churned_mrr(in_the_last: 30.days)`: MRR lost due to churn in the specified period
60
84
  - `Profitable.average_revenue_per_customer`: Average revenue per customer (ARPC)
61
85
  - `Profitable.lifetime_value`: Estimated customer lifetime value (LTV)
62
- - `Profitable.estimated_valuation(at: "3x")`: Estimated company valuation based on ARR
86
+ - `Profitable.estimated_valuation(at: "3x")`: ARR-based valuation heuristic
87
+ - `Profitable.estimated_arr_valuation(at: "3x")`: Explicit ARR-based valuation heuristic
88
+ - `Profitable.estimated_ttm_revenue_valuation(at: "2x")`: TTM revenue-based valuation heuristic
89
+ - `Profitable.estimated_revenue_run_rate_valuation(at: "2x", in_the_last: 30.days)`: Recent revenue run-rate valuation heuristic
63
90
 
64
91
  ### Customer metrics
65
92
 
66
- - `Profitable.total_customers`: Total number of customers who have ever made a purchase or had a subscription (current and past)
67
- - `Profitable.total_subscribers`: Total number of customers who have ever had a subscription (active or not)
68
- - `Profitable.active_subscribers`: Number of customers with currently active subscriptions
69
- - `Profitable.new_customers(in_the_last: 30.days)`: Number of new customers added in the specified period
70
- - `Profitable.new_subscribers(in_the_last: 30.days)`: Number of new subscribers added in the specified period
93
+ - `Profitable.total_customers`: Total number of customers who have ever monetized through a paid charge or a paid subscription state
94
+ - `Profitable.total_subscribers`: Total number of customers who have ever reached a paid subscription state (trial-only subscriptions do not count)
95
+ - `Profitable.active_subscribers`: Number of customers with subscriptions that are billable right now
96
+ - `Profitable.new_customers(in_the_last: 30.days)`: Number of first-time customers added in the period based on first monetization date, not signup date
97
+ - `Profitable.new_subscribers(in_the_last: 30.days)`: Number of customers whose subscriptions first became billable in the specified period
71
98
  - `Profitable.churned_customers(in_the_last: 30.days)`: Number of customers who churned in the specified period
72
99
 
73
100
  ### Other metrics
@@ -101,11 +128,24 @@ Profitable.churn(in_the_last: 3.months).to_readable # => "12%"
101
128
  Profitable.new_mrr(in_the_last: 24.hours).to_readable(2) # => "$123.45"
102
129
 
103
130
  # Get the estimated valuation at 5x ARR (defaults to 3x if no multiple is specified)
104
- Profitable.estimated_valuation(multiple: 5).to_readable # => "$500,000"
131
+ Profitable.estimated_arr_valuation(multiple: 5).to_readable # => "$500,000"
132
+
133
+ # Get trailing twelve-month revenue
134
+ Profitable.ttm_revenue.to_readable # => "$123,456"
105
135
 
106
- # You can also pass the multiplier as a string. You can also use the `at:` keyword argument (same thing as `multiplier:`) – and/or ignore the `at:` or `multiplier:` named arguments altogether
136
+ # Founder-friendly shorthand for trailing twelve-month revenue
137
+ Profitable.ttm.to_readable # => "$123,456"
138
+
139
+ # Get recent revenue annualized (useful for TrustMRR-style revenue multiples)
140
+ Profitable.revenue_run_rate(in_the_last: 30.days).to_readable # => "$96,000"
141
+
142
+ # `estimated_valuation` remains as a backwards-compatible alias of `estimated_arr_valuation`
107
143
  Profitable.estimated_valuation(at: "4.5x").to_readable # => "$450,000"
108
144
 
145
+ # Be explicit about the denominator when comparing marketplace comps
146
+ Profitable.estimated_ttm_revenue_valuation(2).to_readable
147
+ Profitable.estimated_revenue_run_rate_valuation(2.7, in_the_last: 30.days).to_readable
148
+
109
149
  # Get the time to next MRR milestone
110
150
  Profitable.time_to_next_mrr_milestone.to_readable # => "26 days left to $10,000 MRR"
111
151
  ```
@@ -126,11 +166,205 @@ For more precise calculations, you can access the raw numeric value:
126
166
  Profitable.mrr # => 123456
127
167
  ```
128
168
 
169
+ Revenue methods are net of refunds when `amount_refunded` is present on `pay_charges`.
170
+
129
171
  ### Notes on specific metrics
130
172
 
131
173
  - `mrr_growth_rate`: This calculation compares the MRR at the start and end of the specified period. It assumes a linear growth rate over the period, which may not reflect short-term fluctuations. For more accurate results, consider using shorter periods or implementing a more sophisticated growth calculation method if needed.
132
174
  - `time_to_next_mrr_milestone`: This estimation is based on the current MRR and the recent growth rate. It assumes a constant growth rate, which may not reflect real-world conditions. The calculation may be inaccurate for very new businesses or those with irregular growth patterns.
133
175
 
176
+ ## Metric guide: TTM, Revenue, Profit, ARR, and MRR
177
+
178
+ `profitable` exposes both standard recurring revenue metrics (`MRR`, `ARR`) and trailing actuals (`TTM revenue`) on purpose.
179
+
180
+ These metrics are related, but they are not interchangeable:
181
+
182
+ | Metric | What it means | Best for | What it is **not** |
183
+ | --- | --- | --- | --- |
184
+ | `MRR` | Monthly Recurring Revenue from subscriptions that are billable right now | Operating cadence, near-term momentum, tracking upgrades/downgrades | It's **not** monthly cash collected from all sources |
185
+ | `ARR` | Annual Recurring Revenue, calculated as the current recurring base annualized | Forecasting recurring scale, board/investor reporting, recurring-revenue quality | It's **not** a historical last-12-month revenue |
186
+ | `MRR * 12` | Simple annualization of the current monthly recurring base | Fast ARR approximation when the base is normalized monthly | It's **not** TTM revenue or TTM profit |
187
+ | `TTM revenue` | Actual revenue collected over the last 12 months | Buyer-facing historical actuals, smoothing seasonality, sanity-checking ARR | It's **not** a forward recurring run-rate |
188
+ | `TTM profit` | Actual profit over the last 12 months | Small bootstrapped SaaS exits, ROI-minded buyers, earnings-based multiples | It's **not** something `profitable` can derive from `pay` alone |
189
+
190
+ ### The distinction
191
+
192
+ - `ARR` is a run-rate metric. Stripe describes it as revenue you "expect to earn in a year" and notes that `ARR = MRR × 12`.
193
+ - `TTM` is a trailing metric. CFI defines it as the "most recent 12-month period" and uses it for reported actuals such as revenue and EBITDA.
194
+ - `TTM revenue` tells you what customers actually paid over the last year.
195
+ - `TTM profit` tells you what the business actually kept after costs. This is often what smaller acquisition buyers care about most, but it requires cost data outside `pay`.
196
+ - In acquire-style market reports, `TTM` can refer to **both** `TTM profit` and `TTM revenue` depending on the chart. The denominator must always be stated explicitly.
197
+ - In `profitable`, the shorthand method `ttm` is defined to mean `ttm_revenue` because the gem does not yet model costs or profit.
198
+
199
+ In other words:
200
+
201
+ - `ARR` answers: "What is my current recurring run-rate? What do I expect to earn in a year?"
202
+ - `TTM revenue` answers: "What did I actually collect over the last year?"
203
+
204
+ ### What `profitable` calculates
205
+
206
+ - `Profitable.mrr`: Monthly Recurring Revenue (MRR) from subscriptions that are billable right now
207
+ - `Profitable.arr`: Annual Recurring Revenue (ARR), calculated from current MRR
208
+ - `Profitable.ttm`: shorthand alias for `ttm_revenue`
209
+ - `Profitable.ttm_revenue`: trailing 12-month revenue, net of refunds when `amount_refunded` is present
210
+ - `Profitable.revenue_run_rate`: recent revenue annualized to a yearly run-rate
211
+ - `Profitable.estimated_valuation`: ARR-multiple heuristic
212
+ - `Profitable.estimated_ttm_revenue_valuation`: TTM revenue heuristic
213
+ - `Profitable.estimated_revenue_run_rate_valuation`: recent revenue run-rate heuristic
214
+
215
+ `profitable` does **not** calculate `TTM profit`, because payroll, contractor spend, hosting, support, software tools, taxes, and owner add-backs do not live inside `pay`.
216
+
217
+ ### Which metric matters in which situation?
218
+
219
+ - If you're operating the business week to week: `MRR` is usually the best pulse metric.
220
+ - If you want to understand your current subscription run-rate: `ARR` is the right metric.
221
+ - If you're preparing buyer materials for a bootstrapped SaaS exit: add `TTM revenue` and your own `TTM profit`.
222
+ - If your business has meaningful one-time revenue, services, setup fees, or seasonal swings: `TTM revenue` matters more than `ARR`.
223
+ - If you are speaking to serious SaaS buyers about revenue quality: pair `ARR` with churn, growth, concentration, and margins.
224
+
225
+ ### What the market says
226
+
227
+ These are short excerpts from current market and finance sources, followed by why they matter for `profitable`.
228
+
229
+ - [Acquire.com Biannual Multiples Report (Jan 2026)](https://blog.acquire.com/acquire-com-biannual-acquisition-multiples-report-jan-2026/): "anchor valuation on profit"
230
+ Acquire says the January 2026 report is focused "entirely on profit multiples," which is highly relevant for smaller bootstrapped SaaS exits.
231
+ In the same report, some visual breakdowns segment businesses by `TTM revenue` bands, so it is important not to assume one bare `TTM` label means the same thing everywhere.
232
+
233
+ - [Acquire.com Biannual Multiples Report, January 2024](https://blog.acquire.com/wp-content/uploads/2024/01/Acquire-Biannual-Multiples-Report-Jan-2024.pdf): "4.3x TTM profit"
234
+ The earlier report gives a concrete historical benchmark for how these profit multiples looked on the marketplace.
235
+
236
+ - [Acquire.com 2025 webinar recap](https://blog.acquire.com/the-secrets-behind-2024s-biggest-exits-webinar-recap/): "$100k-$1M in TTM revenue"
237
+ Acquire says that cohort averaged `4.35x`, which is a useful live-market anchor for micro-SaaS exits.
238
+
239
+ - [Acquire.com SaaS valuation multiples guide](https://blog.acquire.com/saas-valuation-multiples/): "5x to 15x ARR"
240
+ Stronger recurring SaaS businesses are also routinely discussed in ARR-multiple terms, especially when growth and retention are strong.
241
+
242
+ - [Stripe on ARR](https://stripe.com/us/resources/more/acv-vs-arr-what-each-metric-really-means-and-when-they-matter): "ARR = £50,000 x 12 = £600,000"
243
+ This is the clearest shorthand for why `ARR` is a run-rate, not a trailing actual.
244
+
245
+ - [CFI on TTM](https://corporatefinanceinstitute.com/resources/valuation/railing-twelve-months-ttm-definition/): "most recent 12-month period"
246
+ This is why `ttm_revenue` belongs beside `arr`: it measures trailing actuals, not a projection.
247
+
248
+ - [Quiet Light on selling SaaS](https://quietlight.com/sell-your-saas-business-for-the-best-price/): "EBITDA or SDE"
249
+ Quiet Light explicitly says smaller SaaS businesses are often valued on earnings, not revenue, which is why `TTM profit` matters.
250
+
251
+ - [Quiet Light on larger SaaS](https://quietlight.com/sell-your-saas-business-for-the-best-price/): "ARR of $1M or more"
252
+ The same source says larger SaaS businesses may qualify for revenue multiples, which is why `ARR` becomes more important as the business scales.
253
+
254
+ - [Software Equity Group, 3Q25 SaaS M&A](https://softwareequity.com/blog/saas-ma-deal-volume-and-valuations): "5.4x"
255
+ SEG reported average SaaS M&A valuations of `5.4x` revenue in 3Q25, which is useful context for larger, more institutional software transactions.
256
+
257
+ - [TrustMRR live listing example](https://trustmrr.com/startup/appalchemy): "$164,819 TTM revenue"
258
+ Live marketplaces increasingly show `TTM revenue`, `TTM profit`, and `ARR` side by side, which matches how buyers actually compare deals.
259
+
260
+ - [TrustMRR FAQ](https://trustmrr.com/faq): "asking price divided by annualized revenue"
261
+ TrustMRR explicitly defines its marketplace multiple as asking price divided by `last 30 days revenue × 12`, so its multiple is a revenue run-rate multiple, not an ARR multiple.
262
+
263
+ - [TrustMRR FAQ](https://trustmrr.com/faq): "Only aggregate revenue metrics"
264
+ TrustMRR says it only pulls revenue-level aggregates from payment providers, which is another reason its native multiple is revenue-based rather than profit-based.
265
+
266
+ - [TrustMRR FAQ](https://trustmrr.com/faq): "profit margin for the last 30 days"
267
+ TrustMRR asks sellers to provide profit margin separately when listing for sale, which reinforces that profit-based heuristics need cost inputs outside the payment provider.
268
+
269
+ ### How to use these metrics responsibly
270
+
271
+ - `estimated_valuation` is intentionally simple. It is kept as a backwards-compatible ARR heuristic. Prefer `estimated_arr_valuation` in new code when you want the denominator to be explicit.
272
+ - Do not compare an ARR multiple and a TTM profit multiple as if they were the same kind of number. They are based on different denominators.
273
+ - A `4x TTM profit` deal, a `2x TTM revenue` deal, and an `8x ARR` deal can all describe reasonable SaaS outcomes in different buyer segments.
274
+ - If two businesses both have `$300k ARR`, the one with lower churn, better margins, lower concentration, and cleaner growth usually deserves the higher multiple.
275
+ - If two businesses both have `$300k TTM revenue`, the one with stronger profit and more recurring revenue usually deserves the higher price.
276
+
277
+ ### Typical multiples by SaaS type and size
278
+
279
+ These are rough, source-backed heuristics. They are not interchangeable.
280
+
281
+ | SaaS profile | Common denominator | Rough multiple | Source |
282
+ | --- | --- | --- | --- |
283
+ | Smaller profitable SaaS on Acquire.com (2024-2025 confirmed transactions) | `TTM profit` | `3.9x` median | [Acquire.com Jan 2026 report](https://blog.acquire.com/acquire-com-biannual-acquisition-multiples-report-jan-2026/) |
284
+ | Micro-SaaS under `$100k` TTM revenue | `TTM profit` | `3.55x` average | [Acquire.com webinar recap](https://blog.acquire.com/the-secrets-behind-2024s-biggest-exits-webinar-recap/) |
285
+ | Micro-SaaS with `$100k-$1M` TTM revenue | `TTM profit` | `4.35x` average | [Acquire.com webinar recap](https://blog.acquire.com/the-secrets-behind-2024s-biggest-exits-webinar-recap/) |
286
+ | TrustMRR marketplace listings | `Annualized last 30d revenue` | often roughly `0.6x-5.5x` ask multiples | [TrustMRR homepage snapshot](https://trustmrr.com/) |
287
+ | Mid-6-figure ARR SaaS | `TTM revenue` | `2x-4x` revenue | [Acquire.com founder-driven acquisition recap](https://blog.acquire.com/how-founders-can-drive-their-own-acquisition-process-webinar-recap/) |
288
+ | Older Acquire.com SaaS baseline | `TTM revenue` or `TTM profit` | `2-3x revenue` or `5x profit` | [Acquire.com 7-8 figures webinar recap](https://blog.acquire.com/how-to-sell-your-company-playbook-webinar/) |
289
+ | Strong recurring SaaS with high growth and retention | `ARR` | `5x-15x ARR` | [Acquire.com SaaS valuation multiples guide](https://blog.acquire.com/saas-valuation-multiples/) |
290
+
291
+ How to read this table:
292
+
293
+ - Smaller bootstrapped SaaS buyers on Acquire-style marketplaces often underwrite on `TTM profit`.
294
+ - If profit is low or intentionally reinvested, buyers may fall back to `TTM revenue`.
295
+ - TrustMRR listing multiples are a secondary comparison set: they are based on recent revenue run-rate, specifically `last 30 days revenue × 12`.
296
+ - Higher-quality SaaS with real scale, low churn, and strong growth is more likely to be discussed in `ARR` terms.
297
+
298
+ ### Rough valuation formulas from `profitable`
299
+
300
+ You can only multiply a metric by a multiple if the denominator matches.
301
+
302
+ #### 1. ARR multiple
303
+
304
+ This is already built into the gem:
305
+
306
+ ```ruby
307
+ # Example: 6x ARR
308
+ Profitable.estimated_arr_valuation(multiple: 6).to_readable
309
+ ```
310
+
311
+ Use this when:
312
+
313
+ - your business is strongly recurring,
314
+ - churn and retention are solid,
315
+ - and you want a run-rate-based heuristic.
316
+
317
+ #### 2. TTM revenue multiple
318
+
319
+ This is useful when buyers care more about trailing actuals than annualized run-rate:
320
+
321
+ ```ruby
322
+ ttm_revenue_cents = Profitable.ttm_revenue.to_i
323
+
324
+ low_estimate_cents = (ttm_revenue_cents * 2.0).round
325
+ high_estimate_cents = (ttm_revenue_cents * 4.0).round
326
+ ```
327
+
328
+ Use this when:
329
+
330
+ - the business has meaningful one-time revenue,
331
+ - profit is thin because you are reinvesting,
332
+ - or the buyer is thinking in revenue-band terms.
333
+
334
+ #### 2b. Recent revenue run-rate multiple
335
+
336
+ This is the closest match to TrustMRR-style marketplace multiples:
337
+
338
+ ```ruby
339
+ # Default: annualized last-30-days revenue
340
+ Profitable.revenue_run_rate.to_readable
341
+ Profitable.estimated_revenue_run_rate_valuation(2.7).to_readable
342
+ ```
343
+
344
+ Use this when:
345
+
346
+ - you're comparing against TrustMRR listings,
347
+ - the market is quoting a multiple on recent revenue rather than ARR,
348
+ - and you want the denominator to match the marketplace comp.
349
+
350
+ #### 3. TTM profit multiple
351
+
352
+ `profitable` cannot calculate this yet because it does not know your costs.
353
+
354
+ ```ruby
355
+ # You need to compute this outside of profitable:
356
+ ttm_profit_cents = your_ttm_profit_cents
357
+
358
+ low_estimate_cents = (ttm_profit_cents * 3.5).round
359
+ high_estimate_cents = (ttm_profit_cents * 4.35).round
360
+ ```
361
+
362
+ Use this when:
363
+
364
+ - the business is a smaller profitable micro-SaaS,
365
+ - the buyer is focused on ROI and cash flow,
366
+ - or you're comparing yourself to Acquire.com-style marketplace comps.
367
+
134
368
  ## Development
135
369
 
136
370
  After checking out the repo, install dependencies:
@@ -1,12 +1,25 @@
1
1
  module Profitable
2
2
  class DashboardController < BaseController
3
3
  def index
4
- end
4
+ @mrr = Profitable.mrr
5
+ @mrr_growth_rate = Profitable.mrr_growth_rate
6
+ @total_customers = Profitable.total_customers
7
+ @ttm_revenue = Profitable.ttm_revenue
8
+ @all_time_revenue = Profitable.all_time_revenue
9
+ @estimated_valuation = Profitable.estimated_valuation
10
+ @average_revenue_per_customer = Profitable.average_revenue_per_customer
11
+ @lifetime_value = Profitable.lifetime_value
5
12
 
6
- private
13
+ @show_milestone = @mrr_growth_rate > 0
14
+ @milestone_message = Profitable.time_to_next_mrr_milestone if @show_milestone
7
15
 
8
- def test
9
- end
16
+ @monthly_summary = Profitable.monthly_summary(months: 12)
17
+ @daily_summary = Profitable.daily_summary(days: 30)
10
18
 
19
+ @periods = [24.hours, 7.days, 30.days]
20
+ @period_data = @periods.each_with_object({}) do |period, hash|
21
+ hash[period] = Profitable.period_data(in_the_last: period)
22
+ end
23
+ end
11
24
  end
12
25
  end