# Notch Metrics > Notch Metrics shows your business metrics, revenue, MRR and subscribers, in the MacBook notch. Hover the camera housing to see them; move away and the notch looks untouched. Provider API keys are held encrypted server-side and never reach the Mac. Apple silicon, macOS 14 and later. Every blog post and library entry, full text, newest first. Index at https://notchmetrics.com/llms.txt. --- # Amplitude active users on your Mac URL: https://notchmetrics.com/blog/amplitude-active-users-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: amplitude, analytics, mac Amplitude is built for asking hard questions. Most days you have one easy question, and loading a product analytics suite to answer it is the wrong shape of effort. ## How do you watch Amplitude without opening it? Notch Metrics puts the headline figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What Amplitude reports | Number | What it is | | ----------------------------------------------- | -------------------------------------- | | [Active users](/sources/amplitude/active-users) | Anyone who did anything, de-duplicated | | [New users](/sources/amplitude/new-users) | First seen in the window | | [Events](/sources/amplitude/events) | Every event recorded, of every type | Three numbers rather than thirty, and that is the point. The notch is a few hundred pixels of glass. If you need a cohort breakdown, Amplitude is still the right place to go. ## Daily buckets, with no hourly option Amplitude's Dashboard REST API serves daily, weekly and monthly intervals. There is no hourly one, so [the widgets](/sources/amplitude/active-users) offer none rather than manufacturing it by splitting a day into twenty-four equal pieces. A sparkline that invents its own resolution is worse than a coarser one. ## Why the bars do not add up Active users is de-duplicated across the whole window, so one person active on three days is one user and three bars. A weekly figure is not the sum of seven daily figures. That is Amplitude's own behaviour and it is the correct one. It is worth knowing before you check the arithmetic. ## Connecting An API key, a secret key and your project's region. Amplitude runs US and EU data centres and a key pair from one returns an error against the other, which reads like bad credentials rather than a wrong region. The connector asks, and says so when it fails. [What the Amplitude connector asks for](/sources/amplitude). Both keys are encrypted on the server with a key unique to that connection, so the Mac app never holds them. ## Beta, and the mark The Amplitude connector is marked beta on [its page](/sources/amplitude). It connects and returns figures today. It also has no vendored brand mark, so it shows a lettermark rather than an approximated logo, which is a deliberate choice: an approximated logo looks like a bug and a lettermark looks like a decision. ## Alongside everything else Revenue from [Stripe](/sources/stripe), traffic from [Plausible](/sources/plausible) and [Google Analytics](/sources/google-analytics), product analytics from [PostHog](/sources/posthog). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see Amplitude active users on my Mac?** Install Notch Metrics, connect Amplitude on the web with an API key, a secret key and your project region, and place the active users widget in the MacBook notch. **Why do the Amplitude widgets have no hourly option?** Because the Amplitude Dashboard REST API serves daily, weekly and monthly intervals and offers no hourly one. Splitting a day into twenty-four equal pieces would be inventing resolution the API never returned. **Does Notch Metrics support Amplitude EU?** Yes. The connector asks which region your project is in, because Amplitude runs US and EU data centres and a key pair from one fails against the other with an error that reads like bad credentials. **Why do Amplitude daily bars not add up to the weekly figure?** Because active users is de-duplicated across the whole window. One person active on three days is one user and three bars, so a weekly figure is smaller than the sum of seven daily ones. **Why does Amplitude show a letter instead of a logo?** Because no vendored brand mark exists for it. Notch Metrics shows a lettermark rather than approximating a logo, on the grounds that an approximated logo looks like a bug and a lettermark looks like a choice. --- # How to calculate average order value from Shopify orders URL: https://notchmetrics.com/blog/calculate-average-order-value-from-shopify-orders Published: 11 October 2026 Author: Notch Metrics Tags: shopify, aov, api ## What is average order value? Total sales divided by the number of orders over the same window. Two fields, one division. The arithmetic is trivial and the reporting is where it goes wrong. ## The mistake nearly everyone makes There are two ways to compute AOV over a week and they give different answers. **Weighted.** Sum the week's sales, sum the week's orders, divide. **Unweighted.** Compute each day's AOV, then average the seven numbers. The first is correct. The second treats a Sunday with two orders as equal in weight to a Black Friday with two thousand, and a single quiet day with one expensive order can move it by a third. ```js // Right. const aov = totalSales / totalOrders; // Wrong, and it will look plausible for months. const aov = days.reduce((sum, d) => sum + d.sales / d.orders, 0) / days.length; ``` This is also why a correct AOV chart looks inconsistent: each point is that day's own ratio, and the headline is the window's ratio. They are not supposed to match, and a tool whose headline equals the mean of its points is doing the wrong calculation. ## Getting the two numbers One GraphQL query gives you both, because the order count is the number of nodes you summed. ```graphql query Orders($query: String!, $cursor: String) { orders(first: 250, query: $query, after: $cursor) { pageInfo { hasNextPage endCursor } nodes { createdAt displayFinancialStatus currentTotalPriceSet { shopMoney { amount currencyCode } } } } } ``` Count the nodes you include in the sales total, and divide at the end. Count and sum have to use the same filter, or the ratio is meaningless. ## Decide about cancelled and refunded orders once An order that was placed and then refunded happened. It had a value. Include it in both numerator and denominator and your AOV is the average value of orders placed. Exclude it from both and your AOV is the average value of orders kept. Also defensible. What is not defensible is excluding it from one and not the other, which is the easiest bug to write here: filter the sales sum on financial status and forget to filter the count, and the AOV quietly drops. ## Currency `shopMoney` is in the store's currency. A store selling in several presentment currencies still has one shop currency, so this is already normalised and the division is safe. If you are summing `presentmentMoney` for some reason, AOV across mixed currencies is a number with no units. ## What actually moves AOV Three things, and they call for different responses. **Price changes** move it evenly across every order. **Bundles and upsells** move it only for the orders that take them, so AOV rises while the median order stays put. Worth watching the distribution, not just the mean. **One wholesale order** moves it for a day and then it falls back. If a single order can move your AOV visibly, the mean is the wrong statistic and you want the median. ## In the notch Notch Metrics reports [Shopify average order value](/sources/shopify/average-order-value) as sales divided by orders over the whole window, with each sparkline point being that day's own ratio. The page says so, so the apparent inconsistency is documented rather than discovered. Place it beside [sales](/sources/shopify/revenue) and [orders](/sources/shopify/orders) and the three explain each other. [Everything Shopify reports](/sources/shopify), or [download Notch Metrics](/download). ## Questions **How do you calculate average order value?** Sum sales over the window and divide by the number of orders in the same window. Do not average the daily ratios: that weights a quiet Sunday equally with Black Friday and a single expensive order can move it by a third. **Why does my AOV headline not match the average of the chart?** Because each chart point is that day's own ratio and the headline is the window's total sales divided by its total orders. They are not supposed to match, and a tool whose headline equals the mean of its points is doing the wrong calculation. **Should refunded orders count in average order value?** Either, as long as you are consistent. Include them in both numerator and denominator and AOV is the average value of orders placed. Exclude from both and it is the average value of orders kept. Excluding from one and not the other is the common bug. **What moves average order value?** Price changes move it evenly. Bundles and upsells move it only for the orders that take them, so AOV rises while the median order does not. A single wholesale order moves it for a day and then it falls back. **Can I compute AOV across multiple currencies?** Only after converting. Sum shopMoney, which is the store's own currency and already normalised. Averaging presentmentMoney across mixed currencies produces a number with no units. --- # How to calculate MRR from Dodo Payments subscriptions URL: https://notchmetrics.com/blog/calculate-mrr-from-dodo-payments-subscriptions Published: 11 October 2026 Author: Notch Metrics Tags: dodopayments, mrr, api ## Does Dodo Payments return MRR? No. There is no MRR endpoint, so you compute it from the subscriptions list. The good news is that Dodo Payments gives you the right field to do it with, which is more than most merchants of record manage. ## The field that matters `recurring_pre_tax_amount`, and both halves of that name are the point. **Recurring.** It is the amount that repeats, not the amount of the next invoice, which may include a prorated change or a one-off add-on. **Pre-tax.** Tax is excluded. That is the correct basis for MRR: the VAT a merchant of record collects is not your revenue, so including it would inflate the figure by whatever your customers' tax rates happen to average. If you have ever compared a merchant-of-record MRR against a Stripe MRR and found it mysteriously higher, tax is usually the whole gap. ## The calculation ```js const PER_MONTH = { daily: 365 / 12, weekly: 52 / 12, monthly: 1, yearly: 1 / 12 }; async function mrr(key, host) { const subs = await listSubscriptions(key, host, { status: "active" }); let total = 0; for (const sub of subs) { const amount = sub.recurring_pre_tax_amount; // minor units const per = PER_MONTH[sub.payment_frequency_interval?.toLowerCase()] ?? 0; // The count is the multiplier: every 3 months is monthly / 3. total += (amount * per) / (sub.payment_frequency_count ?? 1); } return Math.round(total); } ``` `payment_frequency_interval` and `payment_frequency_count` work together. A subscription billing every three months has an interval of month and a count of three, so its monthly contribution is the amount divided by three. Treating the count as decoration is the bug that hides, because annual plans have a count of one and look fine, and quarterly plans quietly read as three times their worth until somebody checks. ## Weekly plans, and the month that is not four weeks Multiply a weekly amount by 52 and divide by 12, not by four. A month averages 4.345 weeks, so the four-week shortcut undercounts weekly plans by about eight per cent. Few businesses have many weekly subscriptions, and the ones that do are usually the ones who notice. ## Which statuses count `active`. A subscription that is cancelled, expired or on hold is not paying you this month, and including it reports a book you no longer have. Trials are the judgement call, as always. A trial is revenue nobody has agreed to yet. Excluding it is the honest default and it is what Notch Metrics does. ## A snapshot, not a sum MRR is the value of the book right now. It is not a total over a window, and a sparkline of it is a sparkline of your own history rather than of anything the API returned. That is why the widget shows the figure without one. A chart implying a daily MRR series built from a single current snapshot would be inventing data. ## Or read it off the notch Notch Metrics computes [Dodo Payments MRR](/sources/dodopayments/mrr) exactly this way: active subscriptions, each `recurring_pre_tax_amount` normalised to a monthly amount using its payment frequency, as a snapshot of the book right now. Put it beside [total revenue](/sources/dodopayments/revenue) and [one-time sales](/sources/dodopayments/one-time-sales) and the month reads at a glance. [Everything Dodo Payments reports](/sources/dodopayments), or [download Notch Metrics](/download). ## Questions **Does the Dodo Payments API return MRR?** No. There is no MRR endpoint. You compute it by listing active subscriptions and normalising each recurring_pre_tax_amount to a monthly figure using its payment frequency. **Why does Dodo Payments MRR use a pre-tax amount?** Because the VAT a merchant of record collects is not your revenue. Including it would inflate MRR by whatever your customers' tax rates average, and it is usually the whole gap when a merchant-of-record MRR looks higher than an equivalent Stripe one. **What is payment_frequency_count in Dodo Payments?** The multiplier on the interval. A subscription billing every three months has an interval of month and a count of three, so its monthly contribution is the amount divided by three. Ignoring the count makes quarterly plans read as three times their worth. **How do you convert a weekly subscription to MRR?** Multiply by 52 and divide by 12. A month averages 4.345 weeks, so treating a month as four weeks undercounts weekly plans by about eight per cent. **Why does the MRR widget have no sparkline?** Because MRR is the value of the book right now, not a total over a window. A daily MRR series built from a single current snapshot would be invented data rather than anything the API returned. --- # How to calculate MRR from Paddle Billing URL: https://notchmetrics.com/blog/calculate-mrr-from-paddle-billing Published: 11 October 2026 Author: Notch Metrics Tags: paddle, mrr, api ## Does Paddle Billing have an MRR endpoint? No. Paddle's dashboard shows MRR; its API does not return one. You compute it from `/subscriptions`, and the structure of a Paddle subscription makes that slightly more work than it looks. This is Paddle Billing, the current API at `api.paddle.com`. Paddle Classic was a different product with a different API and none of this applies to it. ## The shape of a Paddle subscription A subscription has an `items` array. Each item has a `price`, and each price has a `billing_cycle` with an `interval` and a `frequency`. The frequency is the multiplier: a price billing every three months has `interval: "month"` and `frequency: 3`. An item can also be non-recurring, in which case `billing_cycle` is null and it is a one-off charge sitting on a subscription. Those must not count towards MRR. ```js const PER_MONTH = { day: 365 / 12, week: 52 / 12, month: 1, year: 1 / 12 }; function subscriptionMrr(sub) { let total = 0; for (const item of sub.items) { const cycle = item.price?.billing_cycle; // A one-off charge on a subscription. Not recurring revenue. if (!cycle) continue; const unit = Number(item.price.unit_price.amount); // minor units, as a string const amount = unit * (item.quantity ?? 1); total += (amount * PER_MONTH[cycle.interval]) / cycle.frequency; } return total; } ``` Two details in there cost people an afternoon each. **Amounts are strings.** Paddle returns `unit_price.amount` as a string of minor units, `"1900"` rather than `1900`. Add two of them without converting and you get `"19001900"`. `Number()` them on the way in. **Divide by `frequency`.** A quarterly price is `month` with frequency 3, so the monthly figure is the amount divided by three. Treating frequency as decoration makes quarterly plans read as three times their worth, and annual-with-frequency-1 plans read fine, so the bug hides until somebody buys quarterly. ## Which statuses to include Paddle subscriptions are `active`, `trialing`, `paused`, `past_due` or `canceled`. Count `active`. Do not count `trialing`, which is revenue nobody has agreed to. Do not count `paused`, which is revenue explicitly suspended. `past_due` is a judgement call: the customer still intends to pay and dunning is running, so including it overstates slightly and excluding it makes your MRR drop on a card expiry that will probably resolve. Excluding it is the safer default. ```js const res = await fetch("https://api.paddle.com/subscriptions?status=active&per_page=100", { headers: { Authorization: `Bearer ${apiKey}` }, }); ``` ## Discounts are not applied Paddle applies discounts at billing time rather than storing a reduced price on the item. So the calculation above is list price, and a customer on a permanent 20% discount contributes more MRR here than they actually pay. Fixing that means reading the subscription's `discount`, looking up the discount object, and applying it, which is another call per distinct discount. Whether that is worth it depends on how much of your book is discounted. Either way, know which one your figure is and say so. ## Pagination Paddle pages with `per_page` and a cursor in `meta.pagination.next`. Page until there is no next link rather than until you see a short page: Paddle can return a short page with more to come. ## Or read it off the notch Notch Metrics computes [Paddle MRR](/sources/paddle/mrr) this way, on its own schedule, and shows it in the MacBook notch. Active subscriptions, each recurring item's unit price times quantity normalised to a month by its billing cycle, discounts not applied, as a snapshot of the book right now. The definition is written on the page so nobody has to read the code. Connect Paddle once on the web. [Everything Paddle reports](/sources/paddle), or [download Notch Metrics](/download). ## Questions **Does Paddle Billing have an MRR API endpoint?** No. The Paddle dashboard shows MRR but the API does not return one. You compute it from the subscriptions list by normalising each recurring item to a monthly amount. **Why are Paddle amounts returned as strings?** Paddle serialises money as a string of minor units, such as "1900", to avoid float precision problems in JSON. Convert with Number() before arithmetic, or adding two amounts concatenates them. **What does frequency mean on a Paddle billing cycle?** It is the multiplier on the interval. A price billing every three months has interval month and frequency 3, so its monthly contribution is the amount divided by three. Ignoring frequency makes quarterly plans read as three times their worth. **Should past_due Paddle subscriptions count towards MRR?** It is a judgement call. The customer still intends to pay and dunning is running, so including them overstates slightly and excluding them makes MRR drop on a card expiry that will probably resolve. Excluding is the safer default. **Does Paddle apply discounts to the subscription item price?** No. Paddle applies discounts at billing time rather than storing a reduced price, so an MRR computed from item prices is list price. A customer on a permanent discount contributes more than they actually pay. --- # How to calculate MRR from the Lemon Squeezy API URL: https://notchmetrics.com/blog/calculate-mrr-from-the-lemon-squeezy-api Published: 11 October 2026 Author: Notch Metrics Tags: lemonsqueezy, mrr, api ## Does Lemon Squeezy return MRR? No. You compute it from `/v1/subscriptions`, and the first obstacle is not the maths. ## JSON:API, and where the data actually is Lemon Squeezy serves JSON:API. That means the fields you want are not on the object, they are on `attributes`, and the relationships you want are references rather than embedded objects. ```json { "data": [ { "type": "subscriptions", "id": "123", "attributes": { "status": "active", "variant_id": 456 }, "relationships": { ... } } ] } ``` Code written against a normal REST API reads `sub.status` and gets `undefined`, every time, silently. It is `sub.attributes.status`. Set the headers or you get nothing useful back: ``` Accept: application/vnd.api+json Content-Type: application/vnd.api+json Authorization: Bearer ``` ## The price is not on the subscription This is the part that makes Lemon Squeezy MRR more work than it looks. A subscription carries a `variant_id`, and the price lives on the variant, not on the subscription. So the calculation is two passes. Collect the distinct variant ids from your active subscriptions, fetch each one's price once, then sum. ```js const PER_MONTH = { day: 365 / 12, week: 52 / 12, month: 1, year: 1 / 12 }; async function mrr(key, storeId) { const subs = await list( key, `/v1/subscriptions?filter[store_id]=${storeId}&filter[status]=active`, ); // Fetch each price once, not once per subscriber. const variantIds = [...new Set(subs.map((s) => s.attributes.variant_id))]; const prices = new Map(); for (const id of variantIds) { const price = await fetchPriceForVariant(key, id); prices.set(id, price); } let total = 0; for (const sub of subs) { const price = prices.get(sub.attributes.variant_id); if (!price) continue; const per = PER_MONTH[price.renewal_interval_unit] ?? 0; total += (price.unit_price * per) / (price.renewal_interval_quantity ?? 1); } return Math.round(total); } ``` Deduplicating the variant lookups matters. A thousand subscribers across six plans is six price calls, not a thousand, and the difference is whether you stay inside the rate limit. ## renewal_interval_quantity Same trap as everywhere else. A price renewing every six months has `renewal_interval_unit` of `month` and `renewal_interval_quantity` of 6, so its monthly value is the amount divided by six. Annual plans have a quantity of one and look correct without it, which is why the bug survives until somebody sells a six-monthly plan. ## Statuses, and the trial question Lemon Squeezy subscriptions can be `on_trial`, `active`, `paused`, `past_due`, `unpaid`, `cancelled` or `expired`. Filtering on `active` excludes trials, which is the honest basis for MRR. Note that this differs from Lemon Squeezy's own active subscriber count, which counts trials in. Two reasonable definitions, and you have to pick which one you are reporting and say so. `cancelled` is worth a second look: a cancelled Lemon Squeezy subscription keeps running until `ends_at`, so it is still being paid for. Whether it belongs in MRR depends on whether you are measuring money arriving or the book you expect to keep. ## Pagination `page[size]` up to 100, and `links.next` to continue. Page until there is no next link. ## Or read it off the notch Notch Metrics computes [Lemon Squeezy MRR](/sources/lemonsqueezy/mrr) this way, caching the variant prices, and shows it in the MacBook notch alongside [revenue](/sources/lemonsqueezy/revenue), [active subscribers](/sources/lemonsqueezy/active-subscribers), [refunds](/sources/lemonsqueezy/refunds) and a feed of [recent orders](/sources/lemonsqueezy/recent-payments). [Everything Lemon Squeezy reports](/sources/lemonsqueezy), or [download Notch Metrics](/download). ## Questions **Why are my Lemon Squeezy API fields undefined?** Because Lemon Squeezy serves JSON:API. The fields live on attributes rather than on the object itself, so sub.status is undefined and sub.attributes.status is the value. Set Accept and Content-Type to application/vnd.api+json as well. **Where is the price on a Lemon Squeezy subscription?** Not on the subscription. A subscription carries a variant_id and the price lives on the variant, so computing MRR means fetching each distinct variant's price and joining. Deduplicate the lookups or a thousand subscribers becomes a thousand calls. **What is renewal_interval_quantity in Lemon Squeezy?** The multiplier on the interval unit. A price renewing every six months has a unit of month and a quantity of six, so its monthly value is the amount divided by six. Annual plans have a quantity of one and hide the bug. **Should on_trial Lemon Squeezy subscriptions count towards MRR?** Usually not, because a trial is revenue nobody has agreed to yet. Note that this differs from Lemon Squeezy's own active subscriber count, which counts trials in, so pick a definition and say which one you are reporting. **Does a cancelled Lemon Squeezy subscription still pay?** Yes, until ends_at. Whether it belongs in MRR depends on whether you are measuring money arriving or the book you expect to keep next month. --- # How to calculate MRR from the Polar API URL: https://notchmetrics.com/blog/calculate-mrr-from-the-polar-api Published: 11 October 2026 Author: Notch Metrics Tags: polar, mrr, api ## Does Polar have an MRR endpoint? Yes, and that is unusual. Most payment providers make you derive MRR from a subscriptions list and argue with yourself about the definition. Polar computes it server-side and serves it from `/v1/metrics/`. ```bash curl -s 'https://api.polar.sh/v1/metrics/?start_date=2026-09-01&end_date=2026-10-01&interval=day&metrics=monthly_recurring_revenue' \ -H 'Authorization: Bearer polar_oat_...' ``` The response carries a `periods` array, one entry per interval, each holding the metric values you asked for. ## What Polar's MRR counts Active subscriptions, normalised to a month. Yearly plans are divided by twelve. Trials are excluded. That is a reasonable definition and, more usefully, it is the same definition the Polar dashboard shows, so your number and the one your co-founder is looking at will match. Ask for `metrics=monthly_recurring_revenue` explicitly. The endpoint will compute fifty-odd figures per call if you let it, and asking for one is faster and kinder. ## The trap: null periods Polar computes metrics on a pipeline, and the newest period can come back with `null` for a while before it is filled in. Code that reads `periods.at(-1)` will show zero shortly after midnight and then correct itself. ```js function latestMrr(periods) { // The last period with a value, not the last period. A null newest // bucket is a pipeline that has not caught up, not an MRR of zero. return ( [...periods].reverse().find((p) => typeof p.monthly_recurring_revenue === "number") ?.monthly_recurring_revenue ?? null ); } ``` Ask for a week of daily periods rather than one, so there is something to fall back to. ## Timezone `/v1/metrics/` takes a `timezone` parameter and it is worth passing explicitly. Omit it and your daily buckets follow Polar's default rather than yours, and every figure near midnight lands on the day you did not expect. Use an IANA name, `Europe/London` rather than an offset, so daylight saving is handled for you. ## Sandbox and production are different hosts Polar's sandbox lives at `sandbox-api.polar.sh` and production at `api.polar.sh`. A production token against the sandbox host, or the reverse, returns 401. That is better than returning an empty account, which is what several providers do, but it still looks like a bad token rather than a wrong host. ## When to compute your own instead Polar's figure is the right default. Compute your own if you need a definition Polar does not offer: MRR excluding a particular product, MRR by plan tier, or a figure that nets off a discount Polar counts differently. In that case list subscriptions and normalise each one yourself, the same way you would on any provider. Be aware that you are now maintaining a number that will drift from the dashboard, and that somebody will eventually ask which is right. ## Or put Polar's own figure in the notch Notch Metrics reads [Polar MRR](/sources/polar/mrr) from that endpoint, handles the null periods, and shows it in the MacBook notch. It also reports [revenue](/sources/polar/revenue), [active](/sources/polar/active-subscribers), [new](/sources/polar/new-subscribers) and [churned subscriptions](/sources/polar/churned-subscribers), and a feed of [recent orders](/sources/polar/recent-payments). Connect once on the web, and the token never reaches your Mac. [Download Notch Metrics](/download). ## Questions **Does the Polar API have an MRR endpoint?** Yes. GET /v1/metrics/ with metrics=monthly_recurring_revenue returns Polar's own MRR per interval. That is unusual among payment providers, most of which make you derive MRR from a subscriptions list. **How does Polar define MRR?** Active subscriptions normalised to a month, with yearly plans divided by twelve and trials excluded. It is the same definition the Polar dashboard shows, so an API figure and a dashboard figure agree. **Why does the Polar metrics endpoint return null for today?** Polar computes metrics on a pipeline and the newest period can be null until it catches up. Read the last period that has a value rather than the last period, or your figure reads zero shortly after midnight. **Which timezone does the Polar metrics endpoint use?** Whatever you pass in the timezone parameter, and Polar's default if you pass nothing. Pass an IANA name such as Europe/London rather than an offset so daylight saving is handled. **What is the Polar sandbox API URL?** sandbox-api.polar.sh, against production's api.polar.sh. A token from one used against the other returns 401, which looks like a bad token rather than a wrong host. --- # How to calculate MRR from the Stripe API URL: https://notchmetrics.com/blog/calculate-mrr-from-the-stripe-api Published: 11 October 2026 Author: Notch Metrics Tags: stripe, mrr, api ## Does Stripe have an MRR endpoint? No. Stripe Billing shows an MRR figure in its own analytics, but there is no API that returns it. If you want MRR programmatically you compute it from the subscriptions list, and the moment you do you have to decide what you mean by it. That is the real difficulty. MRR is always somebody's definition. Gross or net of discounts, trials counted or not, tax included or not. Stripe picked one, you will pick another, and the two will differ by a few per cent forever. ## The calculation Walk every active subscription, normalise each line item to a month, and sum. ```js const SECONDS_PER_MONTH = { day: 1 / 30.4375, week: 12 / 52, month: 1, year: 1 / 12 }; async function mrr(stripe) { let total = 0; for await (const sub of stripe.subscriptions.list({ status: "active", limit: 100, expand: ["data.discounts"], })) { for (const item of sub.items.data) { const price = item.price; if (!price.recurring) continue; // Minor units. Never a float. const amount = (price.unit_amount ?? 0) * (item.quantity ?? 1); const perMonth = (amount * SECONDS_PER_MONTH[price.recurring.interval]) / (price.recurring.interval_count ?? 1); total += perMonth; } } return Math.round(total); } ``` Four things in that snippet are the whole job. **`status: "active"`.** Stripe subscriptions can be `trialing`, `past_due`, `unpaid`, `paused`, `canceled` or `incomplete`. Only `active` is money you are currently being paid. Including `trialing` counts revenue nobody has agreed to yet, and including `past_due` counts revenue that is failing. **Line items, not the subscription.** A subscription has an `items` array and each item has its own price, interval and quantity. A plan with a base fee and a per-seat add-on is two items, and reading only the first one undercounts every customer you have. **Annual divided by twelve.** An annual plan at $1,188 is $99 of MRR, not $1,188. Weekly plans are the awkward ones: multiply by 52 and divide by 12, not by four, because a month is not four weeks. **`interval_count`.** A price can bill every three months. Its interval is `month` and its `interval_count` is 3, so the monthly figure is the amount divided by three. Miss this and quarterly plans read as three times their worth. ## Discounts, which is where numbers diverge A percentage coupon reduces MRR. A fixed-amount coupon reduces it by the amount, floored at zero. A coupon with a `duration` of `repeating` reduces it only for the months it runs, which means your MRR will step up on a date nobody wrote down. ```js function applyDiscount(amount, discounts) { for (const discount of discounts ?? []) { const coupon = discount.coupon; if (coupon?.percent_off) amount -= amount * (coupon.percent_off / 100); if (coupon?.amount_off) amount = Math.max(0, amount - coupon.amount_off); } return amount; } ``` Discounts sit in two places: on the subscription and on the individual item. Both apply, and an implementation that reads only the subscription one reports too high for anybody on a line-item discount. ## Why your number will not match Stripe's Stripe Billing's own MRR normalises differently in places, handles tax differently depending on whether your prices are inclusive, and has its own view of when a subscription starts counting. It also updates on its own schedule rather than at the moment a subscription changes. A few per cent of disagreement is normal. What matters is that your figure is consistent with itself over time, and that you can say exactly what it counts when somebody asks. Write the definition down next to the code. ## Pagination, and the part that bites in production `stripe.subscriptions.list` is paginated at 100. The auto-pagination above handles it, but a business with twenty thousand subscriptions makes two hundred API calls every time you want one number. You will hit rate limits, and you will hit them at the worst moment. Cache the result. MRR is a snapshot that moves slowly, and recomputing it every minute buys nothing. ## Or skip the whole thing Notch Metrics computes Stripe MRR on its own schedule and puts it in the MacBook notch. Active subscriptions, each item normalised to a month, annual divided by twelve, weekly times 52 over 12, reduced by percentage coupons. A snapshot, not a sum over the window, and the definition is written on [the MRR page](/sources/stripe/mrr) so you never have to guess what it counts. Connect Stripe once with a read-only restricted key, place the widget, and the number is there every time you glance up. [Download it](/download), or read what else [the Stripe connector reports](/sources/stripe). ## Questions **Does the Stripe API have an MRR endpoint?** No. Stripe Billing shows MRR in its own analytics, but no API returns it. You compute MRR by listing active subscriptions and normalising each line item to a monthly amount. **How do you convert an annual Stripe plan to MRR?** Divide the annual amount by twelve. An annual plan at $1,188 contributes $99 of MRR. For weekly plans, multiply by 52 and divide by 12 rather than by four, because a month is not four weeks. **Why does my calculated Stripe MRR not match Stripe Billing?** Stripe Billing normalises differently in places, treats tax differently depending on whether your prices are inclusive, and updates on its own schedule. A few per cent of disagreement is normal. What matters is that your figure is consistent and that you can say what it counts. **Should trialing subscriptions count towards Stripe MRR?** Usually not. A trialing subscription is revenue nobody has agreed to pay yet. Filter the subscriptions list to status active, which excludes trialing, past_due, unpaid, paused and canceled. **Do Stripe coupons reduce MRR?** Yes, and they sit in two places. A discount can be on the subscription or on an individual line item, and both apply. A percentage coupon scales the amount, a fixed-amount coupon subtracts from it with a floor of zero, and a repeating coupon stops applying after its duration. **How do I avoid Stripe rate limits when calculating MRR?** Cache the result. The subscriptions list pages at 100, so twenty thousand subscriptions means two hundred calls for one number. MRR moves slowly, so recomputing it every minute buys nothing. --- # How to count active Lemon Squeezy subscriptions URL: https://notchmetrics.com/blog/count-active-lemon-squeezy-subscriptions Published: 11 October 2026 Author: Notch Metrics Tags: lemonsqueezy, subscribers, api ## What counts as an active Lemon Squeezy subscription? It depends what you are counting for, and Lemon Squeezy's own answer is not the only reasonable one. A subscription has one of seven statuses. | Status | Paying now? | Has access? | | ----------- | ------------------- | --------------------- | | `on_trial` | No | Yes | | `active` | Yes | Yes | | `paused` | No | Depends on your setup | | `past_due` | Payment failing | Usually yes | | `unpaid` | No, dunning gave up | Usually no | | `cancelled` | Until `ends_at` | Until `ends_at` | | `expired` | No | No | Three different questions produce three different counts. **How many people are paying me?** `active` alone. **How many people have access?** `active`, `on_trial`, `past_due`, and `cancelled` that has not reached `ends_at`. This is the number your infrastructure costs track. **How big is the book?** `active` plus `cancelled` before `ends_at`, because that money is already committed. None of these is wrong. Picking one quietly and calling it "subscribers" is. ## The call ```bash curl -s 'https://api.lemonsqueezy.com/v1/subscriptions?filter[store_id]=12345&filter[status]=active&page[size]=100' \ -H 'Accept: application/vnd.api+json' \ -H 'Authorization: Bearer ' ``` Remember this is JSON:API: the status is at `data[].attributes.status`, not `data[].status`. ## Lemon Squeezy's own count includes trials Worth knowing before you compare your figure against the dashboard. Lemon Squeezy reports trialling subscriptions as active in its own subscriber count, so a filter on `filter[status]=active` returns fewer rows than the dashboard shows. Neither is a bug. They are answering the second question and the first question respectively. ## cancelled is not expired The distinction people miss. A `cancelled` subscription keeps running until `ends_at`. Somebody who cancels an annual plan today is `cancelled` and paying for another eleven months. ```js const stillRunning = subs.filter((s) => { const a = s.attributes; if (a.status === "active" || a.status === "on_trial") return true; if (a.status === "cancelled") return Date.parse(a.ends_at) > Date.now(); return false; }); ``` That is also where your churn leading indicator lives. The count of `cancelled` subscriptions with a future `ends_at` is revenue you are going to lose, visible months before it goes. ## past_due is recoverable A `past_due` subscription is a card that failed and dunning retrying. Most recover. Counting them as lost today means counting the loss twice: once now and once when they actually go `unpaid`. Watch the `past_due` count as its own number. It is the churn you can fix. ## In the notch Notch Metrics reports [Lemon Squeezy active subscribers](/sources/lemonsqueezy/active-subscribers) as subscriptions in status `active` right now, trials included, which is Lemon Squeezy's own definition. The page says so rather than leaving you to discover it from a disagreement with the dashboard. Place it beside [MRR](/sources/lemonsqueezy/mrr), which excludes trials, and the gap between the two is how many people are still deciding. [Everything Lemon Squeezy reports](/sources/lemonsqueezy), or [download Notch Metrics](/download). ## Questions **How many statuses does a Lemon Squeezy subscription have?** Seven: on_trial, active, paused, past_due, unpaid, cancelled and expired. Which ones you count depends on whether you are asking who is paying, who has access, or how big the committed book is. **Does Lemon Squeezy count trials as active subscribers?** Yes, in its own dashboard figure. Filtering the API on filter[status]=active excludes trials and therefore returns fewer rows than the dashboard shows. Neither is wrong, they answer different questions. **Is a cancelled Lemon Squeezy subscription still paying?** Until ends_at, yes. Somebody who cancels an annual plan today is cancelled and paid up for another eleven months. The count of cancelled subscriptions with a future ends_at is revenue you are going to lose, visible months ahead. **What does past_due mean in Lemon Squeezy?** A payment failed and dunning is retrying. Most recover, so counting past_due as churn means counting the loss twice, once now and once when the subscription actually goes unpaid. Track it separately as the churn you can fix. **Why do my Lemon Squeezy subscriber count and MRR disagree?** Because the active subscriber figure includes trials and MRR does not. The gap between the two is how many people are still deciding. --- # How to count events in Amplitude by API URL: https://notchmetrics.com/blog/count-events-in-amplitude-by-api Published: 11 October 2026 Author: Notch Metrics Tags: amplitude, events, api ## The endpoint Event counts come from `/api/2/events/segmentation`, not from the users endpoint. ```bash curl -s -u "$API_KEY:$SECRET_KEY" \ 'https://amplitude.com/api/2/events/segmentation?e=%7B%22event_type%22%3A%22_active%22%7D&start=20261001&end=20261007&i=1&m=totals' ``` That `e` parameter is a JSON object, URL encoded. Decoded it is: ```json { "event_type": "_active" } ``` ## The special event types Amplitude reserves two event types that are not events you sent. | `event_type` | What it means | | ------------ | ---------------------- | | `_active` | Any event, of any type | | `_new` | A user's first event | `_active` is what you want for total volume. For one specific event, use its name: ```json { "event_type": "subscription_created" } ``` ## Encoding the e parameter This is where most of the pain is. `e` is JSON in a query string, so it must be encoded, and an unencoded brace will either 400 or silently mangle. ```js const url = new URL("https://amplitude.com/api/2/events/segmentation"); url.searchParams.set("e", JSON.stringify({ event_type: "_active" })); url.searchParams.set("start", "20261001"); url.searchParams.set("end", "20261007"); url.searchParams.set("i", "1"); url.searchParams.set("m", "totals"); ``` `URLSearchParams` encodes for you. Building the string by hand is how the quotes get lost. ## m decides totals against uniques | `m` | What you get | | --------- | ---------------------------- | | `totals` | Every event, repeats counted | | `uniques` | Distinct users who fired it | | `avg` | Average per active user | `totals` for volume. `uniques` for "how many people did this", which is a different and usually more useful question, and the one people reach for `totals` by mistake to answer. ## Filtering on a property ```json { "event_type": "subscription_created", "filters": [ { "subprop_type": "event", "subprop_key": "plan", "subprop_op": "is", "subprop_value": ["annual"] } ] } ``` `subprop_value` is an array even for one value. A string there is a quiet no-match rather than an error, so a filtered figure that returns zero is worth checking against the unfiltered one before you believe it. ## What a raw event count is good for Amplitude bills on event volume, so this number is close to a live view of next month's invoice. That is the use case where it is unambiguous. As a product signal it is weak. A single component that starts firing twice doubles your volume with nobody new arriving, and a tracking change looks identical to a usage change in this series. Put [active users](/sources/amplitude/active-users) beside it. Events climbing while active users hold flat means you shipped more instrumentation. Both climbing together means something actually happened. ## No autocapture, which helps Unlike some product analytics tools, Amplitude does not autocapture by default. Every event in that total is one your instrumentation deliberately sent. That makes the figure a cleaner product signal than an autocaptured one, and a less useful proxy for user activity, because anything you forgot to instrument is invisible rather than approximated. ## In the notch Notch Metrics reports [Amplitude events](/sources/amplitude/events) as every event recorded in the window, of every type, bucketed daily, weekly or monthly in your project's own timezone, with a sparkline and a comparison against the previous window. [Everything Amplitude reports](/sources/amplitude), or [download Notch Metrics](/download). ## Questions **Which Amplitude endpoint returns event counts?** /api/2/events/segmentation, with an e parameter holding a URL-encoded JSON object. The users endpoint answers active and new users and does not count events. **What does event_type _active mean in Amplitude?** Any event, of any type. It is one of two reserved types, alongside _new, which matches a user's first event. For a specific event, pass its name instead. **Why does my Amplitude segmentation call return a 400?** Usually the e parameter is not URL encoded. It is JSON inside a query string, so braces and quotes must be escaped. Use URLSearchParams rather than building the string by hand. **What is the difference between totals and uniques in Amplitude?** totals counts every event with repeats included. uniques counts distinct users who fired it. People often reach for totals to answer how many people did something, which uniques is the right metric for. **Is Amplitude event volume a good product metric?** It is a better billing signal than a product one, because Amplitude meters on event volume. A component that starts firing twice doubles the number with nobody new arriving, so place active users beside it. --- # How to count new against returning Shopify customers URL: https://notchmetrics.com/blog/count-new-against-returning-shopify-customers Published: 11 October 2026 Author: Notch Metrics Tags: shopify, customers, api ## What makes a Shopify customer new? Their first order falls inside your window. That is it, and the complication is entirely in how Shopify records customers in the first place. ## Shopify creates a customer at checkout You do not need an account for Shopify to record you. Place an order as a guest and Shopify creates a customer record from the email on the order. That is convenient and it means the customer list is not a list of registered users. It contains buyers, and it also contains anybody who signed up for the newsletter, which is the part that quietly inflates every count built on it. ## Two different numbers **New customers** counts customer records created in the window. It is the flow. **Customers** counts every record the store has ever held. It is a lifetime total, it only goes up, and a window selector does nothing to it. Those answer different questions and reporting either as "customers" without saying which is how two people in the same company end up arguing about a number they both read correctly. ## The query ```graphql query Customers($query: String!, $cursor: String) { customers(first: 250, query: $query, after: $cursor) { pageInfo { hasNextPage endCursor } nodes { createdAt numberOfOrders amountSpent { amount currencyCode } } } } ``` With a search query of `created_at:>='2026-10-01T00:00:00Z' created_at:<'2026-10-08T00:00:00Z'`. `numberOfOrders` is the useful field here. A record with zero orders is a newsletter signup, not a buyer, and filtering them out gives you first-time buyers rather than first-time records. ```js const firstTimeBuyers = nodes.filter((c) => Number(c.numberOfOrders) > 0); ``` ## Returning customers are a subtraction Shopify does not report returning customers directly. Over a window: ``` returning = distinct customers who ordered minus customers whose first order was in the window ``` The first half means paging orders and collecting distinct customer ids, not paging customers. An order carries its customer, and that is the only way to know who bought in a window rather than who was created in it. ## Guest checkout inflates everything A shopper who checks out with a different email each time is a new customer each time. Shopify matches on email, and on the account where one exists. On a store with no account requirement this is a meaningful distortion, and there is no API field that fixes it. Read the direction of the number rather than the absolute value, and be suspicious of a repeat-purchase rate that looks unusually poor. ## Deletions A customer record removed for a GDPR erasure request is gone, not flagged. So the lifetime total can fall, despite being a number that otherwise only goes up. Worth knowing before you treat a dip as a data problem. ## In the notch Notch Metrics reports [Shopify new customers](/sources/shopify/new-customers) as customer records created in the window, and [customers](/sources/shopify/customers) as the lifetime total, which is a snapshot with no sparkline because a window does not apply to it. Both pages say plainly what they include, newsletter signups and all, because a count whose definition is hidden is a count you cannot act on. [Everything Shopify reports](/sources/shopify), or [download Notch Metrics](/download). ## Questions **Does Shopify create a customer record for guest checkout?** Yes. Shopify creates a customer from the email on the order whether or not an account exists, so the customer list is a list of buyers and newsletter signups rather than registered users. **How do I count first-time buyers in Shopify?** Query customers created in the window and filter to those whose numberOfOrders is greater than zero. A record with zero orders is a newsletter signup, not a buyer. **How do I count returning Shopify customers?** Subtract. Page orders in the window and collect distinct customer ids, then subtract the customers whose first order fell in that window. Shopify does not report returning customers directly, and paging customers tells you who was created rather than who bought. **Why does my Shopify repeat purchase rate look poor?** Often guest checkout. A shopper using a different email each time is a new customer each time, because Shopify matches on email and on account. There is no API field that fixes it, so read the direction rather than the absolute value. **Can the Shopify total customer count go down?** Yes. A record removed for a GDPR erasure request is deleted rather than flagged, so the lifetime total falls. It is otherwise a number that only goes up. --- # How to count new users in GA4 URL: https://notchmetrics.com/blog/count-new-users-in-ga4 Published: 11 October 2026 Author: Notch Metrics Tags: google-analytics, ga4, api ## The call ```json { "dateRanges": [{ "startDate": "2026-10-01", "endDate": "2026-10-07" }], "dimensions": [{ "name": "date" }], "metrics": [{ "name": "newUsers" }] } ``` `newUsers`, not `ga:newUsers`. The UA prefix is gone. ## What GA4 means by new A user who triggered `first_visit` on web, or `first_open` in an app, during the window. GA4 fires that event when it sees a client with no existing analytics cookie or app instance id. So "new" means "no cookie", which is not the same as "never been here before". ## Four things that create a new user who is not **Clearing cookies.** The same person, back tomorrow, is new. **A different browser or device.** Unless your property has User-ID configured and the person is signed in, a laptop and a phone are two users and both were new once. **Safari and ITP.** Intelligent Tracking Prevention caps client-side cookie lifetime, so a returning Safari visitor past the window is new again. On a consumer site with heavy Safari traffic this inflates new users materially and permanently. **Consent mode.** With consent denied, GA4 models behaviour rather than observing it. The number is an estimate, and whether it is modelled depends on your consent rates, which change when you change your banner. None of those are bugs. They are the limits of what a cookie can know, and they mean a new-user trend is comparable against itself only while your consent setup and traffic mix hold still. ## Totals, again `newUsers` de-duplicates within the window. Somebody whose `first_visit` lands on Tuesday is new once in the week and once on Tuesday, so summing seven daily rows overcounts the weekly figure. Ask for the total explicitly with `metricAggregations: ["TOTAL"]`, or drop the `date` dimension. In practice the gap is smaller here than for active users, because a person can only be new once in their life. It is not zero, because window boundaries and modelling both move. ## New against active The pair that means something. - **New flat, active rising.** Retention improving. - **New rising, active flat.** Acquisition working, nothing sticking. - **New rising, active falling.** Paying for traffic that leaves faster than it arrives. A new-user chart alone rewards whoever buys the most traffic, which is rarely the outcome anybody wanted to incentivise. ## First-time users against new users GA4 also exposes `firstTimePurchasers` and, in some contexts, a `newVsReturning` dimension. The dimension is a useful cross-check: a breakdown by it against a `sessions` metric tells you how much of your traffic is returning, which is the retention question the new-user count only half answers. ## Thresholding Rows with very few users are withheld, particularly where demographics are involved. A breakdown of new users by country can add up to less than the total, and a missing country is not a country with zero visitors. ## In the notch Notch Metrics reports [GA4 new users](/sources/google-analytics/new-users) in the MacBook notch, daily, weekly or monthly, with a sparkline and a comparison against the previous window. Place it beside [active users](/sources/google-analytics/active-users) and [sessions](/sources/google-analytics/sessions). [Everything Google Analytics reports](/sources/google-analytics), or [download Notch Metrics](/download). ## Questions **What is the GA4 metric for new users?** newUsers. It counts users who triggered first_visit on web or first_open in an app during the window, which GA4 fires when it sees a client with no existing analytics cookie or app instance id. **Why are my GA4 new users higher than my real signups?** Because new means no cookie rather than never been here. Cleared cookies, a second device, Safari's Intelligent Tracking Prevention capping cookie lifetime, and consent-mode modelling all create a new user who is not a new person. **Does Safari ITP affect GA4 new user counts?** Yes, and permanently. ITP caps client-side cookie lifetime, so a Safari visitor returning after that window is counted as new again. On a consumer site with heavy Safari traffic this inflates new users materially. **Can I sum daily GA4 new users for a weekly total?** Not exactly. newUsers de-duplicates within the window, so summing seven daily rows overcounts. The gap is smaller than for active users because a person can only be new once, but window boundaries and modelling still move it. **What should I read alongside GA4 new users?** Active users. New rising while active stays flat means acquisition is working and nothing is sticking. A new-user chart alone rewards whoever buys the most traffic. --- # How to count open issues without counting pull requests URL: https://notchmetrics.com/blog/count-open-issues-without-pull-requests Published: 11 October 2026 Author: Notch Metrics Tags: github, issues, api ## Why does my GitHub issue count look wrong? Because `open_issues_count` on the repository object includes open pull requests. ```bash curl -s https://api.github.com/repos/facebook/react | jq '.open_issues_count' ``` That number is issues plus pull requests. On a repository with an active contribution flow it can be nearly double the backlog you see on the Issues tab, and nothing in the field name warns you. ## The reason, which is not a bug In GitHub's data model a pull request **is** an issue. It has an issue number, it shares the numbering sequence with issues, it has comments and labels and assignees like an issue, and it carries an extra `pull_request` object with the branch details. That is why issue numbers and pull request numbers never collide in a repository: they come from one counter. It is also why the REST issues list returns pull requests: ```bash # Returns issues AND pull requests. curl -s 'https://api.github.com/repos/facebook/react/issues?state=open' ``` The documented workaround for that list is to filter client-side on the presence of `pull_request`: ```js const realIssues = items.filter((item) => !item.pull_request); ``` Which works, and means paging everything to count anything. ## The query that just answers it The search API understands the distinction natively. ```bash curl -s 'https://api.github.com/search/issues?q=repo:facebook/react+is:issue+is:open&per_page=1' \ -H 'Accept: application/vnd.github+json' \ -H 'Authorization: Bearer ' | jq '.total_count' ``` `per_page=1` because you want `total_count`, not the rows. One request, no paging, regardless of backlog size. `is:issue` excludes pull requests by type rather than by filtering them out afterwards, so the figure matches what the Issues tab shows. ## The search API has its own rate limit Worth knowing before you build on it. Search is limited separately from the rest of the REST API: 30 requests per minute authenticated, 10 unauthenticated, against 5,000 per hour for everything else. For one number refreshed every few minutes that is plenty. For a dashboard polling fifty repositories every thirty seconds it is not, and you will get 403s that look like permission problems. `total_count` is also capped at 1,000 for results you page through, though the count itself is reported accurately above that. ## Reading the number A backlog climbing steadily is triage falling behind, which is not the same as quality falling. A figure that drops in one step is almost always a bulk close rather than a productive afternoon. The pair worth placing is open issues against open pull requests. Issues flat and pull requests climbing is a review bottleneck, and that is the one that loses contributors: people will wait for a bug to be fixed, but they do not submit a second pull request after the first one sat for a month. ## In the notch Notch Metrics counts [GitHub open issues](/sources/github/open-issues) through the search API with `is:issue is:open`, so the figure is the backlog rather than the backlog plus the contribution queue. The page says so, because a number that disagrees with the obvious API field needs to explain itself. [Open pull requests](/sources/github/open-pull-requests) is the other half. [Everything GitHub reports](/sources/github), or [download Notch Metrics](/download). ## Questions **Why does GitHub open_issues_count include pull requests?** Because in GitHub's data model a pull request is an issue. It shares the numbering sequence with issues and carries an extra pull_request object, which is also why issue and pull request numbers never collide in a repository. **How do I count only open issues on GitHub?** Use the search API: GET /search/issues?q=repo:owner/name+is:issue+is:open with per_page=1 and read total_count. is:issue excludes pull requests by type, so the figure matches the Issues tab. **Does the GitHub issues list endpoint return pull requests?** Yes. GET /repos/{owner}/{repo}/issues returns both. The documented workaround is to filter client-side on the presence of a pull_request field, which means paging everything to count anything. **What is the GitHub search API rate limit?** 30 requests per minute authenticated and 10 unauthenticated, separate from the 5,000 per hour that covers the rest of the REST API. Exceeding it returns 403s that look like permission problems. **Should I watch open issues or open pull requests?** Both, as a pair. Issues flat while pull requests climb is a review bottleneck, and that is the one that loses contributors: people will wait for a bug fix, but they do not submit a second pull request after the first sat for a month. --- # How to count pageviews and events with PostHog HogQL URL: https://notchmetrics.com/blog/count-pageviews-and-events-with-posthog-hogql Published: 11 October 2026 Author: Notch Metrics Tags: posthog, events, api ## The query ```sql SELECT toUnixTimestamp(toStartOfDay(timestamp)) AS bucket, count() AS value FROM events WHERE timestamp >= toDateTime({from}) AND timestamp < toDateTime({to}) GROUP BY bucket ORDER BY bucket ``` That is every event. The interesting question is whether that is what you meant. ## Autocapture is probably most of your volume If autocapture is enabled, PostHog records a `$autocapture` event for most clicks and form interactions on the page, plus `$pageview` and `$pageleave` on top of your own named events. A product with autocapture on produces several times the volume its own instrumentation would suggest. That makes a raw event count: - **A good billing signal.** PostHog meters on events ingested, so this number is roughly next month's invoice. - **A weak product signal.** One noisy component can double it with nobody new arriving. Filter if you want a product number: ```sql -- Your own events only. WHERE NOT startsWith(event, '$') ``` PostHog prefixes its own events with `$`, so that one clause separates your instrumentation from the framework's opinion of what a click is. ## Pageviews ```sql SELECT toUnixTimestamp(toStartOfDay(timestamp)) AS bucket, count() AS value FROM events WHERE event = '$pageview' AND timestamp >= toDateTime({from}) AND timestamp < toDateTime({to}) GROUP BY bucket ORDER BY bucket ``` `count()` here, not `uniq()`. Repeat views by one person all count, which is what a pageview is. ## The single-page-app caveat On a server-rendered site the snippet fires one `$pageview` per navigation and the number means what you expect. In a single-page application it means whatever your router was wired to send. Some apps fire one per route change. Some fire one per modal. Some fire none after the first load, because the integration was finished enough to ship and never revisited. If your pageview count is an order of magnitude away from your server logs, check the instrumentation before you check the traffic. ## Counting a specific event ```sql WHERE event = 'subscription_created' ``` And for a property: ```sql WHERE event = 'subscription_created' AND properties.plan = 'annual' ``` Properties are a JSON column, accessed with dot notation. A property that does not exist returns null rather than erroring, so a typo in a property name silently returns zero. That is worth a sanity check against a known total before you trust any filtered figure. ## Performance `events` is large. Two things keep a query fast. **Always bound the timestamp.** An unbounded scan over a busy project is slow enough to time out, and the first clause of every `WHERE` should be the window. **Filter on `event` before properties.** `event` is indexed in a way that properties are not, so narrowing by event name first makes a property filter cheap instead of catastrophic. ## Or read it off the notch Notch Metrics reports [PostHog events](/sources/posthog/events) as everything ingested in the window and [pageviews](/sources/posthog/pageviews) as `$pageview` events, both bucketed hourly, daily, weekly or monthly with a sparkline and a comparison against the previous window. Put [active users](/sources/posthog/active-users) beside events, and events climbing while active users hold flat means you shipped more tracking rather than more usage. [Everything PostHog reports](/sources/posthog), or [download Notch Metrics](/download). ## Questions **How do I count events in PostHog with HogQL?** SELECT count() FROM events with a bounded timestamp range, grouped by toStartOfDay for a series. Always bound the timestamp first: an unbounded scan over a busy project is slow enough to time out. **Does PostHog autocapture inflate my event count?** Yes, usually by several times. Autocapture records a $autocapture event for most clicks and form interactions, on top of $pageview, $pageleave and your own events. Filter with NOT startsWith(event, '$') for your own instrumentation only. **How do I count pageviews in PostHog?** Filter on event = '$pageview' and use count(), not uniq(). Repeat views by one person all count, which is what a pageview is. **Why is my PostHog pageview count wrong in a single-page app?** Because it reflects whatever your router was wired to send. Some apps fire one $pageview per route change, some per modal, and some none after the first load. If the figure is an order of magnitude off, check the instrumentation before the traffic. **Why does my filtered PostHog query return zero?** Often a typo in a property name. Properties are a JSON column and a missing one returns null rather than erroring, so a filter on it silently matches nothing. Sanity check against a known unfiltered total. --- # Dodo Payments revenue in the notch URL: https://notchmetrics.com/blog/dodo-payments-revenue-in-the-notch Published: 11 October 2026 Author: Notch Metrics Tags: dodopayments, revenue, mac Dodo Payments is a merchant of record for software, which means it takes the tax and compliance problem off your desk. The numbers still live in a dashboard, and the dashboard is still a tab. ## How do you watch Dodo Payments revenue without a tab? Notch Metrics puts it in the MacBook notch. Collapsed, a thin strip of figures. Hover the camera housing and it expands over whatever you were doing. Move the cursor away and it folds back to the shape it had. ## What Dodo Payments reports | Number | What it is | | -------------------------------------------------------------- | ------------------------------------------- | | [MRR](/sources/dodopayments/mrr) | Active subscriptions, normalised to a month | | [Total revenue](/sources/dodopayments/revenue) | Every succeeded payment in the window | | [One-time sales](/sources/dodopayments/one-time-sales) | Payments not tied to a subscription | | [Active subscribers](/sources/dodopayments/active-subscribers) | Status `active` right now | | [Recent payments](/sources/dodopayments/recent-payments) | The last ten succeeded payments | The split between total revenue and one-time sales is the pair worth placing. Recurring revenue is a book that carries into next month. A one-time sale is money that has to be earned again. A flat total can hide a collapse in one covered by a good week of the other. ## Live and test mode Dodo Payments runs test and live on separate hosts. The connector asks which mode your key belongs to, because a test key against the live host returns an authentication error rather than an empty account. [What the Dodo Payments connector asks for](/sources/dodopayments). Your API key is encrypted on the server with a key unique to that one connection, and bound to your account and the provider so a row copied elsewhere does not decrypt. The Mac app never holds it. ## Alongside everything else Revenue also comes from [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Lemon Squeezy](/sources/lemonsqueezy) and [RevenueCat](/sources/revenuecat). Analytics from [Plausible](/sources/plausible) and [PostHog](/sources/posthog). Search from [Search Console](/sources/google-search-console). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year with every connector included. [Download Notch Metrics](/download). ## Questions **How do I see Dodo Payments revenue on my Mac?** Install Notch Metrics, connect Dodo Payments on the web with an API key, and place the revenue widget in the MacBook notch. Hover the camera housing and the figure is there. **Does Notch Metrics separate Dodo Payments subscriptions from one-time sales?** Yes, as two widgets. Total revenue counts every succeeded payment, and one-time sales counts only payments not tied to a subscription. Placing both shows whether a steady total is hiding movement underneath. **Does Notch Metrics support Dodo Payments test mode?** Yes. The connector asks which mode your API key belongs to, because Dodo Payments runs test and live on separate hosts and a test key against the live host fails to authenticate. **Is my Dodo Payments API key stored on my Mac?** No. It is encrypted on the server with a key unique to that one connection, bound to your account and the provider. The Notch Metrics server calls Dodo Payments and sends your Mac a normalised number. **What does Dodo Payments MRR count?** Active subscriptions, each recurring pre-tax amount normalised to a monthly amount using its payment frequency. It is a snapshot of the book right now rather than a total over the window. --- # GA4 visitors in your MacBook notch URL: https://notchmetrics.com/blog/ga4-visitors-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: google-analytics, ga4, mac GA4 is thorough and slow. Loading a report to read one number is a disproportionate amount of clicking. ## How do you watch GA4 without opening GA4? Notch Metrics puts the figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What Google Analytics reports | Number | What it is | | ------------------------------------------------------ | -------------------------------- | | [Active users](/sources/google-analytics/active-users) | GA4 `activeUsers`, de-duplicated | | [New users](/sources/google-analytics/new-users) | First-time users in the window | | [Sessions](/sources/google-analytics/sessions) | Sessions started in the window | | [Page views](/sources/google-analytics/pageviews) | GA4 `screenPageViews` | ## The de-duplication that explains the chart The headline active user figure is de-duplicated over the whole window, as Google reports it. The chart is per day, so its bars do not add up to the headline: one person on three days is one user and three bars. A weekly active user figure is not the sum of seven daily ones. Any tool whose bars sum to its headline is showing you daily actives seven times. ## OAuth, and picking a property Google Analytics connects over OAuth with read-only analytics scope. After you approve, you pick which GA4 property to read, because a Google account usually has several and guessing would be worse than asking. [What the Google Analytics connector asks for](/sources/google-analytics). Nothing is typed in. The token is encrypted on the server and never reaches your Mac. ## Universal Analytics is gone This is GA4 only. Universal Analytics stopped processing data and its reporting API is no longer the one to build against. If you are looking at a property id beginning with `UA-`, there is nothing to connect. ## Beta The connector is marked beta on [its page](/sources/google-analytics). It connects and returns figures; the edges are still being worked on. ## Alongside everything else Revenue from [Stripe](/sources/stripe), search from [Search Console](/sources/google-search-console), privacy-friendly analytics from [Plausible](/sources/plausible), product analytics from [PostHog](/sources/posthog). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see GA4 visitors on my Mac?** Install Notch Metrics, connect Google Analytics over OAuth, pick the GA4 property, and place the active users widget in the MacBook notch. Hover the camera housing and the figure is there. **Does Notch Metrics work with Universal Analytics?** No. This is GA4 only. Universal Analytics stopped processing data and its reporting API is no longer the one to build against, so a property id beginning with UA- has nothing to connect. **Why do GA4 daily bars not add up to the weekly figure?** Because active users is de-duplicated over the whole window, as Google reports it. One person on three days is one user and three bars, so a weekly figure is not the sum of seven daily ones. **What Google permissions does Notch Metrics ask for?** Read-only analytics scope, and nothing else. Nothing is typed in because Google Analytics connects over OAuth, and the resulting token is encrypted on the server rather than stored on your Mac. **Why does Notch Metrics ask me to pick a GA4 property?** Because a Google account usually has several and reading the wrong one would be worse than asking. The connector pauses after you authorise so you can choose. --- # How to get a repository's star count from the GitHub API URL: https://notchmetrics.com/blog/get-a-repository-star-count-from-the-github-api Published: 11 October 2026 Author: Notch Metrics Tags: github, stars, api ## How do you get a repository's star count? One call, one field. ```bash curl -s https://api.github.com/repos/vercel/next.js \ -H 'Accept: application/vnd.github+json' | jq .stargazers_count ``` `stargazers_count` is the current total. There is also `watchers_count` on the same object, which for historical reasons is a duplicate of the star count and not the number of watchers. If you want actual watchers, that is `subscribers_count`. Three fields, two of which mean something other than their name. It catches everybody once. | Field | What it actually is | | ------------------- | ------------------- | | `stargazers_count` | Stars | | `watchers_count` | Stars, again | | `subscribers_count` | Watchers | ## Authentication, and why you want it even for public data The endpoint works unauthenticated at 60 requests per hour per IP. With a token it is 5,000. Any polling at all needs the token, and the call is the same with an `Authorization: Bearer` header added. A fine-grained token with public repository read access is enough. Do not use a classic token with `repo` scope to read a public star count: that scope grants write access to every private repository you can see, for a number anybody can read logged out. ## Star history is the hard part The current count is trivial. "Stars per day for the last month" is not, and this is what most people are actually asking for. There is no endpoint for it. What exists is the stargazers list with a preview media type that adds the timestamp: ```bash curl -s 'https://api.github.com/repos/vercel/next.js/stargazers?per_page=100&page=1' \ -H 'Accept: application/vnd.github.star+json' ``` Without `application/vnd.github.star+json` you get user objects with no `starred_at`, which is the quiet failure here: the call succeeds and the field you need is simply absent. The real problem is volume. The list pages at 100 and GitHub caps pagination at 400 pages, so a repository past 40,000 stars cannot be fully walked at all. Even below that, 40,000 stars is 400 requests for one chart. That is why every star history site you have used is a service that has been recording the number daily since before you needed it. They are not reading history, they are replaying their own. ## If you only need recent history Page the stargazers list from the end. Stars are returned oldest first, so the last page holds the newest, and `Link: rel="last"` tells you where that is. ```js const res = await fetch(`https://api.github.com/repos/${repo}/stargazers?per_page=100`, { headers: { Accept: "application/vnd.github.star+json", Authorization: `Bearer ${token}` }, }); // Parse the Link header to find rel="last", then walk backwards from there. ``` A handful of calls gets you the last few hundred stars with timestamps, which is enough for a recent trend and costs nothing like a full walk. ## What a star is not A star is a bookmark. It is not a download, not an install and not a user. A project with ten thousand stars may have a hundred people running it, and a tool with four hundred stars may be in production at four hundred companies. Read it as attention. If you want adoption, that number is somewhere else entirely. ## In the notch Notch Metrics reports [GitHub stars](/sources/github/stars) as the current count, with the repository picked per widget. No sparkline, because the API does not offer one without paging the full history on every refresh, and inventing a chart from a single number would be worse than not drawing one. [Forks](/sources/github/forks), [open issues](/sources/github/open-issues) and [open pull requests](/sources/github/open-pull-requests) sit beside it. [Everything GitHub reports](/sources/github), or [download Notch Metrics](/download). ## Questions **Which GitHub API field is the star count?** stargazers_count on the repository object. Note that watchers_count is a duplicate of the star count for historical reasons, and the actual watcher count is subscribers_count. **Can I get GitHub star history from the API?** Not directly. There is no history endpoint. The stargazers list returns starred_at only if you send the Accept header application/vnd.github.star+json, and it pages at 100 with a 400-page cap, so repositories past 40,000 stars cannot be fully walked. **Why is starred_at missing from the GitHub stargazers response?** Because the preview media type is not set. Send Accept: application/vnd.github.star+json. Without it the call still succeeds and returns plain user objects with no timestamp, which is a quiet failure. **What is the GitHub API rate limit for star counts?** 60 requests per hour unauthenticated, 5,000 with a token. Any polling needs a token. A fine-grained token with public repository read access is enough; a classic token with repo scope grants far more than reading a public count requires. **Does a GitHub star mean somebody uses the project?** No. A star is a bookmark, not a download, an install or a user. Read it as attention rather than adoption. --- # How to get active users from the GA4 Data API URL: https://notchmetrics.com/blog/get-active-users-from-the-ga4-data-api Published: 11 October 2026 Author: Notch Metrics Tags: google-analytics, ga4, api ## The endpoint GA4 reporting is one POST to the Data API. ```bash curl -s -X POST \ 'https://analyticsdata.googleapis.com/v1beta/properties/123456789:runReport' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "dateRanges": [{ "startDate": "2026-10-01", "endDate": "2026-10-07" }], "dimensions": [{ "name": "date" }], "metrics": [{ "name": "activeUsers" }] }' ``` The property id is numeric, and it goes in the URL path. It is not the measurement id beginning with `G-`, which is the one in your tracking snippet. Mixing them up returns a 403 that reads like a permissions problem. ## The metric names changed If you are porting anything from Universal Analytics, the vocabulary moved. | What you want | UA | GA4 | | ------------- | -------------- | ----------------- | | Users | `ga:users` | `activeUsers` | | New users | `ga:newUsers` | `newUsers` | | Sessions | `ga:sessions` | `sessions` | | Pageviews | `ga:pageviews` | `screenPageViews` | `screenPageViews` is the one that catches people. There is no `pageViews` in GA4: the name folds app screens and web pages into one metric, because GA4 treats both as the same kind of thing. A metric name GA4 does not know returns a 400 naming the field, which is at least a clear failure. ## The response, and where the total is ```json { "dimensionHeaders": [{ "name": "date" }], "metricHeaders": [{ "name": "activeUsers", "type": "TYPE_INTEGER" }], "rows": [{ "dimensionValues": [{ "value": "20261001" }], "metricValues": [{ "value": "120" }] }], "rowCount": 7 } ``` Values are **strings**, including integers. `"120"`, not `120`. Summing them without converting concatenates, which produces an absurd number rather than a subtly wrong one, so at least it fails loudly. Dates come back as `YYYYMMDD` with no separators. ## Do not sum the rows This is the central thing. Summing `activeUsers` across seven daily rows does not give you weekly active users. Each row is de-duplicated within its own day, and a person active on three days appears in three rows. For the de-duplicated total, ask for it: ```json { "dateRanges": [{ "startDate": "2026-10-01", "endDate": "2026-10-07" }], "metrics": [{ "name": "activeUsers" }], "metricAggregations": ["TOTAL"] } ``` The result arrives in `totals`, separate from `rows`, and it is correctly de-duplicated across the window. Or run the query with no `date` dimension at all, which gives you a single row that means the same thing. Either way, the headline and the chart are two requests or one request with aggregations, and they will not match. That is right. ## Data is sampled and thresholded Two caveats worth carrying with any GA4 figure. **Sampling.** Large properties with complex queries get sampled results, and the response says so. Check for it rather than assuming. **Thresholding.** GA4 withholds rows with very few users to protect identity, especially when demographics are involved. So a breakdown can add up to less than its total, and a missing row is not a zero. ## Authentication OAuth with `https://www.googleapis.com/auth/analytics.readonly`, or a service account added as a viewer on the property. The service account route is better for anything unattended, because an OAuth refresh token is one revocation away from a silent outage. ## Or read it off the notch Notch Metrics calls `runReport` and shows [GA4 active users](/sources/google-analytics/active-users), [new users](/sources/google-analytics/new-users), [sessions](/sources/google-analytics/sessions) and [page views](/sources/google-analytics/pageviews) in the MacBook notch, with the headline de-duplicated over the window and the chart per day, which the page explains rather than leaving you to notice. [Everything Google Analytics reports](/sources/google-analytics), or [download Notch Metrics](/download). ## Questions **What is the GA4 Data API endpoint for a report?** POST to https://analyticsdata.googleapis.com/v1beta/properties/{propertyId }:runReport. The property id is the numeric one, not the measurement id beginning with G- from your tracking snippet. **What is the GA4 metric name for pageviews?** screenPageViews. There is no pageViews metric in GA4: the name folds app screens and web pages into one metric because GA4 treats both as the same kind of thing. **Can I sum daily GA4 active users to get a weekly figure?** No. Each daily row is de-duplicated within its own day, so a person active on three days appears in three rows. Ask for metricAggregations TOTAL, or run the query without a date dimension, to get the de-duplicated window figure. **Why are GA4 Data API numbers returned as strings?** Google serialises metric values as strings, including integers. Convert before arithmetic, or summing them concatenates. The failure is loud rather than subtle, which helps. **Why does my GA4 breakdown add up to less than the total?** Thresholding. GA4 withholds rows with very few users to protect identity, especially where demographics are involved. A missing row is not a zero. --- # How to get average position and CTR from Search Console URL: https://notchmetrics.com/blog/get-average-position-and-ctr-from-search-console Published: 11 October 2026 Author: Notch Metrics Tags: google-search-console, seo, api ## Both numbers are ratios, and that changes the arithmetic Clicks and impressions are counts. CTR and position are not: they are computed over whatever the row covers, and they cannot be added, averaged across rows, or summed. ```json { "startDate": "2026-10-01", "endDate": "2026-10-07", "dimensions": [] } ``` No dimensions, one row, and Google computes both correctly over the whole window. That is the only figure you should quote as "our CTR". ## Why the headline does not match the chart Ask for `dimensions: ["date"]` and each row holds that day's own ratio. The windowed ratio is clicks over impressions across all seven days, which weights each day by its impressions. A Sunday with ten impressions and a Monday with ten thousand carry their real weight in the headline and equal weight in a naive average of the points. The difference can be large, and a tool whose headline equals the mean of its points is doing the wrong calculation. So the chart and the headline disagreeing is correct, and worth saying in the interface before somebody files it as a bug. ## What average position actually describes The position of the topmost result for your site, averaged over every impression it received. Three consequences. **It mixes queries that have nothing to do with each other.** Ranking first for a term nobody searches and fortieth for the term everyone does averages to something in the middle, describing neither. **It moves when nothing about you changed.** Start appearing fifth for ten new long-tail queries and your average drops even though every existing position held exactly. **It is impression-weighted.** A position change on a high-volume query moves it far more than the same change on a quiet one. Treat a shift as a prompt to look at the query breakdown, not as a result. ## Why a falling CTR is often good CTR falls when a page starts ranking for broader terms. More impressions at worse positions means a smaller share of them convert, and the page is earning more clicks in total while the percentage drops. Three things that lower CTR without anything going wrong: - Ranking for more, and therefore more loosely matched, queries. - Google answering the query on the results page, which is outside your control. - A rich result above you taking the clicks. Always read CTR with impressions and position beside it. Alone it is the most misinterpreted number Search Console publishes. ## The pair that is actually actionable Page-level rows where impressions are high and position is between roughly five and fifteen. Those pages are visible and not quite winning, and a title or description change has somewhere to move them. ```json { "startDate": "2026-09-01", "endDate": "2026-10-01", "dimensions": ["page"], "rowLimit": 500 } ``` Filter client-side on position and sort by impressions. A page at position two with a low CTR is a different problem, usually a snippet one. ## The lag applies here too Both ratios settle over two to three days along with their inputs. A CTR for yesterday is computed from incomplete clicks and incomplete impressions, and the two do not necessarily settle at the same rate. End your window three days back for anything you intend to compare. ## In the notch Notch Metrics reports [Search Console CTR](/sources/google-search-console/ctr) as clicks divided by impressions over the window, rather than the mean of daily rates, and [average position](/sources/google-search-console/average-position) with one decimal place and a note that lower is better. Both say on their pages how they are computed, because a ratio whose definition is hidden is a ratio somebody will misread. [Everything Search Console reports](/sources/google-search-console), or [download Notch Metrics](/download). ## Questions **How is Search Console CTR calculated?** Clicks divided by impressions over whatever the row covers. For a window figure, ask with no dimensions and Google computes it across the whole range, weighting each day by its impressions. **Why does my CTR headline not match the average of the daily values?** Because the headline weights each day by its impressions and a naive average weights them equally. A Sunday with ten impressions should not count as much as a Monday with ten thousand, and a tool whose headline equals the mean of its points is doing the wrong calculation. **Is a falling Search Console CTR bad?** Often not. CTR falls when a page starts ranking for broader terms, so more impressions at worse positions convert at a lower rate while total clicks rise. It also falls when Google answers the query on the results page, which is outside your control. **What does Search Console average position mean?** The position of the topmost result for your site, averaged over every impression. It mixes unrelated queries, is weighted by impressions, and can move when nothing about your site changed, such as when you start ranking for new long-tail terms. **Which pages are worth optimising from Search Console?** Pages with high impressions sitting roughly between position five and fifteen. They are visible and not quite winning, so a title or description change has somewhere to move them. A page at position two with low CTR is usually a snippet problem instead. --- # How to get daily sales totals from the Shopify Admin API URL: https://notchmetrics.com/blog/get-daily-sales-totals-from-the-shopify-admin-api Published: 11 October 2026 Author: Notch Metrics Tags: shopify, sales, api ## Does Shopify have a sales endpoint? No. The admin shows a sales figure; the API does not return one. You query orders and sum them, and the two decisions that matter are which API and which money field. ## Use GraphQL, not REST Shopify's REST Admin API is legacy and the order endpoints there are capped and deprecated. The GraphQL Admin API is where new work belongs, and for this job it is also genuinely better: one query returns exactly the fields you want instead of a full order object per row. ```graphql query Orders($query: String!, $cursor: String) { orders(first: 250, query: $query, after: $cursor, sortKey: CREATED_AT) { pageInfo { hasNextPage endCursor } nodes { createdAt displayFinancialStatus currentTotalPriceSet { shopMoney { amount currencyCode } } } } } ``` POST it to `https://{shop}.myshopify.com/admin/api/{version}/graphql.json` with an `X-Shopify-Access-Token` header. ## The query string is a search syntax `query` is not GraphQL, it is Shopify's own search language, passed as a string. ``` created_at:>='2026-10-01T00:00:00Z' created_at:<'2026-10-08T00:00:00Z' ``` Terms are space-separated and implicitly ANDed. Quote the timestamps. A malformed term is usually ignored rather than rejected, which means a typo returns every order in the store instead of an error, and the first sign of trouble is a sales figure that is far too high. ## Which money field Shopify has several and they answer different questions. | Field | What it is | | ---------------------- | -------------------------------------- | | `totalPriceSet` | The order total when it was placed | | `currentTotalPriceSet` | The total now, after edits and refunds | | `subtotalPriceSet` | Before tax and shipping | | `totalReceivedSet` | What was actually captured | `currentTotalPriceSet` is usually what you want for a sales figure, because an order edited after the fact should report its real value. If you want the original, use `totalPriceSet` and be aware that your total will not reconcile against anything. Every one of them is a `MoneySet` with two members: `shopMoney` in the store's currency and `presentmentMoney` in whatever the customer paid in. Sum `shopMoney` or you are adding yen to euros. ## Amounts are decimal strings `amount` comes back as `"129.95"`, a string. Convert to minor units as an integer before you do anything with it, and never hold money in a float. ```js const minor = Math.round(Number(amount) * 100); ``` That rounding step is not optional. `Number("129.95") * 100` is `12994.999999999998`. ## Financial status `displayFinancialStatus` tells you whether money moved: `PAID`, `PARTIALLY_PAID`, `PENDING`, `REFUNDED`, `VOIDED`, `AUTHORIZED`. Include `PAID`, `PARTIALLY_PAID` and `REFUNDED` in a gross figure. A refunded order still took money on the day it was placed, and refunds belong in their own number. Exclude `PENDING`, `VOIDED` and `AUTHORIZED`, which have not been captured. ## Pagination and the cost limit GraphQL pages with `first` and `after`, up to 250 nodes. Follow `pageInfo.endCursor` until `hasNextPage` is false. Shopify also meters by query cost rather than by request count. The response carries `extensions.cost` with your remaining points and the restore rate, and a 250-node order query is expensive. Read the throttle status and back off on it rather than retrying blindly, or a busy store will lock you out mid-sum. ## In the notch Notch Metrics reports [Shopify sales](/sources/shopify/revenue) as order totals for orders placed in the window that took payment, in the store's own currency, gross: a later refund does not remove the order from the day it was placed, and [refunds](/sources/shopify/refunds) are the other half. Hourly, daily, weekly or monthly, with a sparkline and a comparison against the previous window. [Everything Shopify reports](/sources/shopify), or [download Notch Metrics](/download). ## Questions **Which Shopify API should I use for sales totals?** The GraphQL Admin API. The REST order endpoints are legacy and capped, and GraphQL returns exactly the fields you ask for instead of a full order object per row. **What is the difference between totalPriceSet and currentTotalPriceSet in Shopify?** totalPriceSet is the order total when it was placed. currentTotalPriceSet is the total now, after any edits and refunds. For a sales figure that reconciles, currentTotalPriceSet is usually the right one. **What is shopMoney against presentmentMoney in Shopify?** shopMoney is the amount in the store's own currency and presentmentMoney is what the customer actually paid in. Sum shopMoney, or you are adding yen to euros. **Why is my Shopify sales total far too high?** Often a malformed term in the query search string. Shopify's search syntax tends to ignore a bad term rather than reject it, so a typo in a created_at filter returns every order in the store instead of an error. **How does rate limiting work on the Shopify GraphQL API?** By query cost, not request count. The response carries extensions.cost with your remaining points and restore rate. A 250-node order query is expensive, so read the throttle status and back off on it rather than retrying blindly. --- # How to get new users from Amplitude URL: https://notchmetrics.com/blog/get-new-users-from-amplitude Published: 11 October 2026 Author: Notch Metrics Tags: amplitude, users, api ## The call The same endpoint as active users, with one letter changed. ```bash curl -s -u "$API_KEY:$SECRET_KEY" \ 'https://amplitude.com/api/2/users?start=20261001&end=20261007&m=new&i=1' ``` `m=new` rather than `m=active`. Everything else is identical, including the response shape, which puts your numbers at `data.series[0]`. ## What new means A user whose first event Amplitude ever saw falls inside the window. Not somebody who signed up in the window. Not somebody who became a customer. First event, which is usually the first page they loaded or the first screen they opened, and which may be long before they created an account. That makes it a top-of-funnel number. If you want signups, that is a specific event you instrumented and the segmentation endpoint counts it. ## The thing that breaks the number Amplitude decides "first ever" from the data it holds. Three situations make that misleading. **The day you added Amplitude.** Every existing user you have is new on the day tracking went live. Your new user chart has a cliff at the start that is not a launch. **A backfill.** Importing historical events moves users' first-seen dates backwards, which rewrites your new user history. Figures you reported last month change. **An identity change.** If you alter how `user_id` is derived, every returning user looks brand new. A migration that changes id format produces a new-user spike indistinguishable from a successful campaign. That last one is worth saying out loud before anybody celebrates. ## Anonymous users count Amplitude creates a device id for a visitor with no `user_id`, and that device is a user. So new users includes people who loaded one page and never came back. Over time, identity merging ties a device to a user once they identify, and the first-seen date follows the earliest of the two. That is correct and it means a user's first-seen date can be earlier than their account creation date, which looks wrong on a join and is not. ## New against active is the useful pair Neither number means much alone. - **New flat, active rising.** Retention is improving. Existing users are coming back more. - **New rising, active flat.** Acquisition is working and nothing is sticking. The leakiest and most common shape. - **Both rising together.** Growth. - **New rising, active falling.** You are buying traffic that leaves faster than it arrives. Place them side by side or the series tells you nothing you can act on. ## Daily, weekly and monthly disagree A weekly new-user figure is not the sum of seven daily ones, for the same de-duplication reason that applies to active users. Somebody whose first event is on Tuesday is new once in the week, and once on Tuesday. In the new-user case the figures happen to be closer than they are for active users, because a person is only ever new once overall. They still are not equal, because the window boundary moves. ## In the notch Notch Metrics reports [Amplitude new users](/sources/amplitude/new-users) as users first seen in the window, daily, weekly or monthly, bucketed in your Amplitude project's own timezone. Place it beside [active users](/sources/amplitude/active-users) and [events](/sources/amplitude/events) and the three say which of the four shapes above you are in. [Everything Amplitude reports](/sources/amplitude), or [download Notch Metrics](/download). ## Questions **How do I get new users from the Amplitude API?** Call /api/2/users with m=new instead of m=active. Everything else about the request and the response shape is identical. **What counts as a new user in Amplitude?** Somebody whose first event Amplitude ever saw falls inside the window. That is usually the first page or screen they opened, not a signup, and it can be long before they created an account. **Why did my Amplitude new user count spike after a migration?** Probably an identity change. If how user_id is derived changes, every returning user looks brand new, and the spike is indistinguishable from a successful campaign. **Do anonymous visitors count as new users in Amplitude?** Yes. Amplitude creates a device id for a visitor with no user_id and that device is a user. Identity merging later ties the device to the account, taking the earlier first-seen date. **What should I read alongside Amplitude new users?** Active users. New rising while active stays flat means acquisition works and nothing sticks, which is the leakiest and most common shape. Neither number means much alone. --- # How to get realtime visitors from Plausible URL: https://notchmetrics.com/blog/get-realtime-visitors-from-plausible Published: 11 October 2026 Author: Notch Metrics Tags: plausible, realtime, api ## The endpoint ```bash curl -s 'https://plausible.io/api/v1/stats/realtime/visitors?site_id=example.com' \ -H 'Authorization: Bearer ' ``` The response is `47`. Not `{"visitors": 47}`, just the integer. Any JSON parser handles it, but code expecting an object gets `undefined` and reports zero visitors forever. ## It is still v1, deliberately Plausible moved its querying to a v2 POST endpoint, and realtime stayed behind on v1. That is not an oversight: realtime is a single scalar with no dimensions, no metrics list and no date range, so a query object would be ceremony around one number. Do not go looking for a v2 equivalent. This is the endpoint. ## The window is five minutes A realtime visitor is somebody who sent an event in the last five minutes. Not somebody with a tab open, and not somebody currently reading. Two consequences. **Somebody reading a long article is not counted** once they have been still for five minutes, unless your setup sends engagement events. They are on your site and they are not in the number. **The figure is lumpy on a small site.** One person arriving and leaving moves it by a whole unit, so a site with twenty visitors a day shows mostly zero punctuated by ones. That is not a broken integration. ## What to use it for Not trends. A five-minute scalar has no history, and polling it to build one gives you a sampled series with gaps wherever your poll missed. What it is good for is the moment. You post a launch, you share a link, you go on a podcast, and the number tells you whether anything is happening right now. That is a genuinely different question from "how was this week", and it is the one worth putting somewhere you can see without typing. ## Polling politely The figure changes constantly and the API is not a stream. Something in the range of thirty to sixty seconds is plenty: faster buys you nothing a human can read, and Plausible's rate limits are sized for dashboards rather than for tight loops. ```js async function current(siteId, key) { const res = await fetch( `https://plausible.io/api/v1/stats/realtime/visitors?site_id=${encodeURIComponent(siteId)}`, { headers: { Authorization: `Bearer ${key}` } }, ); if (!res.ok) return null; // Null, not zero. They are different. return Number(await res.text()); } ``` Returning `null` on failure rather than `0` matters more than it looks. A failed call that reports zero visitors looks exactly like a quiet night, and you will believe it. ## Self-hosted The same path works on a self-hosted instance, with your own host in place of `plausible.io`. The hosted connector in Notch Metrics targets Plausible Cloud; for a self-hosted box, a [custom metric](/blog/writing-a-custom-metric) declaring your own host is the route. ## In the notch Notch Metrics reports [Plausible unique visitors](/sources/plausible/visitors) and [pageviews](/sources/plausible/pageviews) in the MacBook notch over whatever window you choose, bucketed in the site's own timezone. For the launch-day question, place a short window and hover the camera housing. [Everything Plausible reports](/sources/plausible), or [download Notch Metrics](/download). ## Questions **What is the Plausible realtime visitors endpoint?** GET /api/v1/stats/realtime/visitors with a site_id parameter and a bearer token. It returns a bare integer rather than a JSON object, so code expecting an object reads undefined and reports zero forever. **How long is the Plausible realtime window?** Five minutes. A realtime visitor is somebody who sent an event in the last five minutes, not somebody with a tab open. A reader who has been still for five minutes is on your site and not in the number. **Is there a v2 version of the Plausible realtime endpoint?** No, and that is deliberate. Realtime is a single scalar with no dimensions, metrics list or date range, so it stayed on v1 where a query object would be ceremony around one number. **How often should I poll Plausible realtime visitors?** Thirty to sixty seconds. Faster buys nothing a human can read and Plausible's rate limits are sized for dashboards rather than tight loops. **Why does my Plausible realtime count keep showing zero?** Either the site is genuinely quiet in a five-minute window, which is normal on a small site, or a failed request is being read as zero. Return null on failure instead, because a failed call that reports zero looks exactly like a quiet night. --- # How to get revenue from the Dodo Payments API URL: https://notchmetrics.com/blog/get-revenue-from-the-dodo-payments-api Published: 11 October 2026 Author: Notch Metrics Tags: dodopayments, revenue, api ## Where does Dodo Payments revenue come from? The payments list. There is no revenue endpoint, so you page `/payments`, filter to the ones that succeeded, and sum `total_amount`. ```bash curl -s 'https://live.dodopayments.com/payments?created_at_gte=2026-10-01T00:00:00Z&created_at_lte=2026-10-08T00:00:00Z&page_size=100' \ -H 'Authorization: Bearer ' ``` ## Two hosts, and the error that looks like something else Dodo Payments splits test and live by hostname. - Live: `live.dodopayments.com` - Test: `test.dodopayments.com` A live key against the test host fails to authenticate, and the message says your key is invalid. It is not. It is in the wrong place. This costs people an hour because the obvious reading of an auth error is a bad key, and the first thing everybody does is regenerate one. Check the host before you rotate anything. ## Which payments are money Filter on `status` being `succeeded`. Dodo Payments also reports `processing` and `failed`, and a payment in flight is not revenue. Counting it means your figure drops when something fails, which reads as a refund in your chart and is not one. ```js const succeeded = payments.filter((p) => p.status === "succeeded"); const total = succeeded.reduce((sum, p) => sum + p.total_amount, 0); ``` ## total_amount includes tax `total_amount` is what the customer paid, in minor units, including tax. Dodo Payments is a merchant of record, so it collected that tax and it will remit it. The portion is not yours. That makes a Dodo Payments revenue figure higher than an equivalent Stripe one, which does not include tax. Comparing them directly overstates the Dodo side, and the gap is roughly your VAT rate. It is also gross: before refunds and before Dodo's fee. ## Subscription renewals and one-time purchases are mixed The payments list contains both. A renewal and a one-off purchase look similar, and what separates them is whether the payment carries a subscription id. ```js const oneTime = succeeded.filter((p) => !p.subscription_id); ``` Splitting them is worth doing. Recurring revenue carries into next month. A one-time sale has to be earned again. A total that holds steady while the recurring half shrinks is a business quietly changing shape. ## Pagination Dodo Payments pages with `page_number` and `page_size`. Page until you get a page shorter than `page_size`. ```js async function payments(key, host, from, to) { const rows = []; let page = 0; while (true) { const url = new URL(`https://${host}/payments`); url.searchParams.set("created_at_gte", from); url.searchParams.set("created_at_lte", to); url.searchParams.set("page_size", "100"); url.searchParams.set("page_number", String(page)); const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); const body = await res.json(); const items = body.items ?? []; rows.push(...items); if (items.length < 100) break; page += 1; } return rows; } ``` Bound the loop. An unbounded pager against a large account will page until something gives up, and that something is usually your rate limit. ## Or read it in the notch Notch Metrics reports [Dodo Payments total revenue](/sources/dodopayments/revenue) and [one-time sales](/sources/dodopayments/one-time-sales) as separate widgets in the MacBook notch, both gross and before fees, both bucketed hourly, daily, weekly or monthly with a sparkline. Connect once on the web, pick live or test, and the key never reaches your Mac. [Everything Dodo Payments reports](/sources/dodopayments), or [download Notch Metrics](/download). ## Questions **What is the Dodo Payments API base URL?** live.dodopayments.com for live mode and test.dodopayments.com for test mode. A key from one used against the other fails to authenticate with a message that reads like an invalid key, so check the host before regenerating anything. **Which Dodo Payments statuses count as revenue?** Only succeeded. Payments in processing are in flight and failed ones never completed. Counting processing payments makes your figure drop when one fails, which reads as a refund in a chart and is not one. **Does Dodo Payments total_amount include tax?** Yes. Dodo Payments is a merchant of record, so total_amount is what the customer paid including tax that Dodo will remit. It is also gross, before refunds and before Dodo's fee. **How do I separate one-time sales from subscription renewals in Dodo Payments?** Filter on whether the payment carries a subscription id. Payments without one are one-time purchases. Splitting them matters because recurring revenue carries into next month and a one-time sale has to be earned again. **How does pagination work on the Dodo Payments API?** With page_number and page_size. Page until you get a page shorter than page_size, and bound the loop so an unbounded pager against a large account does not run into your rate limit. --- # How to get views and watch time from the YouTube Analytics API URL: https://notchmetrics.com/blog/get-views-and-watch-time-from-the-youtube-analytics-api Published: 11 October 2026 Author: Notch Metrics Tags: youtube, analytics, api ## Two APIs, and you need the right one YouTube has a Data API and an Analytics API, and they answer different questions. | API | Good for | | ---------------- | ------------------------------------------------------------- | | Data API v3 | Channel metadata, the current subscriber count, video details | | Analytics API v2 | Views, watch time, revenue, anything over a window | For "how many views last week", it is Analytics. ```bash curl -s -G 'https://youtubeanalytics.googleapis.com/v2/reports' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ --data-urlencode 'ids=channel==MINE' \ --data-urlencode 'startDate=2026-10-01' \ --data-urlencode 'endDate=2026-10-07' \ --data-urlencode 'metrics=views,estimatedMinutesWatched' \ --data-urlencode 'dimensions=day' ``` ## The ids parameter `ids=channel==MINE`. Two equals signs, and that is not a typo. It is the API's own filter syntax, and `channel==UC...` works for a specific channel you have access to. `ids=channel=MINE` with one equals returns a 400 that does not explain which parameter is wrong. For a content owner you use `contentOwner==OWNER_ID` instead, which is a different grant entirely. ## The response is rows and column headers ```json { "columnHeaders": [ { "name": "day", "columnType": "DIMENSION", "dataType": "STRING" }, { "name": "views", "columnType": "METRIC", "dataType": "INTEGER" }, { "name": "estimatedMinutesWatched", "columnType": "METRIC", "dataType": "INTEGER" } ], "rows": [["2026-10-01", 4210, 18340]] } ``` Rows are arrays, positional, matching `columnHeaders`. Build an index from the headers rather than from your request: ```js const index = Object.fromEntries(body.columnHeaders.map((h, i) => [h.name, i])); const views = row[index.views]; ``` Using the headers rather than your own metrics string is the safer of the two, because the API can reorder and because a metric it silently dropped shows up as a missing key rather than as a shifted column. ## Everything is Pacific Time `day` buckets are `America/Los_Angeles`. Not UTC, not yours, and not configurable. A view at 21:00 in London is 13:00 Pacific the same day. A view at 02:00 in London is 18:00 Pacific the **previous** day. So a daily series compared against a UTC analytics tool disagrees at every boundary, and the disagreement is largest for channels whose audience is furthest from the Pacific coast. ## Watch time is minutes `estimatedMinutesWatched` is exactly what it says. Divide by 60 for hours, which is the unit any channel of size needs, because a minute figure is too long to read at a glance. Watch time is the metric YouTube's recommendation systems weight most, which makes it the more useful of the two. Views count arrivals; watch time counts attention. ## Views settle downwards YouTube discards automated and repeated playback before publishing, and that validation lags. The newest day or two can fall after the fact. Unlike Search Console, the drop here is a correction rather than an arrival, so the newest figures read **high** and settle **down**. Worth knowing which direction before you read a change. ## Scopes `https://www.googleapis.com/auth/youtube.readonly` for views and watch time. Revenue needs `https://www.googleapis.com/auth/yt-analytics-monetary.readonly` as well, and asking for it when you do not need it makes the consent screen more alarming than it has to be. ## In the notch Notch Metrics calls `reports.query` and shows [YouTube views](/sources/youtube/views), [watch time](/sources/youtube/watch-time) in hours, and [average view duration](/sources/youtube/average-view-duration) in the MacBook notch, daily, weekly or monthly, bucketed in Pacific Time as YouTube reports them. [Everything YouTube reports](/sources/youtube), or [download Notch Metrics](/download). ## Questions **Which YouTube API returns views over a date range?** The YouTube Analytics API v2, at youtubeanalytics.googleapis.com/v2/reports. The Data API v3 answers channel metadata and the current subscriber count, not windowed metrics. **What is the ids parameter in the YouTube Analytics API?** A filter using two equals signs, such as ids=channel==MINE or ids=channel==UC... for a specific channel. A single equals sign returns a 400 that does not say which parameter is wrong. **Which timezone does the YouTube Analytics API use?** America/Los_Angeles, and it is not configurable. A view at 02:00 in London is 18:00 Pacific the previous day, so a daily series disagrees with a UTC analytics tool at every boundary. **What unit is YouTube watch time in?** Minutes. The metric is estimatedMinutesWatched. Divide by 60 for hours, which is the unit any channel of size needs because a minute figure is too long to read at a glance. **Why do my YouTube view counts go down?** Because YouTube discards automated and repeated playback before publishing, and that validation lags. Unlike Search Console, the newest figures read high and settle downwards. --- # GitHub stars in your MacBook notch URL: https://notchmetrics.com/blog/github-stars-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: github, stars, mac If you maintain anything open source you know the refresh. Open the repository, look at the star count, close the tab. Nothing changed. Do it again in an hour. ## How do you watch a repository without the tab? Notch Metrics puts the counts in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands. Move away and it folds back to the shape it had. ## What GitHub reports | Number | What it is | | -------------------------------------------------------- | ----------------------------------- | | [Stars](/sources/github/stars) | The repository's current star count | | [Forks](/sources/github/forks) | Direct forks, not forks of forks | | [Open issues](/sources/github/open-issues) | Issues only, pull requests excluded | | [Open pull requests](/sources/github/open-pull-requests) | Including drafts | | [Followers](/sources/github/followers) | On the account, not a repository | The first four ask which repository to count, in `owner/name` form, because one GitHub connection usually covers several. Add a widget per repository. Followers belongs to the account, so it asks for nothing. ## Open issues, counted properly GitHub's own `open_issues_count` on the repository object includes pull requests, because in GitHub's data model a pull request is an issue. A project with an active contribution flow reads as though its backlog doubled overnight. Notch Metrics counts issues through the search API with `is:issue is:open`, so [the number in your notch](/sources/github/open-issues) is the backlog you would see on the Issues tab. That is the kind of difference worth a sentence on the page rather than a surprise in the figure. ## Why none of these have sparklines GitHub returns the current count. There is no endpoint for the star count on an arbitrary past day, and reconstructing one means paging the entire stargazer history on every refresh. So the widgets show figures rather than charts, which is honest about what the API gives. ## OAuth, no key to paste GitHub connects over OAuth. You approve a prompt that asks to read public repositories you can see and your name and avatar, and that is the whole grant. [What the GitHub connector asks for](/sources/github). No token is typed in anywhere, and the token that results is encrypted on the server. The Mac app never holds it. ## Alongside everything else Revenue from [Stripe](/sources/stripe) and [Polar](/sources/polar), traffic from [Plausible](/sources/plausible), search from [Search Console](/sources/google-search-console). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see GitHub stars on my Mac?** Install Notch Metrics, connect GitHub over OAuth, and place the stars widget in the MacBook notch with the repository in owner/name form. Hover the camera housing and the count is there. **Can I track more than one GitHub repository?** Yes. Each of the repository widgets asks which repository to count, so one GitHub connection covers as many repositories as you place widgets for. **Does Notch Metrics count pull requests as open issues?** No. GitHub's own open_issues_count includes pull requests because a pull request is an issue in its data model. Notch Metrics counts issues through the search API with is:issue is:open, so the figure matches the Issues tab. **Why do the GitHub widgets have no sparkline?** Because GitHub returns only the current count. There is no endpoint for a star count on a past day, and reconstructing one means paging the entire stargazer history on every refresh. **What GitHub permissions does Notch Metrics ask for?** Read access to your repositories, public and private, and your name and avatar. GitHub grants that as full repository access, because it has no read-only option for private repositories. Notch Metrics only reads. Nothing is typed in, because GitHub connects over OAuth. --- # Lemon Squeezy MRR on your Mac URL: https://notchmetrics.com/blog/lemon-squeezy-mrr-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: lemonsqueezy, mrr, mac Lemon Squeezy handles the tax and the invoicing. It does not handle the habit of opening the dashboard twelve times a day to read one number. ## How do you see Lemon Squeezy MRR without the dashboard? Notch Metrics puts it in the MacBook notch, the strip of glass around the camera. Collapsed, a thin line of figures. Hover and it expands over whatever you were doing. Move away and it folds back. No dock icon, nothing in the menu bar. ## What Lemon Squeezy reports | Number | What it is | | -------------------------------------------------------------- | ------------------------------------------- | | [MRR](/sources/lemonsqueezy/mrr) | Active subscriptions, normalised to a month | | [Revenue](/sources/lemonsqueezy/revenue) | Orders in the window that took payment | | [Active subscribers](/sources/lemonsqueezy/active-subscribers) | Status `active`, trials included | | [Refunds](/sources/lemonsqueezy/refunds) | Refunded amounts on orders in the window | | [Recent payments](/sources/lemonsqueezy/recent-payments) | The last ten orders | Two of those carry caveats worth knowing before you trust them. **Active subscribers includes trials.** Lemon Squeezy counts a trialling subscription as active, and Notch Metrics reports what Lemon Squeezy reports rather than second-guessing it. So the figure is the book plus the people still deciding. **Refunds are bucketed by the order date.** Lemon Squeezy exposes `refunded_amount` on the order but not the date the refund happened, so a refund lands on the day the original order was placed. A refund this week on an order from March appears in March. [Why that matters](/sources/lemonsqueezy/refunds). Both of those are stated on the pages rather than buried, because a number whose definition is hidden is a number you cannot argue with. ## One store, or several Lemon Squeezy accounts can hold more than one store, and the connector asks which one. Add a second connection for a second store. [What the Lemon Squeezy connector asks for](/sources/lemonsqueezy). Your API key is encrypted on the server with a key unique to that one connection. The Mac app never holds it, so the number in your notch cost you no credential on the device. ## Alongside everything else Revenue also comes from [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments) and [RevenueCat](/sources/revenuecat). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see Lemon Squeezy MRR on my Mac?** Install Notch Metrics, connect Lemon Squeezy on the web with an API key and a store id, and place the MRR widget in the MacBook notch. Hover the camera housing and the figure is there. **Does the Lemon Squeezy active subscriber count include trials?** Yes. Lemon Squeezy counts a trialling subscription as active, and Notch Metrics reports what Lemon Squeezy reports rather than second-guessing it. The figure is the paying book plus the people still deciding. **Why are Lemon Squeezy refunds bucketed by order date?** Because the API exposes refunded_amount on the order but not the date the refund happened. A refund this week on an order from March therefore appears in March. **Can Notch Metrics handle more than one Lemon Squeezy store?** Yes, as separate connections. A Lemon Squeezy account can hold several stores and the connector asks which one, so add a second connection for a second store. **Is my Lemon Squeezy API key stored on my Mac?** No. It is encrypted on the server with a key unique to that one connection. The Notch Metrics server calls Lemon Squeezy and sends your Mac a normalised number. --- # How to measure sessions in PostHog URL: https://notchmetrics.com/blog/measure-sessions-in-posthog Published: 11 October 2026 Author: Notch Metrics Tags: posthog, sessions, api ## What is a PostHog session? A run of activity from one person with no gap longer than thirty minutes. PostHog does not record sessions; it derives them from the event stream, and everything odd about session numbers follows from that. ## The query PostHog exposes a `sessions` table in HogQL, which saves you stitching events yourself. ```sql SELECT toUnixTimestamp(toStartOfDay(min_timestamp)) AS bucket, count() AS value FROM sessions WHERE min_timestamp >= toDateTime({from}) AND min_timestamp < toDateTime({to}) GROUP BY bucket ORDER BY bucket ``` `min_timestamp` is when the session started, and bucketing on it means a session that spans midnight is counted on the day it began. That is the convention every analytics tool uses and it is worth knowing you are accepting it. ## The thirty-minute rule, and what it does to your numbers Somebody reading a long article, going to make tea, and coming back after thirty-five minutes is two sessions. Somebody who leaves a tab open in the background and clicks once an hour is many sessions. Neither is a bug. It is the definition doing its job, and it means session counts are sensitive to how your product is used in a way that pageviews and users are not. A product people keep open in a tab all day produces session counts that have very little to do with how many times they decided to use it. ## Sessions move when nothing moved Three things change session counts without any change in traffic. **A background poll.** If your app sends an event on a timer, sessions never end and your session count collapses. People discover this after adding heartbeat tracking. **A tracking change.** Firing more events in the gaps bridges what would have been two sessions into one. **Identity merging.** When PostHog merges two persons, their sessions merge too, which can change historical figures. So a session series is the least stable of the standard metrics, and reading a step in it as a behaviour change is usually wrong. ## Pageviews per session is the useful form Sessions on their own say little. Divided into pageviews they say how deep a visit goes. ```sql -- Two queries, divided client-side. Keep the window identical. SELECT count() FROM events WHERE event = '$pageview' AND ... SELECT count() FROM sessions WHERE ... ``` Depth rising is people reading more. Depth rising while sessions fall is usually a navigation problem, with the same people hunting for something that used to be easy to find. ## Sessions are not visits, and not users Three different numbers that get used interchangeably in conversation and should never be in a chart. | | What it counts | | --------- | ---------------------------------------------- | | Sessions | Runs of activity, split on a thirty-minute gap | | Users | De-duplicated people over the window | | Pageviews | Every page load, repeats included | One person can produce many sessions in a day and many pageviews in a session. Label the axis. ## Or read it off the notch Notch Metrics reports [PostHog sessions](/sources/posthog/sessions) as sessions started in the window, bucketed hourly, daily, weekly or monthly in your PostHog project's own timezone, with a sparkline and a comparison against the previous window. Place it beside [pageviews](/sources/posthog/pageviews) and [active users](/sources/posthog/active-users) and the three describe the visit rather than just counting it. [Everything PostHog reports](/sources/posthog), or [download Notch Metrics](/download). ## Questions **What is a session in PostHog?** A run of activity from one person with no gap longer than thirty minutes. PostHog derives sessions from the event stream rather than recording them, which is why session numbers behave differently from users and pageviews. **How do I count sessions in PostHog with HogQL?** SELECT count() FROM the sessions table, bucketing on min_timestamp so a session that spans midnight is counted on the day it started. **Why did my PostHog session count drop suddenly?** Often a background poll. If your app sends events on a timer, sessions never reach the thirty-minute gap that ends them, so many visits merge into one long session. Adding heartbeat tracking is the usual cause. **Are sessions the same as visits or users?** No. One person can produce many sessions in a day and many pageviews in a session. Sessions count runs of activity split on a thirty-minute gap, users are de-duplicated people, and pageviews count every load. **What is the most useful thing to do with a session count?** Divide pageviews by it. Depth rising means people are reading more. Depth rising while sessions fall is usually a navigation problem, with the same people hunting for something that used to be easy to find. --- # How to measure Stripe subscriber churn URL: https://notchmetrics.com/blog/measure-stripe-subscriber-churn Published: 11 October 2026 Author: Notch Metrics Tags: stripe, churn, api ## What counts as a churned Stripe subscriber? The question sounds trivial and it is the whole problem. Stripe has two shapes of cancellation and they happen at different times. One ends the subscription now. The other sets `cancel_at_period_end`, which leaves the subscription `active` until the period they paid for runs out. Somebody who cancels an annual plan today is still a paying customer for the next eleven months. Count them on the day they clicked and your churn figure spikes today while your revenue does not move for a year. Count them when the subscription actually ends and today's churn number tells you nothing about today's cancellations. Both are defensible. Only one can be true at a time. ## Counting by when it ended ```js async function churned(stripe, from, to) { let count = 0; for await (const sub of stripe.subscriptions.list({ status: "canceled", limit: 100, })) { const endedAt = (sub.ended_at ?? 0) * 1000; if (endedAt >= from && endedAt < to) count += 1; } return count; } ``` `ended_at` is when the subscription actually stopped. `canceled_at` is when somebody asked. For a subscription cancelled immediately they are the same second. For one cancelled at period end they can be a year apart. This is the bucket that lines up with your active subscriber count, which is why it is the one worth defaulting to. A churn series that disagrees with the movement in active subscribers is a series nobody will trust. ## The filter you cannot use `subscriptions.list` has no date filter on `ended_at`. It filters on `created`, which is when the subscription began, and that is useless here. You page the cancelled list and filter client-side, which means a business with years of history pages through every subscription it has ever lost to answer a question about last week. Two ways out. Listen for `customer.subscription.deleted` on a webhook and record the date yourself, which is the right answer for anything serious. Or cache aggressively and accept the paging, which is fine below a few thousand cancellations. ## Involuntary churn hides somewhere else A subscription whose payment keeps failing does not go straight to `canceled`. It goes to `past_due`, then to `unpaid` or `canceled` depending on your dunning settings, and the gap is however long you told Stripe to retry. So your churn figure lags the failure that caused it by days or weeks, and a dunning configuration change moves the whole series. Worth knowing before you read a step in the chart as a product problem. Failed payments are also the churn you can actually fix. Separating involuntary churn from people who chose to leave is usually the highest return analysis a subscription business can do. ## A count is not a rate Everything above counts subscriptions lost. A churn rate divides that by a denominator, and the denominator is where every argument happens. Customers at the start of the period. Customers at the start plus new customers. MRR rather than customers. Each gives a different number and each has a published blog post calling it the correct one. Pick one, write it down beside the code, and never change it quietly. A churn rate whose definition moved is worse than no churn rate, because the trend line looks real. ## In the notch Notch Metrics reports [Stripe churned subscribers](/sources/stripe/churned-subscribers) as a count, bucketed by cancellation time, with a sparkline and a comparison against the previous window. It deliberately does not compute a rate: the denominator everybody means by "churn rate" differs by company, and guessing which one you meant would produce a number you could not defend. Place it beside [new subscribers](/sources/stripe/new-subscribers) and [active subscribers](/sources/stripe/active-subscribers), and the three together say which direction the business is going. [Download Notch Metrics](/download). ## Questions **How do I count churned subscribers with the Stripe API?** List subscriptions with status canceled and filter on ended_at falling inside your window. There is no server-side date filter for ended_at, so the filtering happens client-side after paging. **What is the difference between canceled_at and ended_at in Stripe?** canceled_at is when somebody asked to cancel. ended_at is when the subscription actually stopped. For a subscription cancelled at period end they can be a year apart, and ended_at is the one that lines up with your active subscriber count. **Does cancel_at_period_end mean a Stripe subscription has churned?** Not yet. The subscription stays active and keeps paying until the period they bought runs out. Counting it as churn on the day somebody clicks makes churn spike while revenue does not move. **How do I separate involuntary churn in Stripe?** Involuntary churn arrives through past_due and unpaid rather than straight to canceled, after however long your dunning settings retry. Track subscriptions entering past_due separately. Failed payments are the churn you can actually fix. **What is the correct denominator for a Stripe churn rate?** There is no single correct one. Customers at the start of the period, customers at the start plus new customers, and MRR rather than customers all give different figures. Pick one, write it down, and never change it quietly. **How do I get Stripe churn without paging the whole subscription history?** Listen for the customer.subscription.deleted webhook and record the date yourself. The subscriptions list filters on created, not ended_at, so without your own record you page every cancellation you have ever had. --- # Paddle MRR on your Mac URL: https://notchmetrics.com/blog/paddle-mrr-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: paddle, mrr, mac Paddle is a merchant of record, which means it handles tax, invoicing and compliance for you. It also means one more dashboard to open. ## How do you see Paddle MRR without the dashboard? Put it in the notch. Notch Metrics renders your Paddle figures in the strip of glass around your Mac's camera. Collapsed, a thin line of numbers. Hover, and it expands over whatever you were doing. Move away and it folds back. ## What Paddle reports | Number | What it is | | ------------------------------------------------------------ | ------------------------------------------------ | | [MRR](/sources/paddle/mrr) | Active subscriptions, normalised to a month | | [Revenue](/sources/paddle/revenue) | Transactions billed in the window, including tax | | [Active subscriptions](/sources/paddle/active-subscribers) | Status `active` right now | | [New subscriptions](/sources/paddle/new-subscribers) | Started in the window | | [Churned subscriptions](/sources/paddle/churned-subscribers) | Cancelled in the window | | [Recent payments](/sources/paddle/recent-payments) | The last ten transactions | Paddle revenue includes tax, because as merchant of record Paddle collected it. That is worth remembering when comparing against a Stripe figure, which does not. ## The Paddle quirk that shapes everything Paddle's API cannot filter subscriptions by date. There is no `started_at[gte]`. To count subscriptions that began last week, you read the whole subscription list and filter it yourself. Notch Metrics does exactly that, bounded so it cannot run away on a large account. It is worth knowing because it explains why new and churned subscription figures are the slow ones, and why anybody building this themselves hits a wall at scale. ## Sandbox and production Paddle runs a sandbox on a different host. The connector asks which mode your key belongs to, because a sandbox key against the production host returns 401 rather than an empty account. [What the Paddle connector asks for](/sources/paddle). Your API key is encrypted on the server with a key unique to that one connection. The Mac app never holds it. ## Not just Paddle Revenue also comes from [Stripe](/sources/stripe), [Polar](/sources/polar), [Dodo Payments](/sources/dodopayments), [Lemon Squeezy](/sources/lemonsqueezy) and [RevenueCat](/sources/revenuecat). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see Paddle MRR on my Mac?** Install Notch Metrics, connect Paddle on the web with an API key, and place the MRR widget in the MacBook notch. Hover the camera housing and the figure is there. **Does Paddle revenue include tax?** Yes. Paddle is a merchant of record and collects tax on your behalf, so its transaction totals include it. A Paddle revenue figure is therefore higher than an equivalent Stripe one, which does not include tax. **Does Notch Metrics support Paddle sandbox?** Yes. The connector asks which mode your API key belongs to, because Paddle runs its sandbox on a different host and a sandbox key against production returns 401. **Why are Paddle subscription counts slower than revenue?** Paddle's API cannot filter subscriptions by date. Counting subscriptions that started or ended in a window means reading the whole subscription list and filtering it client-side. **Is my Paddle API key stored on my Mac?** No. It is encrypted on the server with a key unique to that one connection. The Notch Metrics server calls Paddle and sends your Mac a normalised number. --- # Plausible visitors in your MacBook notch URL: https://notchmetrics.com/blog/plausible-visitors-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: plausible, analytics, mac Plausible is a single page that loads instantly and tells you everything. It is still a page, and you still have to go to it. ## How do you watch Plausible without the dashboard? Notch Metrics puts the figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What Plausible reports | Number | What it is | | ---------------------------------------------- | ----------------------------------------- | | [Unique visitors](/sources/plausible/visitors) | Distinct people, counted without a cookie | | [Pageviews](/sources/plausible/pageviews) | Every page load, repeats included | | [Sessions](/sources/plausible/sessions) | Visits started in the window | | [Bounce rate](/sources/plausible/bounce-rate) | Sessions with one pageview, as a fraction | ## The cookieless count, and what it costs Plausible identifies a visitor by hashing the IP address with the user agent and a daily rotating salt. No cookie, nothing stored on the device, and in most of Europe no consent banner. The trade is worth understanding. The hash rotates daily, so somebody who visits on Monday and again on Tuesday is two visitors over a two-day window, not one. Over long windows a unique visitor count sits closer to a visit count than to a count of people. That is a privacy decision rather than a measurement failure, and it is the right one. It just means the number does not mean quite what the label says. ## Timezone is the site's Daily buckets follow the timezone set on the Plausible site, not on your Mac. That keeps the notch and your Plausible dashboard agreeing, which matters more than agreeing with your laptop clock. ## Connecting A Plausible API key and the site id, which is the domain as Plausible knows it. [What the Plausible connector asks for](/sources/plausible). The key is encrypted on the server with a key unique to that one connection, so the Mac app never holds it. The connector is marked beta: it connects and returns figures, and the edges are still being worked on. ## Self-hosted Plausible The connector targets Plausible Cloud. A self-hosted instance lives on your own domain, which the hosted connector does not reach. If that is you, a [custom metric](/blog/writing-a-custom-metric) covers it: one JavaScript module declaring your host, running on your own Mac. ## Alongside everything else Revenue from [Stripe](/sources/stripe) and [Polar](/sources/polar), search from [Search Console](/sources/google-search-console), product analytics from [PostHog](/sources/posthog). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see Plausible visitors on my Mac?** Install Notch Metrics, connect Plausible on the web with an API key and your site id, and place the visitors widget in the MacBook notch. Hover the camera housing and the figure is there. **How does Plausible count unique visitors without cookies?** By hashing the IP address with the user agent and a daily rotating salt. Nothing is stored on the device. Because the salt rotates daily, somebody who visits on two days counts as two visitors over a two-day window. **Which timezone do Plausible daily figures use?** The timezone configured on the Plausible site, not the one your Mac is set to. That keeps the notch and the Plausible dashboard agreeing about which day a number belongs to. **Does Notch Metrics work with self-hosted Plausible?** The built-in connector targets Plausible Cloud. For a self-hosted instance, write a custom metric: one JavaScript module that declares your own host and runs on your Mac. **Is my Plausible API key stored on my Mac?** No. It is encrypted on the server with a key unique to that one connection. The Notch Metrics server calls Plausible and sends your Mac a normalised number. --- # Polar revenue in your MacBook notch URL: https://notchmetrics.com/blog/polar-revenue-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: polar, revenue, mac Polar's dashboard is good. It is also a browser tab, and a browser tab is somewhere you have to go. ## How do you watch Polar revenue without a dashboard? Notch Metrics puts the numbers in the MacBook notch. Collapsed, it is a thin strip showing what you chose. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. No dock icon, nothing in the menu bar, no tab. ## What Polar reports | Number | What it is | | ----------------------------------------------------------- | ------------------------------------ | | [MRR](/sources/polar/mrr) | Polar's own MRR, trials excluded | | [Revenue](/sources/polar/revenue) | Order subtotals, before fees and tax | | [Active subscriptions](/sources/polar/active-subscribers) | On the latest day Polar has computed | | [New subscriptions](/sources/polar/new-subscribers) | Started in the window | | [Churned subscriptions](/sources/polar/churned-subscribers) | Ended in the window | | [Recent payments](/sources/polar/recent-payments) | The last ten orders, newest first | Notch Metrics reads MRR and active subscriptions from Polar's own metrics endpoint rather than recomputing them. Polar normalises yearly plans by dividing by twelve and excludes trials, and reporting a second opinion would mean your notch and your Polar dashboard quietly disagreeing. ## One detail worth knowing Polar computes metrics per period, and the newest period can be empty for a while as its pipeline catches up. Notch Metrics takes the latest period that has a value rather than the latest period, so the widget does not read zero at midnight and then correct itself an hour later. ## Your Polar token stays on the server Connect Polar once on the web with an organisation access token. It is encrypted with a key unique to that one connection, and our server calls Polar and hands your Mac a normalised number. The Mac app never holds the token. [What the Polar connector asks for](/sources/polar). Polar runs a sandbox as well as production, and the connector asks which mode the token belongs to. A sandbox token against the production host returns 401 rather than an empty account, which is at least a clear failure. ## Polar is not the only source Revenue also comes from [Stripe](/sources/stripe), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments), [Lemon Squeezy](/sources/lemonsqueezy) and [RevenueCat](/sources/revenuecat). Analytics come from [Plausible](/sources/plausible), [PostHog](/sources/posthog) and others. [Every source](/sources). ## Getting it Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year with every connector included. [Download Notch Metrics](/download). ## Questions **How do I see Polar MRR on my Mac?** Install Notch Metrics, connect Polar on the web with an organisation access token, and place the MRR widget in the MacBook notch. Hover the camera housing and the figure is there. **Does Notch Metrics compute its own Polar MRR?** No. It reads Polar's own MRR from the metrics endpoint, so the notch and the Polar dashboard agree. Polar normalises yearly plans by dividing by twelve and excludes trials. **Does Notch Metrics support Polar sandbox?** Yes. The connector asks which mode your token belongs to, sandbox or production, because a sandbox token against the production host returns 401 rather than an empty account. **Is my Polar access token stored on my Mac?** No. The token is encrypted on the server with a key unique to that one connection. The Notch Metrics server calls Polar and sends your Mac a normalised number. **Can I show Polar and Stripe revenue together?** Yes. Notch Metrics connects Polar, Stripe, Paddle, Dodo Payments, Lemon Squeezy, RevenueCat and Shopify, with every connector included on both plans and no per-integration charge. --- # PostHog active users on your Mac URL: https://notchmetrics.com/blog/posthog-active-users-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: posthog, analytics, mac PostHog answers almost any question you can phrase. That is its strength and the reason opening it costs you ten minutes rather than one. ## How do you watch PostHog without opening PostHog? Notch Metrics puts the headline figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back. ## What PostHog reports | Number | What it is | | --------------------------------------------- | --------------------------------------------- | | [Active users](/sources/posthog/active-users) | People with at least one event, de-duplicated | | [New users](/sources/posthog/new-users) | First seen in the window | | [Events](/sources/posthog/events) | Everything ingested, autocapture included | | [Pageviews](/sources/posthog/pageviews) | `$pageview` events | | [Sessions](/sources/posthog/sessions) | Sessions started in the window | ## The de-duplication that explains the chart Active users is de-duplicated across the whole window. One person active on three days is one user and three bars, so the bars in the sparkline do not add up to the headline. That is correct and it surprises people every time. A weekly active user count is not the sum of seven daily ones, and any tool whose bars do add up is reporting daily actives seven times rather than weekly actives once. ## Timezones are your project's PostHog buckets in the timezone configured on the project, not on your Mac. That is deliberate: it means the notch and your PostHog dashboards agree, and two screens in the same company showing different Tuesdays is worse than being an hour out from your laptop clock. ## Cloud region matters PostHog runs US and EU clouds and a key from one returns 401 against the other, which reads like a revoked key. The connector asks which region, and says so when it fails. [What the PostHog connector asks for](/sources/posthog). A personal API key and a project id. The key is encrypted on the server with a key unique to that connection, so the Mac app never holds it. ## Beta, honestly The PostHog connector is marked beta on [its page](/sources/posthog). It connects and returns figures; the edges are still being worked on. ## Alongside everything else Revenue from [Stripe](/sources/stripe) and [Polar](/sources/polar), traffic from [Plausible](/sources/plausible) and [Google Analytics](/sources/google-analytics), search from [Search Console](/sources/google-search-console). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see PostHog active users on my Mac?** Install Notch Metrics, connect PostHog on the web with a personal API key, a project id and your cloud region, and place the active users widget in the MacBook notch. **Why do PostHog daily bars not add up to the weekly active user figure?** Because active users is de-duplicated across the whole window. One person active on three days is one user and three bars. A weekly active count is not the sum of seven daily ones. **Does Notch Metrics use my PostHog project timezone?** Yes. Buckets follow the timezone configured on the PostHog project rather than your Mac, so the notch and your PostHog dashboards agree about which day a number belongs to. **Does Notch Metrics support PostHog EU cloud?** Yes. The connector asks which region your key belongs to, because a US key against the EU cloud returns 401 and the other way round, which reads like a revoked key rather than a wrong host. **Is the PostHog connector finished?** It is marked beta. It connects and returns active users, new users, events, pageviews and sessions today; the edges are still being worked on. --- # How to pull clicks and impressions from the Search Console API URL: https://notchmetrics.com/blog/pull-clicks-and-impressions-from-the-search-console-api Published: 11 October 2026 Author: Notch Metrics Tags: google-search-console, seo, api ## The endpoint One POST, and everything comes from it. ```bash curl -s -X POST \ 'https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "startDate": "2026-10-01", "endDate": "2026-10-07", "dimensions": ["date"] }' ``` Every row comes back with all four metrics, so there is no metric list to specify: `clicks`, `impressions`, `ctr` and `position` are always present. ## The site URL in the path This is where most first attempts fail. The property identifier goes in the URL path and it must be encoded. | Property type | Identifier | Encoded | | ------------- | ----------------------- | ------------------------------ | | Domain | `sc-domain:example.com` | `sc-domain%3Aexample.com` | | URL prefix | `https://example.com/` | `https%3A%2F%2Fexample.com%2F` | A URL-prefix property keeps its trailing slash, and dropping it returns a 403 that reads like a permissions problem rather than a not-found. Use `encodeURIComponent` and copy the identifier exactly as the Search Console API's site list returns it. ## Dimensions change what a row is | `dimensions` | One row per | | ------------------- | ------------------------------- | | `[]` | The whole window, one row total | | `["date"]` | Day | | `["query"]` | Search term | | `["page"]` | URL | | `["query", "page"]` | Combination | Omitting dimensions entirely is the right call for a headline figure. It returns a single row with the window's totals and no row limit to worry about. ## Why your breakdown does not add up to your total Two separate reasons, and both surprise people. **Row limits.** `rowLimit` defaults to 1,000 and maxes at 25,000. A site with more queries than that returns the top rows only, so summing them undercounts. **Anonymised queries.** Google withholds queries issued by very few people, for privacy. Those clicks exist in the totals and appear in no query breakdown, at any row limit. On a long-tail site the withheld portion can be a third of your traffic. So a query-dimension sum is always less than or equal to the undimensioned total, and the gap is not a bug to chase. Ask without dimensions when you want the true total. ## CTR and position are computed, not summed `ctr` is clicks over impressions **for that row**, and `position` is the average position over that row's impressions. Averaging the `position` values across rows is wrong: it weights a query with ten impressions equally with one that had ten thousand. If you need an overall position, ask for the undimensioned row and let Google compute it. ## The lag Data settles over two to three days. An `endDate` of today returns a partial figure that will grow. For a stable series, end your window two or three days back. For a chart that includes today, expect the last points to rise on every refresh, and say so in the interface rather than letting somebody read it as a decline. ## Dates are property timezone, and that is Pacific Search Console buckets by day in Pacific Time regardless of where you or your users are. A daily figure compared against a UTC analytics series will disagree at the edges for every site outside that zone. ## Auth OAuth with `https://www.googleapis.com/auth/webmasters.readonly`, or a service account granted access to the property. Use `sites.list` first to get the exact identifier strings rather than constructing them. ## In the notch Notch Metrics calls this endpoint and reports [clicks](/sources/google-search-console/clicks), [impressions](/sources/google-search-console/impressions), [CTR](/sources/google-search-console/ctr) and [average position](/sources/google-search-console/average-position) in the MacBook notch, daily, weekly or monthly. The two-to-three-day lag is stated on each page rather than hidden in a drooping chart. [Everything Search Console reports](/sources/google-search-console), or [download Notch Metrics](/download). ## Questions **What is the Search Console API endpoint for clicks and impressions?** POST to /webmasters/v3/sites/{siteUrl}/searchAnalytics/query. Every row returns all four metrics, so there is no metric list to specify: clicks, impressions, ctr and position are always present. **How do I encode the site URL in the Search Console API path?** With encodeURIComponent, exactly as sites.list returns it. A domain property becomes sc-domain%3Aexample.com and a URL-prefix property keeps its trailing slash. Dropping the slash returns a 403 that reads like a permissions problem. **Why does my Search Console query breakdown not add up to the total?** Two reasons. rowLimit caps rows at 25,000, and Google withholds queries issued by very few people for privacy at any row limit. Ask without dimensions for the true total. **Can I average Search Console position across rows?** No. Each row's position is already averaged over that row's impressions, so averaging rows weights a query with ten impressions equally with one that had ten thousand. Ask for the undimensioned row instead. **Which timezone does Search Console use for daily data?** Pacific Time, regardless of where you or your users are. A daily figure compared against a UTC analytics series will disagree at the edges for every site outside that zone. --- # How to pull Lemon Squeezy revenue and refunds by store URL: https://notchmetrics.com/blog/pull-lemon-squeezy-revenue-and-refunds Published: 11 October 2026 Author: Notch Metrics Tags: lemonsqueezy, revenue, api ## Where does Lemon Squeezy revenue come from? The orders list, filtered to your store. ```bash curl -s 'https://api.lemonsqueezy.com/v1/orders?filter[store_id]=12345&page[size]=100' \ -H 'Accept: application/vnd.api+json' \ -H 'Authorization: Bearer ' ``` One account can hold several stores and the API returns all of them by default, so the `filter[store_id]` is not optional in practice. Without it you are summing a business you may not have meant to include. ## Which orders took money An order's `attributes.status` tells you. `paid` is money. `pending` has not completed, `failed` did not, and `refunded` did take money and then gave it back, which means it still belongs in your gross figure for the day it happened. ```js const paid = orders.filter((o) => ["paid", "refunded"].includes(o.attributes.status)); const gross = paid.reduce((sum, o) => sum + o.attributes.total, 0); ``` Excluding refunded orders from revenue and also reporting refunds separately double counts the refund. Pick gross plus a separate refund figure, which is the convention everybody expects. ## What total actually is `attributes.total` is in minor units of `attributes.currency`, and Lemon Squeezy gives you several totals on the same order: | Field | What it holds | | ----------------- | ------------------------ | | `subtotal` | Before tax and discounts | | `discount_total` | What a coupon took off | | `tax` | Collected on your behalf | | `total` | What the customer paid | | `refunded_amount` | What has been given back | Lemon Squeezy is a merchant of record, so `tax` is not yours. A `total` compared against a Stripe gross figure overstates the Lemon Squeezy side by roughly the tax rate. There is also `total_usd` and friends, which are Lemon Squeezy's own conversion. Useful for a multi-currency store, and worth noting in your chart that a conversion happened. ## The refund date does not exist This is the real limitation. `refunded_amount` sits on the order, and so does `refunded_at` on some payloads, but the orders list gives you no way to filter or bucket by when the refund occurred. So a refunds series built from the orders list is bucketed by the order date, not the refund date. A refund processed this week on an order from March appears in March, and your current week shows nothing. That is a real constraint rather than an implementation choice, and the only way around it is to listen for the `order_refunded` webhook and record the date yourself. ## Pagination ```js async function orders(key, storeId) { const rows = []; let url = `https://api.lemonsqueezy.com/v1/orders?filter[store_id]=${storeId}&page[size]=100`; while (url) { const res = await fetch(url, { headers: { Accept: "application/vnd.api+json", Authorization: `Bearer ${key}` }, }); const body = await res.json(); rows.push(...body.data); url = body.links?.next ?? null; } return rows; } ``` The `next` link carries your filters already. Rebuilding the URL by hand is how a store filter gets dropped on page two. ## In the notch Notch Metrics reports [Lemon Squeezy revenue](/sources/lemonsqueezy/revenue) as orders in the window that took payment, by `total`, gross: a later refund does not remove the order from the day it was paid. [Refunds](/sources/lemonsqueezy/refunds) are a separate widget, and the page says plainly that they are bucketed by the order date rather than the refund date, because the API does not expose the latter. A number whose limits are hidden is a number you cannot use. [Everything Lemon Squeezy reports](/sources/lemonsqueezy), or [download Notch Metrics](/download). ## Questions **How do I filter Lemon Squeezy orders by store?** Pass filter[store_id] on the orders endpoint. One account can hold several stores and the API returns all of them by default, so without the filter you are summing a business you may not have meant to include. **Should refunded orders count in Lemon Squeezy revenue?** Yes, in a gross figure, on the day they were paid. Report refunds as a separate number. Excluding refunded orders from revenue and also reporting refunds separately counts the refund twice. **Does Lemon Squeezy order total include tax?** Yes. total is what the customer paid, including tax that Lemon Squeezy collected as merchant of record and will remit. Use subtotal if you want the figure before tax and discounts. **Can I get Lemon Squeezy refunds by refund date?** Not from the orders list. refunded_amount sits on the order and there is no way to filter or bucket by when the refund occurred, so a refunds series is bucketed by order date. Listen for the order_refunded webhook to record the real date. **How does pagination work on the Lemon Squeezy API?** page[size] up to 100, with a links.next URL to continue. Follow the next link rather than rebuilding the URL, because it already carries your filters and a hand-built page two is how a store filter gets dropped. --- # How to pull Paddle transactions for a date range URL: https://notchmetrics.com/blog/pull-paddle-transactions-for-a-date-range Published: 11 October 2026 Author: Notch Metrics Tags: paddle, revenue, api ## How do you get Paddle revenue for a window? List transactions, filter by billing date, and sum the totals. ```bash curl -s 'https://api.paddle.com/transactions?billed_at[GTE]=2026-10-01T00:00:00Z&billed_at[LT]=2026-10-08T00:00:00Z&status=completed&per_page=100' \ -H 'Authorization: Bearer pdl_live_...' ``` The square-bracket operator syntax is Paddle's, and it is case-sensitive: `[GTE]` works, `[gte]` does not. Dates are RFC 3339. ## Billed, created, or updated A Paddle transaction carries several timestamps and picking the wrong one produces a figure that is plausible and wrong. | Field | What it means | | ------------ | ---------------------------------------- | | `created_at` | When the transaction record was made | | `billed_at` | When it was actually billed | | `updated_at` | Any change, including status transitions | Use `billed_at`. A subscription renewal is often created before it is billed, and a transaction that sat in `past_due` for a week while dunning ran was created last Monday and billed this Monday. Bucketing by creation puts the money on the wrong day. ## Which statuses are money `completed` and `paid` are revenue. `draft` and `ready` have not been charged. `billed` is awaiting payment. `canceled` and `past_due` are not money you have. Filtering on `status=completed` alone misses accounts that settle into `paid`, so take both unless you have checked which yours produces. ## The total includes tax `details.totals.total` is the amount charged including tax, in minor units, as a string. Paddle is a merchant of record: it collected the VAT and it will remit it, and that portion is not yours. If you want your own take, `details.totals.earnings` is after Paddle's fee and after tax. The two answer different questions: - `total` is what the customer paid. - `earnings` is what you will be paid out. Pick one and label your chart accordingly. A Paddle `total` compared against a Stripe gross figure is not a like-for-like comparison, because Stripe's does not include tax. ## Currency Each transaction has its own `currency_code`, and Paddle sells in many. Summing totals across currencies without converting produces a number with no meaning. Either filter to one currency or convert, and if you convert, record the rate and the date you used. ## Pagination ```js async function transactions(key, from, to) { const rows = []; let url = `https://api.paddle.com/transactions?billed_at[GTE]=${from}&billed_at[LT]=${to}&per_page=100`; while (url) { const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); const body = await res.json(); rows.push(...body.data); // Page until there is no next link. A short page does not mean the end. url = body.meta?.pagination?.next ?? null; } return rows; } ``` The `next` link already carries your filters, so do not rebuild it. ## Or put it in the notch Notch Metrics reports [Paddle revenue](/sources/paddle/revenue) from transactions billed in the window that are paid or completed, by their total including tax, before Paddle's fee, bucketed by billing date. Hourly, daily, weekly or monthly, with a sparkline and a comparison against the previous window. It also reports [recent payments](/sources/paddle/recent-payments) as a feed of the last ten transactions with who paid. [Everything Paddle reports](/sources/paddle), or [download Notch Metrics](/download). ## Questions **How do I filter Paddle transactions by date?** Use the square-bracket operator syntax, such as billed_at[GTE] and billed_at[LT], with RFC 3339 timestamps. The operators are case-sensitive: [GTE] works and [gte] does not. **Should I bucket Paddle revenue by created_at or billed_at?** billed_at. A renewal is often created before it is billed, and a transaction that sat in past_due while dunning ran was created one week and billed the next. Bucketing by creation puts the money on the wrong day. **What is the difference between total and earnings on a Paddle transaction?** total is what the customer paid, including tax. earnings is what you will be paid out, after Paddle's fee and after tax. Paddle is a merchant of record, so the tax portion is not yours. **Why is Paddle revenue higher than the same figure in Stripe?** Because Paddle totals include tax and Stripe gross charges do not. Paddle is a merchant of record and collects VAT on your behalf, so the two are not a like-for-like comparison. **How does pagination work on the Paddle API?** Paddle returns a cursor at meta.pagination.next, which already carries your filters. Page until there is no next link rather than until you see a short page, because Paddle can return a short page with more to come. --- # How to pull Polar revenue and orders by date URL: https://notchmetrics.com/blog/pull-polar-revenue-and-orders-by-date Published: 11 October 2026 Author: Notch Metrics Tags: polar, revenue, api ## Which Polar endpoint should you use for revenue? Polar gives you two ways to answer "how much did we take last week" and they are not interchangeable. `/v1/metrics/` returns a precomputed series. `/v1/orders/` returns the orders themselves. The first is one call and gives you a chart. The second is many calls and gives you the rows behind it. Use metrics for a figure. Use orders when you need to know who, or what, or to apply a filter Polar does not offer. ## Revenue from the metrics endpoint ```bash curl -s 'https://api.polar.sh/v1/metrics/?start_date=2026-10-01&end_date=2026-10-08&interval=day&metrics=revenue&timezone=Europe/London' \ -H 'Authorization: Bearer polar_oat_...' ``` What comes back is order subtotals in each period, before Polar's fees and before tax, in the organisation's currency. Three words in that sentence matter. **Subtotals.** Tax is excluded. A business selling to the EU takes considerably more at the card than this figure shows, and the difference is not yours to keep. **Before fees.** This is gross. What lands in your bank is less Polar's cut, and that is a separate calculation. **The organisation's currency.** Polar converts, so a multi-currency business gets one number rather than several. Useful, and worth knowing it happened. ## Orders, when you need the rows ```js async function orders(token, from, to) { const rows = []; let page = 1; while (true) { const url = new URL("https://api.polar.sh/v1/orders/"); url.searchParams.set("limit", "100"); url.searchParams.set("page", String(page)); const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const body = await res.json(); for (const order of body.items) { const at = new Date(order.created_at).getTime(); if (at >= from && at < to) rows.push(order); } if (body.items.length < 100) break; page += 1; } return rows; } ``` Polar pages with `page` and `limit` rather than a cursor, and `pagination.max_page` tells you when to stop if you would rather not infer it from a short page. Each order carries the customer, the product, the subtotal, the tax and the net amount. That is what you need for a recent payments feed, and it is what you need to split revenue by product, which the metrics endpoint will not do. ## Why your totals might not match Summing order subtotals and asking the metrics endpoint for the same window should agree, and when they do not it is almost always one of three things. **Timezone.** The metrics endpoint buckets in whatever timezone you passed. `created_at` on an order is UTC. Compare them without converting and every order near midnight is misfiled. **Refunded orders.** An order refunded later still happened on the day it was placed. Both figures include it, which is correct and surprises people. **Pagination.** A loop that stops at the first short page is right. A loop that stops when `items` is empty makes one wasted call. A loop that trusts a hardcoded page count is wrong the week you grow. ## In the notch Notch Metrics reports [Polar revenue](/sources/polar/revenue) from the metrics endpoint, bucketed hourly, daily, weekly or monthly with a sparkline and a comparison against the previous window, and [recent payments](/sources/polar/recent-payments) from the orders list as a feed of the last ten with who paid. Both in the MacBook notch, both from one connection whose token never reaches your Mac. [Everything Polar reports](/sources/polar), or [download Notch Metrics](/download). ## Questions **How do I get Polar revenue for a date range?** Call GET /v1/metrics/ with start_date, end_date, interval and metrics=revenue. It returns order subtotals per period, before Polar's fees and before tax, in the organisation's currency. **Does Polar revenue include tax?** No. The revenue metric is order subtotals, which exclude tax. A business selling into the EU takes more at the card than this figure shows, and the difference is not theirs to keep. **How does pagination work on the Polar orders endpoint?** Polar pages with page and limit rather than a cursor, and the response carries pagination.max_page. Stop at the first page shorter than your limit, or read max_page. **Why do my Polar order totals disagree with the metrics endpoint?** Usually timezone. The metrics endpoint buckets in the timezone you passed and created_at on an order is UTC, so orders near midnight land on different days. Refunded orders are also counted by both on the day they were placed. **Can I split Polar revenue by product?** Not from the metrics endpoint. Page /v1/orders/ instead; each order carries the product, the subtotal, the tax and the net amount. --- # How to pull sessions and page views from GA4 URL: https://notchmetrics.com/blog/pull-sessions-and-page-views-from-ga4 Published: 11 October 2026 Author: Notch Metrics Tags: google-analytics, ga4, api ## One request, both metrics The Data API takes a list, so sessions and page views come back together. ```json { "dateRanges": [{ "startDate": "2026-10-01", "endDate": "2026-10-07" }], "dimensions": [{ "name": "date" }], "metrics": [{ "name": "sessions" }, { "name": "screenPageViews" }] } ``` `metricValues` in each row is positional, matching the order you asked. Build a lookup from your own request rather than indexing with a literal, because reordering the metrics array silently reassigns every reader downstream. ```js const index = Object.fromEntries(query.metrics.map((m, i) => [m.name, i])); const sessions = Number(row.metricValues[index.sessions].value); ``` ## What a GA4 session is A session starts with a `session_start` event and ends after thirty minutes of inactivity, or at midnight in the property's timezone. That midnight rule is the one that differs from most tools. A visit spanning midnight is **two sessions** in GA4, not one. On a consumer product with evening traffic this inflates session counts relative to anything that stitches across the boundary. The thirty-minute timeout is configurable per property. If somebody changed it, your historical series has a step in it that is not behaviour. ## engagedSessions is the one people actually want GA4 added a definition that Universal Analytics did not have. | Metric | What it counts | | ----------------- | ------------------------------------------------- | | `sessions` | Every session | | `engagedSessions` | Lasted 10s+, or had 2+ pageviews, or a conversion | | `engagementRate` | Engaged over total | | `bounceRate` | 1 minus engagement rate | GA4's bounce rate is therefore the inverse of engagement, not Universal Analytics' single-pageview definition. The two numbers are not comparable and a chart that puts them on one axis across the migration date is wrong. For most purposes `engagedSessions` is the more honest traffic figure, because it excludes the accidental arrivals that a raw session count treats as visits. ## screenPageViews counts repeats Unlike users, page views are not de-duplicated. A visitor loading the same page five times is five page views, which is what a page view is. That also means summing daily rows **is** correct for page views, unlike active users. The two metrics in the same response behave differently under addition, which is a genuinely confusing property of GA4 reporting and worth a comment in whatever code does the summing. ## Pages, not just totals Swap the dimension to get the breakdown: ```json { "dimensions": [{ "name": "pagePath" }], "metrics": [{ "name": "screenPageViews" }], "orderBys": [{ "metric": { "metricName": "screenPageViews" }, "desc": true }], "limit": 20 } ``` `pagePath` excludes the host and the query string. `pagePathPlusQueryString` keeps the query, which fragments your top pages across every UTM variant and is almost never what you want in a ranking. ## Timezone Every date dimension is bucketed in the property's reporting timezone, which is set in GA4 and not derived from your request. A change to it does not restate history, so a property whose timezone was corrected has a seam. ## In the notch Notch Metrics reports [GA4 sessions](/sources/google-analytics/sessions) and [page views](/sources/google-analytics/pageviews) in the MacBook notch, daily, weekly or monthly, with a sparkline and a comparison against the previous window. Place them beside [active users](/sources/google-analytics/active-users) and the three describe the visit rather than just counting it. [Everything Google Analytics reports](/sources/google-analytics), or [download Notch Metrics](/download). ## Questions **What counts as a session in GA4?** A session starts with a session_start event and ends after thirty minutes of inactivity, or at midnight in the property's timezone. The midnight rule means a visit spanning midnight is two sessions, unlike most other tools. **What is the difference between sessions and engagedSessions in GA4?** An engaged session lasted at least ten seconds, had two or more pageviews, or included a conversion. engagedSessions excludes the accidental arrivals that a raw session count treats as visits, which makes it the more honest traffic figure. **Is GA4 bounce rate the same as Universal Analytics bounce rate?** No. GA4 bounce rate is one minus engagement rate. Universal Analytics counted single-pageview sessions. The two are not comparable and should not share an axis across a migration date. **Can I sum daily GA4 page views?** Yes. Unlike active users, screenPageViews is not de-duplicated, so daily rows add up correctly. The two metrics behave differently under addition even in the same response. **Should I use pagePath or pagePathPlusQueryString in GA4?** pagePath for a top pages ranking. pagePathPlusQueryString keeps the query string, which fragments one page across every UTM variant. --- # How to query active users from the Amplitude Dashboard REST API URL: https://notchmetrics.com/blog/query-active-users-from-the-amplitude-dashboard-api Published: 11 October 2026 Author: Notch Metrics Tags: amplitude, analytics, api ## Which Amplitude API answers this? The Dashboard REST API, which is the read-only one. Amplitude also has an HTTP API for sending events and an Export API for raw data, and neither answers "how many active users last week". ```bash curl -s -u "$API_KEY:$SECRET_KEY" \ 'https://amplitude.com/api/2/users?start=20261001&end=20261007&m=active&i=1' ``` ## Basic auth with two keys Not a bearer token. The API key is the username and the secret key is the password, in HTTP Basic auth. ```js const auth = Buffer.from(`${apiKey}:${secretKey}`).toString("base64"); const res = await fetch(url, { headers: { Authorization: `Basic ${auth}` } }); ``` Both come from project settings and both are project-scoped. The secret key is a secret in the real sense: it reads every event in the project, so it belongs on a server and never in a client. ## The regional host | Region | Host | | ------ | ---------------------------- | | US | `amplitude.com` | | EU | `analytics.eu.amplitude.com` | A US key pair against the EU host fails to authenticate, and the message reads like a revoked key. People regenerate credentials over this. Check the host first. ## The m parameter decides what you get `/api/2/users` returns active or new users depending on one letter. | `m` | What it counts | | -------- | ------------------------------------------- | | `active` | Users with at least one event in the window | | `new` | Users seen for the first time in the window | Everything else about the call is identical, which is convenient and means a single character typo changes your metric without changing your code shape. ## Dates and intervals `start` and `end` are `YYYYMMDD` with no separators. Not ISO, not a timestamp. `20261001`. `i` is the interval: `1` for daily, `7` for weekly, `30` for monthly. There is no hourly option, so anything finer than a day is not available from this endpoint at all. ## The response shape ```json { "data": { "series": [[120, 98, 143, 131, 110, 76, 89]], "seriesLabels": [0], "xValues": ["2026-10-01", "2026-10-02", "..."] } } ``` `series` is an array of series, and with no segmentation there is exactly one, so the numbers you want are at `data.series[0]`. `xValues` holds the matching dates. Reading `data.series` as the array of numbers rather than as the array of series is the mistake everybody makes once, and it fails with a type error rather than a wrong number, which is the kind way round. ## De-duplication The daily series de-duplicates within each day. A weekly figure, asked for with `i=7`, de-duplicates across the week. So summing seven daily values does not give you the weekly value. It gives you something larger, inflated by exactly how much your users come back, which is the retention you were presumably trying to measure separately. If you want both, ask twice. ## Event counts use a different endpoint Active users come from `/api/2/users`. Event volume comes from `/api/2/events/segmentation`, with its own parameters and the same response shape. Two endpoints, one shape, which makes a shared parser worth writing. ## Or read it off the notch Notch Metrics calls this endpoint and reports [Amplitude active users](/sources/amplitude/active-users), [new users](/sources/amplitude/new-users) and [events](/sources/amplitude/events) in the MacBook notch, daily, weekly or monthly, bucketed in your Amplitude project's own timezone. Both keys are encrypted on the server and neither reaches your Mac. [Everything Amplitude reports](/sources/amplitude), or [download Notch Metrics](/download). ## Questions **How do I authenticate with the Amplitude Dashboard REST API?** HTTP Basic auth, with the API key as the username and the secret key as the password. Not a bearer token. Both are project-scoped and the secret key reads every event in the project, so it belongs on a server. **What is the Amplitude EU API host?** analytics.eu.amplitude.com, against amplitude.com for US. A key pair from one region fails against the other with a message that reads like a revoked key, so check the host before regenerating credentials. **How do I get new users instead of active users in Amplitude?** Change the m parameter on /api/2/users from active to new. Everything else about the call is identical, which means a single character changes your metric. **What date format does the Amplitude API use?** YYYYMMDD with no separators, such as 20261001. Not ISO 8601 and not a timestamp. **Why is data.series an array of arrays in Amplitude?** Because it holds one entry per segment. With no segmentation there is exactly one, so your numbers are at data.series[0]. Reading data.series directly as numbers fails with a type error rather than a wrong figure. --- # How to query active users from the PostHog API URL: https://notchmetrics.com/blog/query-active-users-from-the-posthog-api Published: 11 October 2026 Author: Notch Metrics Tags: posthog, analytics, api ## Which PostHog endpoint answers "how many active users"? The query endpoint, with HogQL. PostHog has an insights API and a trends API, but both make you construct a filter object and then interpret an output shape designed for a chart component. HogQL is SQL. You write what you want and get back columns you named. ```bash curl -s 'https://eu.posthog.com/api/projects/12345/query/' \ -H 'Authorization: Bearer phx_...' \ -H 'Content-Type: application/json' \ -d '{ "query": { "kind": "HogQLQuery", "query": "SELECT uniq(person_id) AS value FROM events WHERE timestamp >= toDateTime(1759276800) AND timestamp < toDateTime(1759881600)" } }' ``` The response carries a `results` array of rows, matching the columns you selected. No shape to reverse-engineer. ## uniq, and why the number is smaller than you expected `uniq(person_id)` de-duplicates across everything the query covers. Over a week, somebody active on three days counts once. That means a weekly active user count is **not** the sum of seven daily counts, and it is not the average either. It is strictly smaller than the sum and usually much smaller. If you want a daily series alongside the de-duplicated total, that is two queries, not one: ```sql -- The series. Each day de-duplicated within itself. SELECT toUnixTimestamp(toStartOfDay(timestamp)) AS bucket, uniq(person_id) AS value FROM events WHERE timestamp >= toDateTime({from}) AND timestamp < toDateTime({to}) GROUP BY bucket ORDER BY bucket -- The headline. De-duplicated across the whole window. SELECT uniq(person_id) AS value FROM events WHERE timestamp >= toDateTime({from}) AND timestamp < toDateTime({to}) ``` Running only the first and summing it is the single most common PostHog reporting bug, and it inflates the figure by however much your users come back, which is exactly the thing you were trying to measure. ## person_id, not distinct_id `distinct_id` is per device and per session identifier. `person_id` is PostHog's resolved person after identity merging, so the same human on a phone and a laptop is one person. Counting `distinct_id` counts devices. Sometimes that is what you want. It is almost never what somebody means by "active users". ## toStartOfWeek starts on Monday ClickHouse's `toStartOfWeek` defaults to Sunday. PostHog's own dashboards default to Monday. The two disagree by a day and the difference is invisible until somebody compares a weekly chart against the UI. ```sql toStartOfWeek(timestamp, 1) -- 1 = Monday ``` ## Timezone HogQL evaluates in the project's timezone, which is what you want: it makes your query agree with the PostHog UI. If you need a different one, `toTimeZone(timestamp, 'Europe/London')` is explicit, and being explicit is worth the extra characters in anything that will be read later. ## Name your queries ```json { "query": { "kind": "HogQLQuery", "query": "..." }, "name": "notch:active_users" } ``` PostHog surfaces query names in its own usage views. Anybody looking at who is hammering their project sees a name rather than an anonymous `SELECT`, which is the difference between a question and an incident. ## Or read it off the notch Notch Metrics queries PostHog this way and reports [active users](/sources/posthog/active-users) in the MacBook notch, with the headline de-duplicated across the window and each bar de-duplicated within its own day. The page says so rather than leaving you to discover it. [New users](/sources/posthog/new-users), [events](/sources/posthog/events), [pageviews](/sources/posthog/pageviews) and [sessions](/sources/posthog/sessions) sit beside it. [Everything PostHog reports](/sources/posthog), or [download Notch Metrics](/download). ## Questions **How do I query active users in PostHog with the API?** POST to /api/projects/{id}/query/ with a HogQLQuery and SELECT uniq(person_id) over your time range. HogQL is SQL, so you get back the columns you named instead of a chart-shaped payload. **Why is my PostHog weekly active user count lower than the sum of daily counts?** Because uniq de-duplicates across the whole window. Somebody active on three days counts once. A weekly active count is strictly smaller than the sum of seven daily ones, and summing a daily series is the most common PostHog reporting bug. **Should I count person_id or distinct_id in PostHog?** person_id for users. It is PostHog's resolved person after identity merging, so the same human on a phone and a laptop is one person. distinct_id counts devices. **Why does my PostHog weekly chart disagree with the UI by a day?** ClickHouse's toStartOfWeek defaults to Sunday and PostHog's dashboards default to Monday. Pass 1 as the second argument, toStartOfWeek(timestamp, 1), to start weeks on Monday. **Should I name my PostHog HogQL queries?** Yes. Pass a name alongside the query. PostHog surfaces query names in its usage views, so anybody looking at load on the project sees who is asking rather than an anonymous SELECT. --- # How to read bounce rate from Plausible URL: https://notchmetrics.com/blog/read-bounce-rate-from-plausible Published: 11 October 2026 Author: Notch Metrics Tags: plausible, bounce-rate, api ## What is a bounce in Plausible? A session with exactly one pageview. The visitor arrived, looked, and left without loading a second page. ```bash curl -s https://plausible.io/api/v2/query \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "site_id": "example.com", "metrics": ["bounce_rate"], "date_range": "30d" }' ``` Plausible returns it as a percentage. Notch Metrics holds it as a fraction and renders the percentage, because a ratio stored as a number between 0 and 100 is a number somebody eventually divides by 100 twice. ## The thing a bounce rate cannot see Time. A session with one pageview is a bounce whether the visitor left after two seconds or read for twenty minutes. That is not a Plausible limitation, it is what a pageview-based definition can know. Without a second page load or an engagement event, the browser never tells anybody how long the visitor stayed. So the number conflates two opposite outcomes: somebody who bounced off a page that was wrong for them, and somebody who got exactly what they came for on the first page and had no reason to click further. ## Which is why high is not automatically bad Three cases where a high bounce rate is fine or good. **A blog.** Search brings somebody to one article, they read it, they leave. That is the article working. **A documentation page.** They came for a specific answer, found it, and went back to their editor. A low bounce rate here would mean people cannot find things. **A contact or pricing page as a landing page.** One page, one decision. The case where it is genuinely bad is a landing page whose whole job is to move somebody to the next step. There a bounce is a failure, and the number is directly actionable. Segment before you judge. A site-wide bounce rate averages a blog that should bounce with a funnel that should not, and the average describes neither. ```json { "site_id": "example.com", "metrics": ["bounce_rate", "visitors"], "date_range": "30d", "dimensions": ["event:page"] } ``` ## Where the number moves without behaviour changing **Adding a cookie banner that fires an event.** If a consent interaction registers as activity, bounces fall overnight with nobody reading more. **Anchor links.** A same-page anchor is not a pageview in Plausible, so an in-page table of contents does not rescue a bounce rate, which is correct and catches people who expected it to. **Single-page apps.** If your router does not fire a pageview on navigation, every session is one pageview and your bounce rate is near 100%. That is an instrumentation reading, not a traffic reading, and it is the first thing to check if the figure looks absurd. ## Read it with visitors A bounce rate falling while visitors fall is not an improvement, it is a smaller and more committed audience. Falling while visitors rise is the genuinely good case. The ratio alone has no direction. Put the volume beside it. ## In the notch Notch Metrics reports [Plausible bounce rate](/sources/plausible/bounce-rate) as a ratio, bucketed daily, weekly or monthly in your Plausible site's own timezone, with a sparkline and a comparison against the previous window. Place it beside [unique visitors](/sources/plausible/visitors) and [pageviews](/sources/plausible/pageviews), because a rate with no volume next to it is a number you cannot act on. [Everything Plausible reports](/sources/plausible), or [download Notch Metrics](/download). ## Questions **What counts as a bounce in Plausible?** A session with exactly one pageview. The visitor arrived, looked, and left without loading a second page. Time on the page is not part of the definition. **Is a high bounce rate bad?** Not necessarily. On a blog, a documentation page or a pricing page, somebody arriving, getting what they came for and leaving is the page working. It is genuinely bad only on a page whose job is to move somebody to a next step. **Why is my bounce rate nearly 100% in a single-page app?** Because your router is probably not firing a pageview on navigation, so every session has exactly one pageview. That is an instrumentation reading rather than a traffic reading. **Do anchor links reduce bounce rate in Plausible?** No. A same-page anchor is not a pageview, so an in-page table of contents does not affect the figure. That is correct behaviour and it catches people who expected otherwise. **Should I look at bounce rate on its own?** No. A rate has no direction without volume beside it. Bounce rate falling while visitors fall is a smaller, more committed audience rather than an improvement. --- # How to read estimated revenue and RPM from YouTube Analytics URL: https://notchmetrics.com/blog/read-estimated-revenue-and-rpm-from-youtube-analytics Published: 11 October 2026 Author: Notch Metrics Tags: youtube, revenue, api ## The four money metrics YouTube Analytics reports several revenue figures and they are routinely confused with each other. | Metric | What it includes | | -------------------- | --------------------------------------------------------------- | | `estimatedRevenue` | Everything: ads, memberships, Super Chat, Super Thanks, Premium | | `estimatedAdRevenue` | Advertising only | | `grossRevenue` | Before YouTube's share | | `cpm` | Cost per thousand ad impressions | Plus RPM, which is not in that list because the API does not return it directly. More on that below. ```bash curl -s -G 'https://youtubeanalytics.googleapis.com/v2/reports' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ --data-urlencode 'ids=channel==MINE' \ --data-urlencode 'startDate=2026-09-01' \ --data-urlencode 'endDate=2026-09-30' \ --data-urlencode 'metrics=estimatedRevenue,estimatedAdRevenue,views' \ --data-urlencode 'dimensions=day' ``` ## Estimated revenue against ad revenue The gap between these two is the part of your channel that is not advertising. For a creator with memberships it can be most of the income. Watching only the total hides the shape. A channel whose estimated revenue holds steady while ad revenue falls is being carried by its members, which is a more durable business and a completely different one from the total's implication. Track both. ## RPM is computed, not returned RPM is revenue per thousand views: total revenue over total views, with every view counted. ```js const rpm = (estimatedRevenue / views) * 1000; ``` Compute it over the **whole window**, not by averaging daily rates. A quiet day with one high-value view produces an enormous daily RPM and should contribute almost nothing to the figure. Averaging the points weights it equally with a day of ten thousand views, and the result is a number that jumps around for no reason. ## RPM is not CPM, and this is the big one They sound alike and measure opposite sides of the transaction. | | Denominator | Numerator | | ------- | ------------------------ | --------------------------------------------- | | **RPM** | Every view | Your revenue, after YouTube's share | | **CPM** | Monetised ad impressions | What advertisers paid, before YouTube's share | So CPM is always much higher than RPM, on every channel, and the gap is not a problem to fix. It is two different ratios. Specifically: not every view carries an ad, so the RPM denominator is far larger. And CPM is what the advertiser paid, while RPM is what reached you after YouTube took its cut. Comparing your RPM against somebody's quoted CPM and concluding you are underpaid is the most common mistake in creator revenue discussions. ## Estimated means estimated Every one of these is adjusted after publication for invalid traffic and advertiser disputes. The last few days of a window move, and a month only finalises well into the following one, when YouTube closes it for payment. So a month-to-date revenue figure is a forecast, and a comparison against last month's final figure is not like for like until this month closes. ## Currency Revenue is reported in your AdSense account's currency, and YouTube returns it as a decimal rather than as minor units: `1234.56`, not `123456`. Convert to integer minor units before storing it and never hold money in a float. ## The scope `https://www.googleapis.com/auth/yt-analytics-monetary.readonly`, in addition to the read-only YouTube scope. A channel outside the YouTube Partner Programme has no revenue to report, and the API answers with an error rather than zero. That is the correct behaviour: zero revenue and no revenue programme are different states, and reporting the second as the first would be a lie with a number attached. ## In the notch Notch Metrics reports [YouTube estimated revenue](/sources/youtube/revenue), [ad revenue](/sources/youtube/ad-revenue) and [RPM](/sources/youtube/rpm) in the MacBook notch, with RPM computed over the whole window rather than averaged across daily rates, and the page saying plainly that it is not CPM. [Everything YouTube reports](/sources/youtube), or [download Notch Metrics](/download). ## Questions **What is the difference between RPM and CPM on YouTube?** RPM is your revenue after YouTube's share, divided by every view. CPM is what advertisers paid, before YouTube's share, divided by monetised ad impressions. CPM is always much higher, on every channel, and the gap is two different ratios rather than a problem. **Does the YouTube Analytics API return RPM?** No. Compute it as revenue divided by views times a thousand, over the whole window. Averaging daily RPM values weights a quiet day with one high-value view equally with a day of ten thousand views. **What is the difference between YouTube estimated revenue and estimated ad revenue?** Estimated revenue includes advertising, channel memberships, Super Chat, Super Thanks and Premium watch-time revenue. Ad revenue is the advertising part alone. The gap is the part of the channel that is not advertising. **Which scope do I need for YouTube revenue data?** https://www.googleapis.com/auth/yt-analytics-monetary.readonly, in addition to the read-only YouTube scope. Asking for it when you do not need revenue makes the consent screen more alarming than it has to be. **Why does the YouTube API return an error instead of zero revenue?** Because the channel is outside the YouTube Partner Programme. Zero revenue and no revenue programme are different states, and reporting the second as the first would be a lie with a number attached. --- # How to read MRR and revenue from the RevenueCat Charts API URL: https://notchmetrics.com/blog/read-mrr-and-revenue-from-the-revenuecat-charts-api Published: 11 October 2026 Author: Notch Metrics Tags: revenuecat, revenue, api ## Where does RevenueCat expose its dashboard numbers? Under v2 project metrics. The Overview endpoint returns the figures the dashboard header shows, and the Charts API gives you the series behind the dashboard's graphs. For a headline number, Overview is one call and no paging: ```bash curl -s 'https://api.revenuecat.com/v2/projects/{project_id}/metrics/overview' \ -H 'Authorization: Bearer sk_...' ``` ## Read metrics by id The response is an array, and an array's order is not a contract. A new metric appearing in the middle would silently shift every positional read after it. ```js const by = (body, id) => body.metrics?.find((m) => m.id === id)?.value ?? null; const mrr = by(body, "mrr"); const revenue = by(body, "revenue"); const subs = by(body, "active_subscriptions"); const trials = by(body, "active_trials"); ``` ## What MRR means here RevenueCat's own figure: active paid subscriptions normalised to a month, as shown on the Overview. It is a snapshot of the book right now. Read RevenueCat's rather than computing your own. Mobile subscriptions have more edges than card subscriptions do, with introductory offers, promotional offers, family sharing, billing grace periods and two stores reporting differently, and RevenueCat has already decided what to do with each. A second opinion is a second number to defend. ## Revenue is gross, and the gap is large This catches everybody once. RevenueCat revenue is gross across every app in the project for the days in the window, **before store commission and before taxes**. Apple and Google take a commission that depends on the programme you are in and on how long the subscriber has been subscribed. Tax comes off separately and varies by country. The money that reaches your bank is well below the revenue figure, and the difference is not a constant you can multiply by. If you want proceeds rather than revenue, that is a different question and RevenueCat exposes it separately. Label your chart for whichever one you chose, because a revenue figure presented as income is the kind of mistake that reaches a board deck. ## Today is partial The newest day in any window is still filling. Transactions arrive from the stores with a lag, so today's figure reads low and settles over the following hours. A daily chart droops at the right-hand end on every refresh. Read trends from the settled part. If you need to know whether today was good, you will find out tomorrow. ## The 28-day metrics `new_customers` and `active_customers` on the Overview are computed over a fixed 28-day window. That window is RevenueCat's and it is not a parameter: asking the endpoint for a different range does not change them. Any interface that offers a range selector next to those two numbers is either ignoring the selector or inventing the answer. Say which. ## In the notch Notch Metrics reads this endpoint and reports [RevenueCat MRR](/sources/revenuecat/mrr), [revenue](/sources/revenuecat/revenue), [active subscriptions](/sources/revenuecat/active-subscribers), [trials](/sources/revenuecat/trials), [new customers](/sources/revenuecat/new-users) and [active customers](/sources/revenuecat/active-users) in the MacBook notch. The two 28-day metrics say on their own pages that the window is fixed and ignores the widget's range, because a control that does nothing is worse than no control at all. [Everything RevenueCat reports](/sources/revenuecat), or [download Notch Metrics](/download). ## Questions **Which RevenueCat endpoint returns MRR?** GET /v2/projects/{project_id}/metrics/overview. It returns the dashboard header figures, including MRR, revenue, active subscriptions and active trials, in a metrics array that should be read by id rather than by position. **Is RevenueCat revenue before or after Apple's commission?** Before. The revenue figure is gross across every app in the project, before store commission and before taxes. The commission depends on the programme and on how long the subscriber has been subscribed, so the gap is not a constant you can multiply by. **Why does today's RevenueCat revenue look low?** Because it is still filling. Transactions arrive from the stores with a lag, so the newest day reads low and settles over the following hours. A daily chart droops at the right-hand end on every refresh. **Can I change the window on RevenueCat new customers?** No. new_customers and active_customers are computed over a fixed 28-day window that is not a parameter. Asking the endpoint for a different range does not change them. **Should I compute my own MRR instead of using RevenueCat's?** Usually not. Mobile subscriptions have introductory offers, promotional offers, family sharing, grace periods and two stores reporting differently, and RevenueCat has already decided what to do with each. A second opinion is a second number to defend. --- # How to get active subscribers from the RevenueCat API, v1 against v2 URL: https://notchmetrics.com/blog/revenuecat-active-subscribers-api-v1-v2 Published: 11 October 2026 Author: Notch Metrics Tags: revenuecat, subscribers, api ## Is there a RevenueCat endpoint for active subscribers? Not the one you want. This is the single most searched RevenueCat API question and the honest answer is that no endpoint returns "every subscriber who is currently active" as a list. What exists is three different things, and picking between them is the actual task. ## The three options **v1 subscriber lookup.** `GET /v1/subscribers/{app_user_id}` returns one customer with their entitlements and subscriptions. It is designed for checking access for a specific user, and it is the right call inside your app or your backend when somebody signs in. It is the wrong call for counting, because you need the list of ids before you can look any of them up, and RevenueCat will not give you that. **v2 list endpoints.** `GET /v2/projects/{id}/customers` pages customers, and there are subscription endpoints under v2 as well. The catch is coverage: the v2 subscription listing is oriented around subscriptions RevenueCat processes directly, and store-managed subscriptions from App Store and Play Store do not all surface there the way people expect. Read it as a customer directory rather than as a subscriber count. **The Overview metrics.** `GET /v2/projects/{id}/metrics/overview` returns the figures the dashboard shows, including active subscriptions, active trials, MRR and revenue. One call, no paging, and it agrees with the dashboard. For a count, this is the answer. For a list, there is no answer. ## The call that actually counts ```bash curl -s 'https://api.revenuecat.com/v2/projects/proj1ab2c3d4/metrics/overview' \ -H 'Authorization: Bearer sk_...' ``` The response carries a `metrics` array of objects with an `id` and a `value`. Read them by id rather than by position, because the order is not a contract and a new metric appearing in the middle would silently shift everything after it. ```js function metric(body, id) { return body.metrics?.find((m) => m.id === id)?.value ?? null; } const activeSubs = metric(body, "active_subscriptions"); const activeTrials = metric(body, "active_trials"); const mrr = metric(body, "mrr"); ``` ## What active subscriptions includes Paid subscriptions active right now, including ones that have been cancelled or are in a grace period, until they actually expire. Trials are not counted, which is why there is a separate trials figure. Including grace-period subscriptions is the right call: a billing retry in progress is still a customer, and excluding them would make your count dip every time Apple's renewal queue is slow. ## Use a v2 secret key v1 and v2 keys are different objects. A v1 key against a v2 endpoint returns 401, which reads like a revoked key rather than a wrong version. Create a new secret key in project settings, select V2, and give it read permissions. Project-scoped, so the project id in the URL has to be the one the key belongs to. ## If you really need the list Record it yourself. Subscribe to RevenueCat's webhooks for `INITIAL_PURCHASE`, `RENEWAL`, `CANCELLATION` and `EXPIRATION`, and maintain your own table of who is active. That is the only reliable way to hold a current subscriber list, and it has the useful side effect of letting you query it however you like. ## Or read the count off the notch Notch Metrics reads the overview endpoint and reports [RevenueCat active subscriptions](/sources/revenuecat/active-subscribers) and [active trials](/sources/revenuecat/trials) in the MacBook notch, with [MRR](/sources/revenuecat/mrr) and [revenue](/sources/revenuecat/revenue) beside them. One connection, a v2 secret key that never reaches your Mac. [Everything RevenueCat reports](/sources/revenuecat), or [download Notch Metrics](/download). ## Questions **Is there a RevenueCat API endpoint to list all active subscribers?** No. No endpoint returns every currently active subscriber as a list. The v2 project metrics overview returns the count, and for a list you have to maintain your own table from RevenueCat webhooks. **What is the difference between RevenueCat API v1 and v2?** v1 is oriented around looking up one customer by app user id, which is what you want inside an app. v2 adds project-scoped list and metrics endpoints. They use different keys: a v1 key against a v2 endpoint returns 401, which reads like a revoked key rather than a wrong version. **Which RevenueCat endpoint returns active subscription count?** GET /v2/projects/{id}/metrics/overview. It returns the same figures the dashboard shows, including active subscriptions, active trials, MRR and revenue, in one call with no paging. **Does RevenueCat count grace-period subscriptions as active?** Yes. Paid subscriptions that have been cancelled or are in a billing grace period count until they actually expire. That is the right call: a retry in progress is still a customer, and excluding them would make the count dip whenever a store renewal queue is slow. **How do I get a list of RevenueCat subscribers?** Build it yourself from webhooks. Subscribe to INITIAL_PURCHASE, RENEWAL, CANCELLATION and EXPIRATION and maintain your own table. It is the only reliable way to hold a current subscriber list, and it lets you query it however you like. --- # RevenueCat MRR and trials on your Mac URL: https://notchmetrics.com/blog/revenuecat-mrr-and-trials-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: revenuecat, mrr, mac App subscriptions live behind two stores and one dashboard. RevenueCat unifies the stores. The dashboard is still a tab. ## How do you watch RevenueCat without opening it? Notch Metrics renders the figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What RevenueCat reports | Number | What it is | | -------------------------------------------------------------- | ---------------------------------------- | | [MRR](/sources/revenuecat/mrr) | RevenueCat's own MRR for the project | | [Revenue](/sources/revenuecat/revenue) | Gross, before store commission and tax | | [Active subscriptions](/sources/revenuecat/active-subscribers) | Paid, trials excluded | | [Active trials](/sources/revenuecat/trials) | Free trials running right now | | [New customers](/sources/revenuecat/new-users) | First seen in the last 28 days | | [Active customers](/sources/revenuecat/active-users) | Contacted RevenueCat in the last 28 days | Notch Metrics reads MRR from RevenueCat's own overview endpoint rather than recomputing it, so the notch and the RevenueCat dashboard agree. Reporting a second opinion on somebody else's headline number is how two screens in the same company end up disagreeing about the business. ## Two things that are not obvious **Revenue is gross.** Before Apple's and Google's commission, and before tax. What reaches your bank is meaningfully less, and the gap depends on which store and which year of the subscription. **The customer windows are fixed at 28 days.** RevenueCat computes new and active customers over its own 28-day window on the Overview, and that window ignores whatever range you set on the widget. The [new customers](/sources/revenuecat/new-users) and [active customers](/sources/revenuecat/active-users) pages say so plainly, because a widget that silently ignores its own range control is worse than one that explains why. ## Trials beside subscriptions [Active trials](/sources/revenuecat/trials) is the earliest number in a mobile subscription funnel. It is a snapshot of how many people are inside a free trial right now, not how many started one in the window. Place it next to active subscriptions and the pair reads properly: trials falling while subscriptions rise is conversion, and trials falling while subscriptions hold flat is not. ## Connecting A RevenueCat v2 secret key and a project id. [What the connector asks for](/sources/revenuecat). The key is encrypted on the server with a key unique to that one connection, so the Mac app never holds it. ## Alongside everything else Revenue also comes from [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments) and [Lemon Squeezy](/sources/lemonsqueezy). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see RevenueCat MRR on my Mac?** Install Notch Metrics, connect RevenueCat on the web with a v2 secret key and a project id, and place the MRR widget in the MacBook notch. Hover the camera housing and the figure is there. **Is RevenueCat revenue net of Apple and Google commission?** No. The revenue figure is gross, before store commission and before tax. What reaches your bank is meaningfully less, and the gap depends on which store and which year of the subscription. **Why do RevenueCat new and active customers ignore the widget's range?** Because RevenueCat computes them over a fixed 28-day window on its Overview, and that window is not configurable. The metric pages say so rather than letting a range control appear to work when it does not. **What is the difference between RevenueCat active subscriptions and active trials?** Active subscriptions counts paid subscriptions, including ones cancelled or in a grace period until they expire, with trials excluded. Active trials counts free trials running right now. Placed together, trials falling while subscriptions rise is conversion. **Does Notch Metrics compute its own RevenueCat MRR?** No. It reads RevenueCat's own MRR from the project overview, so the notch and the RevenueCat dashboard agree rather than offering a second opinion on somebody else's headline number. --- # Search Console clicks in your MacBook notch URL: https://notchmetrics.com/blog/search-console-clicks-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: google-search-console, seo, mac Search Console is where you find out whether yesterday's post landed. It is also a slow interface you load, filter, and close. ## How do you watch Search Console without opening it? Notch Metrics puts the four numbers in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back. ## What Search Console reports | Number | What it is | | ------------------------------------------------------------------- | ------------------------------------------ | | [Clicks](/sources/google-search-console/clicks) | Visits that started in Google Search | | [Impressions](/sources/google-search-console/impressions) | How often you appeared in results | | [CTR](/sources/google-search-console/ctr) | Clicks over impressions, across the window | | [Average position](/sources/google-search-console/average-position) | Where you sat, lower is better | ## The two to three day lag, which changes how you read all of them Search Console data settles over roughly two to three days. The newest buckets read low until it does. So a daily sparkline droops at the right-hand end, on every site, on every refresh. That droop is a reporting artefact and not a drop in traffic. Read the trend from the settled part of the window. It also means the honest answer to "how did today go" is that you will find out on Thursday. Every [metric page](/sources/google-search-console) says so rather than letting the chart imply otherwise. ## Average position reads backwards Lower is better. A move from 12.4 to 8.1 is an improvement and the line goes down, which is the opposite of every other widget in the notch. It is formatted with one decimal and no compacting to make clear it is a rank rather than a count. [Why that matters](/sources/google-search-console/average-position). ## OAuth, and picking a site Search Console connects over OAuth with read-only Webmasters scope. After approving, you pick which property to read, because most accounts have several. [What the Search Console connector asks for](/sources/google-search-console). Nothing is typed in. The token is encrypted on the server and never reaches your Mac. The connector is marked beta: it connects and returns figures, and the edges are still being worked on. ## Alongside everything else Traffic from [Plausible](/sources/plausible) and [Google Analytics](/sources/google-analytics), revenue from [Stripe](/sources/stripe), video from [YouTube](/sources/youtube). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see Search Console clicks on my Mac?** Install Notch Metrics, connect Google Search Console over OAuth, pick the property, and place the clicks widget in the MacBook notch. Hover the camera housing and the figure is there. **Why does my Search Console chart always dip at the end?** Because Search Console data settles over two to three days, so the newest buckets read low until it does. The droop is a reporting artefact rather than a drop in traffic, and it appears on every site on every refresh. **Why is lower better for average position?** Because it is a rank rather than a count. Position 8.1 is better than 12.4, so an improving line goes down. Notch Metrics formats it with one decimal and no compacting to make clear it is a rank. **What Google permissions does the Search Console connector ask for?** Read-only Webmasters scope, and nothing else. Search Console connects over OAuth, so nothing is typed in, and the resulting token is encrypted on the server rather than stored on your Mac. **Can I track more than one Search Console property?** Yes, as separate connections. The connector pauses after you authorise so you can pick which property to read, because most accounts have several. --- # How to separate one-time sales from subscriptions in Dodo Payments URL: https://notchmetrics.com/blog/separate-one-time-sales-from-subscriptions Published: 11 October 2026 Author: Notch Metrics Tags: dodopayments, revenue, api ## Why split revenue at all? Because recurring revenue and one-time sales are different assets, and a single total treats them as the same money. Recurring revenue is a book. It carries into next month without anybody doing anything. A one-time sale is finished the moment it lands: next month starts at zero again. A business whose total revenue is flat for three months might be perfectly healthy, or it might be losing subscriptions and covering the gap with launches. The total cannot tell you which. The split can. ## How Dodo Payments distinguishes them A payment tied to a subscription carries a subscription id. A one-off purchase does not. ```js const succeeded = payments.filter((p) => p.status === "succeeded"); const recurring = succeeded.filter((p) => p.subscription_id); const oneTime = succeeded.filter((p) => !p.subscription_id); const sum = (rows) => rows.reduce((total, p) => total + p.total_amount, 0); ``` That is the whole mechanism. It is simple enough that the interesting part is the edge cases. ## The first invoice is recurring When somebody subscribes, the first charge looks exactly like a one-off purchase on the day it happens. A single payment, a single product, a card charged once. It is not. It carries a subscription id, so the filter above puts it in the right bucket, and that is correct: the first invoice of a subscription is the start of a book, not a sale that is over. Code that classifies by product rather than by subscription id gets this wrong, and gets it wrong in the most flattering direction, because launch months look better when every new subscriber counts as a one-time sale. ## MRR is not the recurring half of revenue Two different numbers, and conflating them is the common mistake. Recurring revenue in a window is what renewals actually charged in those days. MRR is the normalised monthly value of the book as it stands right now, including annual plans divided by twelve. A month with many annual renewals shows enormous recurring revenue and unchanged MRR. Both figures are correct. They answer different questions: recurring revenue is cash that arrived, MRR is what the book is worth. ## What to watch Three ratios are worth more than any one of these figures alone. **One-time share of total.** If it climbs, the business is becoming launch-driven, and launch-driven businesses have to keep launching. **Recurring revenue against MRR.** If recurring revenue is well above one twelfth of ARR, you are collecting annual plans, which is good for cash and means churn will be invisible for a year. **One-time sales against new subscriptions.** One-time buyers who never subscribe are a different audience, and worth knowing about before you build only for the subscribers. ## In the notch Notch Metrics reports both halves as separate widgets. [Total revenue](/sources/dodopayments/revenue) counts every succeeded payment in the window, renewals and one-off purchases together. [One-time sales](/sources/dodopayments/one-time-sales) counts only the payments not tied to a subscription. Both gross, before refunds and fees. [MRR](/sources/dodopayments/mrr) sits beside them as a snapshot of the book, so the three together say what kind of business you are running this month. [Everything Dodo Payments reports](/sources/dodopayments), or [download Notch Metrics](/download). ## Questions **How do I tell a Dodo Payments subscription renewal from a one-time sale?** A payment tied to a subscription carries a subscription id and a one-off purchase does not. Filtering on the presence of that field is the whole mechanism. **Is the first invoice of a subscription a one-time sale?** No. It carries a subscription id, so it belongs to recurring revenue. Classifying by product rather than by subscription id gets this wrong in the most flattering direction, because launch months then look better than they were. **Is recurring revenue the same as MRR?** No. Recurring revenue in a window is what renewals actually charged in those days. MRR is the normalised monthly value of the book as it stands, with annual plans divided by twelve. A month full of annual renewals shows huge recurring revenue and unchanged MRR. **Why does splitting one-time sales from subscriptions matter?** Because a flat total can hide a business losing subscriptions and covering the gap with launches. Recurring revenue carries into next month on its own. A one-time sale has to be earned again. **Can Notch Metrics show both halves at once?** Yes. Total revenue and one-time sales are separate widgets, and MRR sits beside them. Place all three in the notch and the shape of the month is legible at a glance. --- # Shopify sales and orders in your MacBook notch URL: https://notchmetrics.com/blog/shopify-sales-and-orders-in-your-notch Published: 11 October 2026 Author: Notch Metrics Tags: shopify, sales, mac Shopify's admin is a good dashboard and a slow page. On a busy day you open it to read one number and wait for twelve. ## How do you watch Shopify sales without the admin? Notch Metrics puts the figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What Shopify reports | Number | What it is | | ----------------------------------------------------------- | ----------------------------------------- | | [Sales](/sources/shopify/revenue) | Order totals for orders that took payment | | [Orders](/sources/shopify/orders) | Orders placed in the window | | [Average order value](/sources/shopify/average-order-value) | Sales divided by orders | | [Refunds](/sources/shopify/refunds) | Refunded amounts in the window | | [New customers](/sources/shopify/new-customers) | First-time buyers | | [Customers](/sources/shopify/customers) | Every customer record, ever | | [Recent payments](/sources/shopify/recent-payments) | The last ten orders | The first three belong together. Sales rising on flat orders is a bigger basket. Sales rising on flat average order value is simply more customers. One widget cannot tell you which; three can. ## Connecting A shop domain and a custom app access token. [What the Shopify connector asks for](/sources/shopify). The token needs read access to orders and customers and nothing else, which is the smallest set that answers these questions. It is encrypted on the server with a key unique to that one connection, bound to your account and the provider, so the Mac app never holds it. ## The connector is in beta Shopify's connector is marked beta on [its page](/sources/shopify), and beta here means the edges are still being worked on rather than that the numbers are unavailable. It connects and it returns figures today. ## Alongside everything else If you also take subscription revenue, [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments) and [Lemon Squeezy](/sources/lemonsqueezy) sit in the same notch. Traffic comes from [Plausible](/sources/plausible) and [Google Analytics](/sources/google-analytics), search from [Search Console](/sources/google-search-console). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year with every connector included. [Download Notch Metrics](/download). ## Questions **How do I see Shopify sales on my Mac without opening the admin?** Install Notch Metrics, connect Shopify on the web with your shop domain and a custom app access token, and place the sales widget in the MacBook notch. Hover the camera housing and the figure is there. **What Shopify permissions does Notch Metrics need?** Read access to orders and customers. That is the smallest set that answers sales, orders, average order value, refunds and customer counts, and a read-only token cannot change anything in the store. **Does Notch Metrics show Shopify average order value?** Yes, as its own widget. It is sales divided by orders over the window, and each sparkline point is that day's own ratio, so the headline is deliberately not the average of the points. **Is my Shopify access token stored on my Mac?** No. It is encrypted on the server with a key unique to that one connection. The Notch Metrics server calls Shopify and sends your Mac a normalised number. **Can I show Shopify and Stripe in the same notch?** Yes. Notch Metrics connects Shopify alongside Stripe, Polar, Paddle, Dodo Payments, Lemon Squeezy and RevenueCat, with every connector included and no per-integration charge. --- # Stripe MRR on your Mac, without opening the dashboard URL: https://notchmetrics.com/blog/stripe-mrr-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: stripe, mrr, mac You check Stripe more often than you would admit. Not to do anything, just to look. Open the tab, wait for the chart, read one number, close the tab. That loop costs a context switch every time, and the number was never the expensive part. Getting to it was. ## How do you see Stripe MRR without opening Stripe? Put it somewhere you already look. Notch Metrics renders your Stripe numbers in the MacBook notch, the strip of glass around the camera that every other Mac app pretends is not there. Collapsed, it shows the figures you chose. Hover, and it expands over whatever you were doing. Move the cursor away and it folds back. There is no dock icon and nothing in the menu bar. The app has one surface, and it exists only while you are looking at it. ## What Stripe numbers you get Seven, each one a widget you place and size yourself: | Number | What it is | | ---------------------------------------------------------- | ------------------------------------------------- | | [MRR](/sources/stripe/mrr) | Active subscriptions, each normalised to a month | | [Revenue](/sources/stripe/revenue) | Gross charges in the window, before fees | | [Active subscribers](/sources/stripe/active-subscribers) | Subscriptions in Stripe status `active` right now | | [New subscribers](/sources/stripe/new-subscribers) | Subscriptions created in the window | | [Churned subscribers](/sources/stripe/churned-subscribers) | Subscriptions cancelled in the window | | [Refunds](/sources/stripe/refunds) | Refund balance transactions, as a positive amount | | [Recent payments](/sources/stripe/recent-payments) | The last ten succeeded charges, with who paid | That last one is the surprise. A feed of the ten most recent payments, with the payer's billing name, sitting beside the headline figures. You see who just paid without opening anything. ## Your Stripe key never reaches your Mac You connect Stripe once, on the web. Paste a key, and it is encrypted with a key unique to that one connection before it is stored. Our server calls Stripe and hands your Mac a normalised series of numbers. That split is deliberate. A desktop app holding a live Stripe secret key is a laptop you cannot afford to lose, and people lose laptops. A restricted key with read access to Charges and Subscriptions is enough, and it is safer than a full secret key: a read-only key cannot move money even if it leaked. [What the Stripe connector asks for](/sources/stripe). ## Stripe is not the only source If you take money in more than one place, the notch holds all of it. Revenue also comes from [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments), [Lemon Squeezy](/sources/lemonsqueezy) and [RevenueCat](/sources/revenuecat). Sales come from [Shopify](/sources/shopify). Each reports in its own currency and the conversion happens on the way out. ## Getting it Apple silicon, macOS 14 and later. Seven days of everything, with no card, then $9 a month or $49 a year with every connector included. [Download Notch Metrics](/download), connect Stripe, and close the tab. ## Questions **How do I see my Stripe MRR on my Mac?** Install Notch Metrics, connect Stripe on the web, and place the MRR widget in the MacBook notch. Hover the camera housing and your MRR is there. Notch Metrics runs on Apple silicon, macOS 14 and later. **Does Notch Metrics store my Stripe secret key on my Mac?** No. You paste the key on the web and it is encrypted with a key unique to that one connection. The Notch Metrics server calls Stripe and sends your Mac a normalised number. The Mac app never holds a Stripe key. **What Stripe permissions does Notch Metrics need?** A restricted key with read access to Charges and Subscriptions. That is enough for MRR, revenue, subscribers, churn, refunds and recent payments, and a read-only key cannot move money even if it leaked. **Can I see Stripe and Paddle revenue in the same place?** Yes. Notch Metrics connects Stripe, Polar, Paddle, Dodo Payments, Lemon Squeezy, RevenueCat and Shopify. Each reports in its own currency and the conversion happens on the way out, so a notch showing three providers shows one total. **Is there a free Stripe menu bar app instead?** There are several free menu bar apps that show Stripe MRR. Notch Metrics is $9 a month or $49 a year, and what you get for it is every connector with no per-integration charge, server-side key storage so no credential sits on your Mac, and custom metrics for numbers no connector covers. **How often does the Stripe MRR widget refresh?** Each metric is fetched and normalised on a schedule, so the notch holds a recent figure rather than calling Stripe every time you look. Hover the camera housing and the number is already there. --- # How to get Stripe revenue for a date range with the Balance Transactions API URL: https://notchmetrics.com/blog/stripe-revenue-for-a-date-range Published: 11 October 2026 Author: Notch Metrics Tags: stripe, revenue, api ## Which Stripe endpoint gives revenue for a date range? Balance transactions, not charges. Most people reach for `/v1/charges` because it is the obvious one, and it is wrong in two ways that only show up once real money is moving. A charge is an intent that succeeded. A balance transaction is money that actually arrived in your Stripe balance, and it is where fees, refunds, disputes, payouts and adjustments all live. If you sum charges you will miss every refund and every dispute, and you will have no idea what Stripe took. ## The call ```js async function revenue(stripe, from, to) { let gross = 0; let fees = 0; for await (const txn of stripe.balanceTransactions.list({ created: { gte: Math.floor(from / 1000), lt: Math.floor(to / 1000) }, type: "charge", limit: 100, })) { gross += txn.amount; fees += txn.fee; } return { gross, fees, net: gross - fees }; } ``` `created` takes Unix seconds, not milliseconds, and passing milliseconds returns an empty list rather than an error. That is thirty minutes of your life if you have not met it before. ## The types you have to choose between `type` is the parameter that decides what your number means. | Type | What it is | | ------------ | --------------------------------------------------------- | | `charge` | A successful payment | | `payment` | The same thing, on accounts using certain payment methods | | `refund` | Money returned, as a negative amount | | `adjustment` | A dispute, usually, also negative | | `payout` | Money leaving for your bank. Not revenue | | `stripe_fee` | Monthly billing fees, separate from per-charge fees | Omitting `type` entirely sums all of them, which nets payouts against income and produces a figure close to zero. That is a surprisingly common bug. Note that `charge` and `payment` are different types for historical reasons and which one your account produces depends on how the payment was taken. An account taking both and filtering on only `charge` undercounts. Either list both types or omit the filter and bucket by type yourself. ## Gross, net, and which one to report A balance transaction carries `amount` and `fee`. Gross is the sum of `amount`. Net is gross minus fees, and it is the number that arrives in your bank. Report gross, and report refunds separately. A single net figure hides the thing you need to see: a month that takes fifteen thousand and refunds three is a different business from one that takes twelve and refunds none, and netting them calls the two identical. ## Currency, and the mistake that silently corrupts totals Every amount is in minor units of `txn.currency`. Not dollars, cents. Not yen-with-decimals, yen. Zero-decimal currencies like JPY and KRW have no minor unit at all, so 1000 means 1000 yen and dividing by 100 is wrong. Never hold money in a float. `0.1 + 0.2` is not `0.3` and a year of summing cents into a double drifts. Integers and a currency code, always. An account taking several currencies has several balances, and summing them without converting produces a number that means nothing. ## Timezones `created` is UTC. Stripe's own dashboard reports in the timezone set on your account, so a daily figure computed in UTC will disagree with the dashboard for every charge near midnight. Pick one and write it down. Disagreeing with the dashboard is fine. Not knowing why you disagree is not. ## Or let the notch do it Notch Metrics reads balance transactions and reports Stripe revenue in the MacBook notch, bucketed hourly, daily, weekly or monthly, with a sparkline and a comparison against the previous window. Gross, before Stripe fees, with [refunds reported separately](/sources/stripe/refunds) rather than netted off. Connect Stripe with a read-only restricted key. [What the revenue widget counts](/sources/stripe/revenue), or [everything else Stripe reports](/sources/stripe). ## Questions **Which Stripe API endpoint returns revenue for a date range?** The balance transactions endpoint, filtered with created gte and lt and a type of charge. The charges endpoint misses refunds and disputes and tells you nothing about fees. **Does the Stripe created filter take seconds or milliseconds?** Seconds. Stripe uses Unix seconds throughout. Passing JavaScript milliseconds returns an empty list rather than an error, which is a common and quiet bug. **What is the difference between a Stripe charge and a balance transaction?** A charge is a payment intent that succeeded. A balance transaction is money that actually moved into your Stripe balance, carrying the amount, the fee and the type. Refunds, disputes and payouts exist only as balance transactions. **Should I report gross or net Stripe revenue?** Report gross and report refunds separately. A single net figure hides the difference between a month that takes fifteen thousand and refunds three and a month that takes twelve and refunds none. **How is money represented in the Stripe API?** Integer minor units plus an ISO 4217 currency code. Never a float. Zero- decimal currencies such as JPY and KRW have no minor unit, so dividing by a hundred is wrong for those. **Why does my Stripe API total disagree with the Stripe dashboard?** Usually timezones. The created filter is UTC and the dashboard reports in the timezone set on your account, so every charge near midnight lands on a different day. Pick one convention and write down which. --- # How to track open pull requests on a repository URL: https://notchmetrics.com/blog/track-open-pull-requests-across-a-repository Published: 11 October 2026 Author: Notch Metrics Tags: github, pull-requests, api ## How do you count open pull requests? The search API, same as issues, with the type flipped. ```bash curl -s 'https://api.github.com/search/issues?q=repo:owner/name+is:pr+is:open&per_page=1' \ -H 'Authorization: Bearer ' | jq '.total_count' ``` One request, `total_count`, no paging. The alternative is `/repos/{owner}/{repo}/pulls?state=open`, which returns the rows and makes you page them to count them. Use that when you need the pull requests themselves. ## Do drafts count? Yes, and the argument for it is simple: a draft is open and it is work in flight. Somebody is building something and has shown you. Excluding drafts means your number only moves when work is finished, which is exactly when you no longer need a leading indicator. If you disagree, `is:pr is:open draft:false` excludes them. What matters is picking one and not changing it quietly, because the series is only useful compared against itself. ## The number that actually matters The open count is a stock. What you usually want is the flow, and specifically how long things sit. ```bash # Open, and not touched in a fortnight. q=repo:owner/name+is:pr+is:open+updated:<2026-09-27 ``` Search supports date qualifiers on `created`, `updated`, `merged` and `closed`, which makes a surprising amount of analysis a single query: | Query | What it tells you | | ----------------------------- | ------------------------ | | `is:pr is:open created:DATE` | Throughput | `review:none` is the sharpest of those. A pull request with no review is not slow, it is ignored, and the difference matters to the person who wrote it. ## Open against merged is the real health signal The open count alone says nothing. Fifty open pull requests on a project merging thirty a week is healthy. Twelve on a project merging one a month is a graveyard. Query both and divide: open count over weekly merge rate gives you a rough queue time in weeks. That single number tells a contributor whether their work will be looked at, and it is the one that decides whether they come back. ## Rate limits The search API is 30 requests per minute authenticated, separate from the 5,000 per hour the rest of REST gets. Each of the queries above is one request, so a handful of repositories is fine and a polling loop over fifty is not. Cache. A pull request queue does not change in seconds. ## Forks, and where contributions come from Most outside contributions arrive as a pull request from a fork. The pull request lives on your repository, so it is counted here regardless of where the branch is, and `head.repo.full_name` tells you whether it came from inside or outside. Splitting internal from external is worth doing once. A project whose external pull requests are all old is not short of contributors, it is short of reviewers. ## In the notch Notch Metrics reports [GitHub open pull requests](/sources/github/open-pull-requests) as a count, drafts included, with the repository picked per widget. Place it beside [open issues](/sources/github/open-issues) and the two say whether the project is blocked on maintainers or on contributors. [Everything GitHub reports](/sources/github), or [download Notch Metrics](/download). ## Questions **How do I count open pull requests on GitHub?** Use the search API with q=repo:owner/name+is:pr+is:open and per_page=1, then read total_count. The pulls endpoint returns rows instead and makes you page them to count them. **Should draft pull requests count as open?** Usually yes. A draft is open and it is work in flight. Excluding drafts means the number only moves when work is finished, which is when you no longer need a leading indicator. Use draft:false if you disagree, and keep the choice consistent. **How do I find GitHub pull requests nobody has reviewed?** Search with is:pr is:open review:none. A pull request with no review is not slow, it is ignored, and the difference matters to whoever wrote it. **Is a high open pull request count bad?** Not on its own. Fifty open on a project merging thirty a week is healthy; twelve on a project merging one a month is a graveyard. Divide the open count by the weekly merge rate to get a rough queue time. **How do I tell internal from external GitHub contributions?** Read head.repo.full_name on the pull request. Outside contributions arrive from a fork, and a project whose external pull requests are all old is short of reviewers rather than contributors. --- # How to track Paddle subscription churn URL: https://notchmetrics.com/blog/track-paddle-subscription-churn Published: 11 October 2026 Author: Notch Metrics Tags: paddle, churn, api ## What counts as Paddle churn? Only `canceled`. Paddle has five subscription statuses and three of them look like churn without being it. | Status | Churn? | | ---------- | ----------------------------------------------- | | `active` | No | | `trialing` | No, and not revenue either | | `paused` | No. Suspended on purpose, billing will resume | | `past_due` | No. A payment is failing and dunning is running | | `canceled` | Yes | `paused` is the one that catches people. A customer who pauses intends to come back and Paddle will resume billing them. Counting a pause as churn makes a feature you deliberately offered look like a loss, and it double counts when they resume. `past_due` is the other. Dunning runs for however long you configured, and during that time the customer has not left. Counting past due as churn means counting again when it finally cancels. ## The awkward part Paddle's API cannot filter subscriptions by date. There is no `canceled_at[GTE]`. To answer "how many subscriptions were cancelled last week" you read the subscription list and filter client-side. ```js async function churned(key, from, to) { let count = 0; let url = "https://api.paddle.com/subscriptions?status=canceled&per_page=100"; while (url) { const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } }); const body = await res.json(); for (const sub of body.data) { const at = Date.parse(sub.canceled_at ?? ""); if (at >= from && at < to) count += 1; } url = body.meta?.pagination?.next ?? null; } return count; } ``` That works and it does not scale. An account with fifty thousand historical cancellations pages through all of them to answer a question about seven days, every time. The fix is the same one it always is: subscribe to `subscription.canceled` on a webhook, record the date in your own store, and query that. Paddle's notification system is reliable and this is what it is for. ## canceled_at against the period end `canceled_at` is when the cancellation took effect. Paddle also carries `scheduled_change`, which is where a pending cancellation lives before it happens: a subscription cancelled with the period left to run is still `active` with a scheduled change of type `cancel` and an `effective_at` date. So there are two populations worth tracking separately. Subscriptions that have ended, and subscriptions that are going to. The second is a leading indicator and it is sitting in plain sight on the active list. ## Dunning makes churn lag Involuntary churn arrives through `past_due` and only becomes `canceled` after your retry schedule gives up. Change the schedule and the whole churn series shifts, with no change in customer behaviour at all. Worth remembering before reading a step in the chart as a product signal. ## Count, not rate This counts subscriptions lost. A rate needs a denominator and there is no agreed one: active at the start of the window, active plus new, or MRR rather than count all produce different figures. Pick one, write it down, and do not change it quietly. ## In the notch Notch Metrics reports [Paddle churned subscriptions](/sources/paddle/churned-subscribers) as a count, bucketed by `canceled_at`, with a sparkline and a comparison against the previous window. Paddle cannot filter by date, so the list is read and bounded, which is stated on the page rather than hidden. Place it beside [new subscriptions](/sources/paddle/new-subscribers) and [MRR](/sources/paddle/mrr). [Everything Paddle reports](/sources/paddle), or [download Notch Metrics](/download). ## Questions **Does a paused Paddle subscription count as churn?** No. A pause is suspended on purpose and Paddle will resume billing. Counting it as churn makes a feature you deliberately offered look like a loss, and it double counts when the customer resumes. **Can the Paddle API filter subscriptions by cancellation date?** No. There is no canceled_at operator on the subscriptions endpoint, so counting cancellations in a window means paging the list and filtering client-side. For anything at scale, record the date yourself from the subscription.canceled webhook. **What is scheduled_change on a Paddle subscription?** It is where a pending cancellation lives before it takes effect. A subscription cancelled with the period left to run stays active with a scheduled change of type cancel and an effective_at date, which makes it a leading indicator of churn. **Does past_due count as Paddle churn?** No. A payment is failing and dunning is retrying it. Counting past due as churn means counting the same customer again when it finally cancels, and the figure moves whenever you change your retry schedule. **Why does my Paddle churn series shift when I change dunning settings?** Because involuntary churn only becomes a cancellation after your retry schedule gives up. A longer schedule pushes every involuntary cancellation later, with no change in customer behaviour at all. --- # How to track Polar subscription cancellations URL: https://notchmetrics.com/blog/track-polar-subscription-cancellations Published: 11 October 2026 Author: Notch Metrics Tags: polar, churn, api ## When does a Polar subscription count as cancelled? When it ends, not when somebody clicks. A Polar subscription can be revoked immediately, or cancelled with the current period left to run. In the second case the customer keeps access and keeps being counted as active until the period they paid for expires. An annual plan cancelled in January ends the following January. Polar's own churn metric buckets by the end, and that is the behaviour you want. It lines up with active subscriptions: every drop in one is a bump in the other, which is what makes the two readable as a pair. ## The call ```bash curl -s 'https://api.polar.sh/v1/metrics/?start_date=2026-09-01&end_date=2026-10-01&interval=day&metrics=churned_subscriptions&timezone=Europe/London' \ -H 'Authorization: Bearer polar_oat_...' ``` One call, one series, no paging. Ask for `new_subscriptions` in the same request and you have both halves: ``` metrics=churned_subscriptions,new_subscriptions ``` That is the pair worth plotting. The difference is net movement, and net movement is what decides whether MRR goes up. ## Trials are not churn A trial that ends without converting never started paying. It belongs to the top of your funnel, not to churn, and counting it here makes a healthy flow of trials look like a leak. Polar excludes trials from MRR and from active subscriptions, so it excludes them here too. If your trial-to-paid rate is what you actually want to know, that is a different measurement against your trial starts. ## The subscription states Polar subscriptions move between several states and only one of them is churn. | State | Churn? | | ---------- | ----------------------------------------------- | | `active` | No | | `trialing` | No, and not counted as active either | | `past_due` | No. A payment is failing and dunning is running | | `canceled` | Not yet, if the period is still running | | `revoked` | Yes. Access has ended | `past_due` is the one worth watching separately. It is involuntary churn in progress, and unlike somebody who chose to leave it is the kind you can actually fix. By the time it becomes a cancellation, several days of retries have already happened and the figure in your churn series lags the card that failed. ## Counting is not a rate This gives you subscriptions lost. A churn rate needs a denominator, and there are at least three defensible ones: active subscriptions at the start of the window, active at the start plus new, or MRR rather than count. Pick one and write it down beside the code. A churn rate whose definition moved is worse than no churn rate, because the trend line still looks real. ## In the notch Notch Metrics reports [Polar churned subscriptions](/sources/polar/churned-subscribers) as a count, bucketed by when the subscription ended, with a sparkline and a comparison against the previous window. Put it beside [new subscriptions](/sources/polar/new-subscribers) and [MRR](/sources/polar/mrr) and the three say what is happening. It reports a count rather than a rate, deliberately: the denominator everybody means by "churn rate" differs by company, and guessing yours would produce a number you could not defend. [Everything Polar reports](/sources/polar), or [download Notch Metrics](/download). ## Questions **How do I get Polar churn from the API?** Call GET /v1/metrics/ with metrics=churned_subscriptions. It returns subscriptions that ended in each period, with no paging. Ask for new_subscriptions in the same call to get both halves. **Does a cancelled Polar subscription count as churn immediately?** No. A subscription cancelled with the period left to run stays active and keeps paying until it expires. Polar counts it when it actually ends, which is the bucket that lines up with active subscriptions. **Do lapsed Polar trials count as churn?** No. A trial that ends without converting never started paying, so it belongs to the top of the funnel rather than to churn. Polar excludes trials from MRR, active subscriptions and churn alike. **What does past_due mean on a Polar subscription?** A payment is failing and dunning is retrying it. It is involuntary churn in progress rather than churn, and it is the kind you can actually fix. By the time it becomes a cancellation, several days of retries have already happened. **Does Polar give a churn rate or a churn count?** The metrics endpoint gives a count. A rate needs a denominator, and active at the start of the window, active plus new, and MRR are all defensible choices that produce different numbers. --- # How to track subscriber growth with the YouTube API URL: https://notchmetrics.com/blog/track-subscriber-growth-with-the-youtube-api Published: 11 October 2026 Author: Notch Metrics Tags: youtube, subscribers, api ## Two numbers, two APIs The current total and the change over a window come from different places. **The total** is on the Data API's channel resource: ```bash curl -s 'https://www.googleapis.com/youtube/v3/channels?part=statistics&mine=true' \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` `statistics.subscriberCount`, as a string. **The change** is on the Analytics API: ```bash curl -s -G 'https://youtubeanalytics.googleapis.com/v2/reports' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ --data-urlencode 'ids=channel==MINE' \ --data-urlencode 'startDate=2026-10-01' \ --data-urlencode 'endDate=2026-10-07' \ --data-urlencode 'metrics=subscribersGained,subscribersLost' \ --data-urlencode 'dimensions=day' ``` ## The total is rounded This is the important one. Above a thousand subscribers YouTube rounds the count to three significant figures, publicly and in the Data API. 12,345 reads as 12,300. The rounding gets coarser as the channel grows, so a large channel can gain hundreds of subscribers a day and report the same number for several days before stepping. Consequences: **Do not build a growth series from the total.** Polling `subscriberCount` daily and differencing it produces a staircase, with long flat runs and sudden jumps that correspond to the rounding boundary rather than to anything that happened. **Do not compute a daily gain from it.** The difference between two rounded numbers has an error of up to the rounding increment, which on a large channel is larger than a normal day's gain. The exact figures are in Analytics. Use them. ## Gained is gross, not net `subscribersGained` counts people who subscribed. `subscribersLost` counts people who left. YouTube reports them separately and does not net them. A channel can gain two thousand and lose five hundred in the same week. A "new subscribers" figure that shows two thousand is correct and is not growth. ```js const net = gained - lost; ``` Net is the number that moves your total. Gross is the number that tells you whether your content is finding people, which is a different and also useful question. Report whichever you mean and label it. ## Where subscriptions come from Add the dimension and you get the surface: ``` dimensions=subscribedStatus,day ``` Or, more usefully: ``` dimensions=insightTrafficSourceType metrics=subscribersGained ``` That tells you whether your subscribers came from search, from suggested videos, from a channel page or from an external link. The answer usually changes what you would do next far more than the total does. ## The lag Subscriber metrics settle like everything else in Analytics, over a day or two, and days are Pacific Time. A spike that looks like it happened on Tuesday in London may be Monday in the data. ## A subscriber is not a viewer Worth stating because the total is the number everybody optimises for. Subscribing costs one click and implies nothing about watching. A channel's subscriber count and its returning-viewer count can move in opposite directions for a long time. If you want the number that predicts future views, it is [watch time](/sources/youtube/watch-time) and returning viewers, not this one. ## In the notch Notch Metrics reports [YouTube subscribers](/sources/youtube/subscribers) as the current total without a sparkline, because a chart of a rounded series draws steps that look like stalled growth. The change against the previous window comes from Analytics and is exact, so the arrow is right even where the total is rounded. [New subscribers](/sources/youtube/new-subscribers) is the gross gain in the window, and the page says gross rather than letting you assume net. [Everything YouTube reports](/sources/youtube), or [download Notch Metrics](/download). ## Questions **Why is the YouTube subscriber count rounded?** YouTube rounds counts above a thousand to three significant figures, publicly and in the Data API, so 12,345 reads as 12,300. The rounding gets coarser as a channel grows. **Can I build a subscriber growth chart by polling subscriberCount?** No. Differencing a rounded total produces a staircase, with flat runs and jumps that correspond to the rounding boundary rather than to anything that happened. Use subscribersGained and subscribersLost from the Analytics API instead. **Is subscribersGained net or gross?** Gross. YouTube reports subscribersGained and subscribersLost separately and does not net them, so a channel can gain two thousand and lose five hundred in the same week. Net is gained minus lost. **How do I find out where my YouTube subscribers came from?** Query subscribersGained with the insightTrafficSourceType dimension. It tells you whether they came from search, suggested videos, a channel page or an external link, which usually changes what to do next more than the total does. **Does a YouTube subscriber mean somebody watches?** Not necessarily. Subscribing costs one click and implies nothing about watching, and a channel's subscriber count and returning-viewer count can move in opposite directions for a long time. Watch time predicts future views better. --- # How to track trial to paid conversion in RevenueCat URL: https://notchmetrics.com/blog/track-trial-to-paid-conversion-in-revenuecat Published: 11 October 2026 Author: Notch Metrics Tags: revenuecat, trials, api ## Why is trial conversion hard to measure? Because the obvious calculation is wrong and it looks right. Take active trials, take active subscriptions, divide. You now have a number that moves, feels meaningful, and answers nothing. The two figures are snapshots of different populations at the same instant, and a trial started today has not had the chance to convert. Conversion is a cohort question. Of the people who started a trial in a given week, how many were paying when it ended? Answering it needs the start and the outcome tied together, which no single snapshot gives you. ## What active trials actually is A count of free trials running right now, including cancelled ones, until they convert to paid or expire. Read it as queue depth. Trials doubling means a cohort arrived that will decide, one way or the other, within whatever trial length the store is enforcing. The number falling means they decided, and it does not say which way. That makes it a genuinely useful leading indicator and a useless conversion rate. ## The pair that tells you something Put [active trials](/sources/revenuecat/trials) next to [active subscriptions](/sources/revenuecat/active-subscribers) and watch them together over a trial length. - Trials fall, subscriptions rise. Conversion. - Trials fall, subscriptions flat. They left. - Trials rise, subscriptions flat. The cohort has not decided yet. That is not a conversion rate, but it is directionally honest, and it is available from one endpoint with no extra work. ## Measuring it properly, with webhooks For a real conversion rate you need the cohort, and that means recording events as they happen. RevenueCat sends `INITIAL_PURCHASE` with a `period_type` that distinguishes a trial start, and then one of several outcomes later. ```js // Sketch. Store these and the ratio becomes a query rather than a guess. const OUTCOMES = { INITIAL_PURCHASE: "started", // period_type TRIAL RENEWAL: "converted", // first renewal after a trial is the conversion CANCELLATION: "cancelled", // intent to leave, trial still running EXPIRATION: "lapsed", // trial ended without paying }; ``` `CANCELLATION` during a trial is the signal people misread. It does not mean the trial ended. It means auto-renew was switched off, and the trial keeps running to its end date. Count it as intent, not as an outcome, or you will report people as lost while they are still using the app. The conversion rate is then: trials started in week N, divided into those whose first `RENEWAL` arrived within the trial length plus a grace margin. ## Two stores, two clocks A project shipping to both App Store and Play Store mixes trial lengths, because the trial is configured per store product and the two rarely match exactly. A single blended conversion rate across both is an average of two different experiments. Split by store before you draw conclusions about a trial length change. ## Billing grace periods blur the end A subscription that converts but whose first payment fails enters a grace period rather than expiring. Depending on where you draw the line, that user converted, or has not yet, or never will. Pick a rule, write it down, and apply it consistently. The rate matters far less than the rate being comparable to last month's. ## In the notch Notch Metrics reports [RevenueCat active trials](/sources/revenuecat/trials) as a live snapshot of how many are running, beside [active subscriptions](/sources/revenuecat/active-subscribers), [MRR](/sources/revenuecat/mrr) and [new customers](/sources/revenuecat/new-users). It deliberately does not show a conversion rate. A rate computed from two snapshots would be a number that moves convincingly and means nothing, and the real one needs a cohort nobody can derive from a single API call. [Everything RevenueCat reports](/sources/revenuecat), or [download Notch Metrics](/download). ## Questions **Can I calculate trial conversion from RevenueCat active trials and active subscriptions?** No. Those are snapshots of two different populations at the same instant, and a trial started today has not had the chance to convert. Dividing one by the other produces a number that moves convincingly and means nothing. **What does RevenueCat active trials count?** Free trials running right now, including ones that have been cancelled, until they convert to paid or expire. Read it as queue depth: a cohort that will decide within whatever trial length the store is enforcing. **Does a CANCELLATION webhook during a trial mean the trial ended?** No. It means auto-renew was switched off and the trial keeps running to its end date. Count it as intent rather than as an outcome, or you report people as lost while they are still using the app. **How do I measure RevenueCat trial conversion properly?** As a cohort, from webhooks. Record INITIAL_PURCHASE with period_type TRIAL as the start, then look for the first RENEWAL within the trial length plus a grace margin. EXPIRATION without a renewal is a lapse. **Should I split trial conversion by store?** Yes. Trial lengths are configured per store product and App Store and Play Store rarely match exactly, so a single blended rate is an average of two different experiments. --- # How to use the Plausible Stats API v2 URL: https://notchmetrics.com/blog/use-the-plausible-stats-api-v2 Published: 11 October 2026 Author: Notch Metrics Tags: plausible, analytics, api ## What is different about Stats API v2? v1 was a family of GET endpoints: one for aggregate, one for timeseries, one for breakdowns, each with its own parameters. v2 is a single POST endpoint that takes a query object. ```bash curl -s https://plausible.io/api/v2/query \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "site_id": "example.com", "metrics": ["visitors", "pageviews", "bounce_rate"], "date_range": ["2026-10-01", "2026-10-07"], "dimensions": ["time:day"] }' ``` One request, three metrics, bucketed daily. Under v1 that was three requests and three response shapes. ## The response is rows, not objects ```json { "results": [ { "metrics": [120, 340, 0.41], "dimensions": ["2026-10-01"] }, { "metrics": [98, 250, 0.38], "dimensions": ["2026-10-02"] } ] } ``` `metrics` is positional: it matches the order you asked for them in. There are no keys. That is efficient and it is a trap. Reorder your request and every reader downstream silently reads the wrong column. Build a lookup from your own request rather than indexing by a hardcoded number: ```js const index = Object.fromEntries(query.metrics.map((m, i) => [m, i])); const visitors = row.metrics[index.visitors]; ``` ## site_id is the domain Not a UUID, not a numeric id. `example.com`, exactly as it appears in your Plausible site list, without a scheme and without a trailing slash. A site id that does not match returns an error rather than empty data, which is at least a clear failure. ## Dimensions do the work `dimensions` is where v2 gets its range. | Dimension | What you get | | ------------------------------------- | ----------------- | | `time:day`, `time:hour`, `time:month` | A series | | `visit:source`, `visit:referrer` | Traffic sources | | `event:page` | Top pages | | `visit:country` | Geography | | `event:props:` | A custom property | Omit `dimensions` entirely and you get one row: the aggregate over the whole range. That is the equivalent of the old aggregate endpoint and it is the right call when you want a headline figure rather than a chart. ## date_range accepts shorthand Either an explicit pair of ISO dates, or a shorthand string: ```json "date_range": "7d" ``` `"day"`, `"7d"`, `"30d"`, `"month"`, `"6mo"`, `"12mo"` and `"all"` are available. The shorthand is resolved in the site's timezone, which saves you computing the boundaries yourself and getting them wrong near midnight. ## Realtime is still its own thing Current visitors is not a v2 query. It remains: ```bash curl -s 'https://plausible.io/api/v1/stats/realtime/visitors?site_id=example.com' \ -H 'Authorization: Bearer ' ``` A bare integer, no JSON wrapper. ## Rate limits and keys API keys are per user rather than per site, and one key reads every site you have access to. The limit is generous for dashboards and not for a tight polling loop. Cache anything you refresh more than once a minute. ## Or read it off the notch Notch Metrics queries the v2 endpoint and reports [unique visitors](/sources/plausible/visitors), [pageviews](/sources/plausible/pageviews), [sessions](/sources/plausible/sessions) and [bounce rate](/sources/plausible/bounce-rate) in the MacBook notch, bucketed in your Plausible site's own timezone. One connection, a key that never reaches your Mac. [Everything Plausible reports](/sources/plausible), or [download Notch Metrics](/download). ## Questions **What is the Plausible Stats API v2 endpoint?** A single POST to https://plausible.io/api/v2/query taking a JSON body with site_id, metrics, date_range and optional dimensions. v1 used separate GET endpoints for aggregate, timeseries and breakdown. **How do I read the Plausible v2 response?** Each result row has a metrics array that is positional, matching the order you requested metrics in, and a dimensions array. Build a lookup from your own request rather than indexing by a hardcoded number, or reordering the request silently breaks every reader. **What format is Plausible site_id?** The domain as it appears in your Plausible site list, such as example.com, with no scheme and no trailing slash. It is not a UUID or a numeric id. **Can I get a Plausible aggregate figure rather than a series?** Yes. Omit dimensions entirely and the response is a single row covering the whole date range. Adding time:day to dimensions turns it into a daily series. **Is realtime visitors part of the Plausible v2 API?** No. Current visitors remains a v1 GET at /api/v1/stats/realtime/visitors, and it returns a bare integer rather than a JSON object. --- # Why Search Console data lags two to three days URL: https://notchmetrics.com/blog/why-search-console-data-lags Published: 11 October 2026 Author: Notch Metrics Tags: google-search-console, seo, data ## Why is today's Search Console data always low? Because it is not finished. Search Console reports settle over roughly two to three days, and the newest buckets are partial until they do. This produces the single most-asked Search Console question, usually phrased as "why did my traffic drop yesterday". It did not. The data has not arrived. ## What is actually happening Search Console is not a live counter. Clicks and impressions are logged, aggregated, filtered for spam and automated traffic, processed for privacy, and only then published. Each of those stages takes time, and they do not all finish together. A day's figure therefore grows in steps rather than smoothly, and different metrics settle at slightly different rates: impressions tend to arrive before the clicks that belong with them, which means a CTR for yesterday can be lower than the truth even after the clicks catch up. ## It is not uniform The lag is roughly two to three days and it is not a guarantee. Large sites, weekends, and periods when Google is doing something else can push it out. The Performance report itself shows a note when the most recent days are still incomplete, and the API simply returns the partial figure with nothing to indicate it. That last part is the trap for anybody building on the API: there is no flag. A partial day looks exactly like a quiet day. ## How to report around it **End your window three days back** for anything you compare period over period. A week-over-week comparison where one week includes unsettled days and the other does not is comparing a partial number against a complete one, and it will show a decline every single time. **Label the recent points** if you must include them. A dotted line, a lighter colour, anything that says the last two or three days are not final. **Never alert on the newest day.** An alert on yesterday's clicks will fire every day, which trains everyone to ignore it, which is worse than having no alert. ## What it means for a daily chart Every correct Search Console daily chart droops at the right-hand end on every refresh, forever. That droop is the data arriving, not traffic leaving. If a chart you are looking at does not droop, one of two things is true: either the window already ends several days back, or the tool is filling in the gap with something it made up. ## Separate it from a real drop Three checks tell you whether a fall is real. **Wait three days.** The cheapest and most reliable test, and the one everybody skips. **Compare the same day of week.** Search traffic is weekly. Monday against Sunday is not a comparison. **Look at impressions and clicks together.** Impressions falling with position steady is Google showing you less. Clicks falling with impressions and position steady is a snippet or a results-page change. Both falling with position worsening is a ranking change. Only the last one is what people usually mean by a drop. ## The other reason totals look low Separate from the lag, and worth not confusing with it: Google withholds queries issued by very few people. Those clicks are in your totals and in no query breakdown, at any row limit. So a query table that sums to less than the headline is privacy filtering, not lag, and waiting will not fix it. ## In the notch Notch Metrics reports [Search Console clicks](/sources/google-search-console/clicks), [impressions](/sources/google-search-console/impressions), [CTR](/sources/google-search-console/ctr) and [average position](/sources/google-search-console/average-position) in the MacBook notch, and every one of those pages says the data runs two to three days behind. A number whose limits are hidden is a number somebody acts on wrongly, and a drooping chart with no explanation has caused more panic than most real traffic drops. [Everything Search Console reports](/sources/google-search-console), or [download Notch Metrics](/download). ## Questions **How far behind is Google Search Console data?** Roughly two to three days. Clicks and impressions are logged, aggregated, filtered for spam and processed for privacy before publication, and those stages do not all finish together. **Why did my Search Console traffic drop yesterday?** Usually it did not. The newest days are partial until the data settles over two to three days. Wait three days before treating a fall as real, and compare the same day of week rather than consecutive days. **Does the Search Console API tell me when data is incomplete?** No. The Performance report in the interface shows a note, but the API returns the partial figure with no flag. A partial day looks exactly like a quiet day, which is the trap for anybody building on it. **How do I compare Search Console periods fairly?** End both windows at least three days back. A week-over-week comparison where one week includes unsettled days and the other does not compares a partial number against a complete one, and will show a decline every time. **Why does my Search Console query table add up to less than the total?** That is privacy filtering, not lag. Google withholds queries issued by very few people, and those clicks appear in the totals and in no query breakdown at any row limit. Waiting does not fix it. --- # YouTube views and revenue on your Mac URL: https://notchmetrics.com/blog/youtube-views-and-revenue-on-your-mac Published: 11 October 2026 Author: Notch Metrics Tags: youtube, creator, mac YouTube Studio is a good analytics product and a heavy page. Checking one number costs you a load, a date picker and a scroll. ## How do you watch YouTube without Studio? Notch Metrics puts the figures in the MacBook notch. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back to the shape it had. ## What YouTube reports Ten numbers, the richest connector in the product. | Number | What it is | | --------------------------------------------------------------- | ------------------------------------- | | [Views](/sources/youtube/views) | Validated views in the window | | [Watch time](/sources/youtube/watch-time) | Total minutes, shown in hours | | [Average view duration](/sources/youtube/average-view-duration) | Watch time over views | | [Subscribers](/sources/youtube/subscribers) | The channel total, rounded by YouTube | | [New subscribers](/sources/youtube/new-subscribers) | Gained in the window, gross | | [Likes](/sources/youtube/likes) | Likes added in the window | | [Comments](/sources/youtube/comments) | Comments added, replies included | | [Estimated revenue](/sources/youtube/revenue) | Everything the channel earned | | [Ad revenue](/sources/youtube/ad-revenue) | The advertising part alone | | [RPM](/sources/youtube/rpm) | Revenue per thousand views | ## Three things worth knowing up front **Days are Pacific Time.** YouTube Analytics buckets everything in America/Los_Angeles regardless of where you are. A view at nine in the evening in London lands on the previous Pacific day, and a figure that disagrees with your sense of when a spike happened by a day is usually this. **Subscribers is rounded.** Above a certain size YouTube rounds the public and Analytics totals, so 12,345 reads as 12,300. The widget shows the total without a sparkline, because a sparkline of a rounded series draws steps and reads as though growth stopped and restarted. The change against the previous window comes from Analytics and is exact, so the arrow is right even where the total is rounded. [Why that is](/sources/youtube/subscribers). **Revenue is estimated and it settles.** YouTube adjusts for invalid traffic and advertiser disputes after the fact, so the last few days of any window move and a month finalises well into the following one. ## Revenue, split Estimated revenue is everything: advertising, channel memberships, Super Chat, Super Thanks and Premium watch-time revenue. [Ad revenue](/sources/youtube/ad-revenue) is the advertising part alone. Placing both is the useful move. A channel whose total is flat while ad revenue falls is being carried by memberships, which is a different business from the one the total implies. ## Connecting OAuth, with read-only YouTube scope and the monetary analytics scope for the revenue figures. After approving you pick which channel, because an account can manage several. [What the YouTube connector asks for](/sources/youtube). Nothing is typed in, and the token is encrypted on the server rather than stored on your Mac. ## Alongside everything else Search from [Search Console](/sources/google-search-console), traffic from [Plausible](/sources/plausible), revenue from [Stripe](/sources/stripe). [Every source](/sources). Apple silicon, macOS 14 and later. Seven days with no card, then $9 a month or $49 a year. [Download Notch Metrics](/download). ## Questions **How do I see YouTube views on my Mac?** Install Notch Metrics, connect YouTube over OAuth, pick the channel, and place the views widget in the MacBook notch. Hover the camera housing and the figure is there. **Which timezone does YouTube Analytics use?** America/Los_Angeles, regardless of where you are. A view at nine in the evening in London lands on the previous Pacific day, which is usually why a figure disagrees with your sense of when a spike happened. **Why is my YouTube subscriber count rounded?** Because YouTube rounds public and Analytics subscriber totals above a certain channel size, so 12,345 reads as 12,300. Notch Metrics shows the total without a sparkline for that reason, but the change against the previous window comes from Analytics and is exact. **What is the difference between YouTube estimated revenue and ad revenue?** Estimated revenue includes advertising, channel memberships, Super Chat, Super Thanks and Premium watch-time revenue. Ad revenue is the advertising part alone. A flat total with falling ad revenue means memberships are carrying the channel. **Is YouTube revenue in Notch Metrics final?** No, it is estimated. YouTube adjusts for invalid traffic and advertiser disputes after the fact, so the last few days of any window move and a month finalises well into the following one. --- # Writing a custom metric with Claude and MCP URL: https://notchmetrics.com/blog/writing-a-custom-metric Published: 8 October 2026 Author: Notch Metrics Tags: guide, custom-metrics, mcp Notch Metrics ships connectors for the providers most people need: Stripe, Polar, Paddle, Dodo Payments, PostHog, GitHub. Those are server-side, cached, and need no code from you. If your number is in one of them, connect it and stop reading. This post is about the other kind of number. Rows in your own Postgres. A count from an internal admin endpoint. Visitors from a self-hosted Plausible. Open pull requests across four repos. A custom metric is one JavaScript module that returns that number, and it runs on your machine. ## The module The entire contract is one exported function. ```js export async function fetchMetric(ctx) { const response = await ctx.http("https://api.github.com/repos/you/your-repo", { headers: { authorization: `Bearer ${ctx.secrets.GITHUB_TOKEN}` }, }); if (!response.ok) throw new Error(`github returned ${response.status}`); const repo = await response.json(); return { value: repo.stargazers_count, unit: "count" }; } ``` Three things in there are worth pointing at. **`ctx.http` is the only network access.** There is no `fetch` in scope. The helper requires HTTPS, refuses private and loopback addresses, caps responses at 4 MB, and, the important one, only calls hostnames your manifest declared. If a call is blocked, the test run tells you which host it tried. **`value` must be a number.** It is validated after your function returns, so a string fails immediately with a message naming the field, rather than rendering as `NaN` in your notch three days later when you have forgotten the metric exists. **Money is integer minor units plus an ISO 4217 code.** Never a float. `{ value: 123456, unit: "money", currency: "USD" }` is $1,234.56. This is the same convention the built-in connectors use, which is what lets a notch showing Stripe and your own revenue number add them up correctly. ## Never put the key in the file The natural thing to do with an API token is paste it into the source. Do not, and we try hard to make it impossible: publishing a metric whose source contains anything key-shaped is refused outright. Declare it in the manifest instead: ```json "secrets": [{ "key": "GITHUB_TOKEN", "label": "GitHub token", "required": true }] ``` The value then goes into the macOS Keychain and reaches the runner over stdin and not on argv, which any process can read through `ps`, nor in the environment, which is inherited by anything the plugin spawns. ## allowedHosts ```json "allowedHosts": ["api.github.com", "*.mycompany.com"] ``` Exact hostnames, or one leading `*.` matching exactly one more label. A bare `*` is refused. The reasoning is short: a metric that may call anywhere can post every other secret it was handed to a pastebin, and "it needs the whole internet" is never true of a single number. This is a containment boundary, not a sandbox, and that is worth saying plainly. It stops exfiltration and accidental scope creep. It is not a hypervisor. ## Let Claude write it The module is small enough to hand-write, and most of the work is reading someone's API docs, which is exactly the work worth handing off. We publish an MCP server for it: ```bash claude mcp add notch-metrics -- npx -y @notchmetrics/mcp ``` Then describe the number you want. The useful shape of the conversation is: 1. **`describe_runtime`**: the context shape, the return shape, the policy. One call, and it is the difference between a metric that works first time and three rounds of guessing. 2. **`list_providers`**: if a built-in connector already has the number, use that instead. No code, and it is cached server-side. 3. **`test_metric`** with an inline manifest and source. Nothing is written to disk while iterating, so you can be wrong cheaply. 4. **`create_metric`**: stages it, test-runs it, and publishes only if it succeeded. 5. **`set_metric_secret`** for each declared secret. One thing to remember afterwards: a new metric does not run until you enable it in **Settings → Custom Apps**. That is intentional. A tool that can write an executable module to your disk should not also be able to start it running on a schedule without you looking at it first. ## When it breaks The failures cluster into three kinds, and each one reports itself by name. - **Policy**: a blocked host, a non-HTTPS URL, a private address. The test run lists the call with `blocked: true` and the host it wanted. Add that host; do not guess at a wildcard. - **Timeout**: the runner is cancelled through `ctx.signal`. If an upstream API is genuinely slow, cache inside the metric with `ctx.cache` rather than widening the window. - **Bad result**: the return value failed validation. The message names the field. Almost always a string where a number belongs, or money as a float. Start with [the MCP server](https://www.npmjs.com/package/@notchmetrics/mcp) and one number you check too often. ## Questions **What is a Notch Metrics custom metric?** A custom metric is one JavaScript module that exports a fetchMetric function returning a number. It lives in Application Support on your own Mac and runs in a runner built into the app, so no Node install is needed and nothing is uploaded. **Can Claude write a Notch Metrics custom metric for me?** Yes. Add the Notch Metrics MCP server with `claude mcp add notch-metrics -- npx -y @notchmetrics/mcp`, then describe the number you want. The agent reads the runtime contract, writes the module, test-runs it, and publishes only if it returns a valid result. **Where are custom metric secrets stored?** In the macOS Keychain. They are declared in the manifest and handed to the runner on stdin, never on argv where any process could read them through ps, and never in the environment. A metric with a key written into its source is refused. **What can a custom metric reach over the network?** Only the hostnames its manifest declares, over HTTPS, with private and loopback addresses blocked and responses capped at 4 MB. A bare wildcard in allowedHosts is refused. This is containment, not a sandbox: the metric still runs as you. **Why is my custom metric failing with a policy error?** A policy error names a blocked host, a non-HTTPS URL or a private address. Add the host to allowedHosts in the manifest, switch the URL to HTTPS, and re-run test_metric. The message names the exact host or address that was refused. --- # How we encrypt your API keys URL: https://notchmetrics.com/blog/how-we-encrypt-your-api-keys Published: 1 October 2026 Author: Notch Metrics Tags: security, engineering To show you your MRR we have to hold a key that can read your Stripe account. There is no version of this product that avoids that. So the only honest thing to do is be specific about what we hold, how it is wrapped, and which attacks the design actually stops. ## How is each credential encrypted? Credentials are never encrypted with a single site-wide key. Each connection, meaning one row for one provider belonging to one user, gets its own data key. That key encrypts the credential; a key-encryption key held as a platform secret, entirely outside the database, encrypts the data key. The database stores the ciphertext and the wrapped key, and nothing that can unwrap it. The first consequence is the usual one: a leaked database dump is inert. There is no key material in it. Someone who walks away with every row has walked away with a pile of AES ciphertext. The second consequence is blast radius. Rotating one connection's credential touches one data key. There is no operation in the system that decrypts every credential at once, because there is no key that could. ## What is AAD in AES-GCM? AES-GCM lets you bind data to a ciphertext without encrypting it. The _additional authenticated data_ is not stored in the ciphertext. It is fed into the authentication tag, so decryption fails unless you supply the exact same bytes you encrypted with. We bind four things in: the user id, the integration id, the provider, and the key version. That turns a confidentiality control into an integrity control, and it closes a hole encryption alone leaves wide open. Consider the attack: someone with write access to the database, through a SQL injection, a compromised admin tool or a bug in a migration, copies the ciphertext blob out of your `stripe` row and into a row belonging to their own account. Nothing was decrypted. Nothing was cracked. They simply moved bytes. With a single global key and no AAD, that works. Their next metrics request decrypts your Stripe key perfectly and renders your revenue in their notch. With the user id in the AAD, the tag check fails. The row does not decrypt into the wrong account because the account it belongs to is part of what was authenticated. Same for the provider: a credential cannot be replayed against a different API. Same for the integration: it cannot be moved between two of your own connections. This is why our contributor notes carry the line **never remove the AAD**. It reads like boilerplate. It is the whole control. ## Key versions, and the secret you must not delete The key version goes in the AAD too, which gives us rotation: new rows are written under `v2` while `v1` rows keep decrypting under the key they were written with. Nothing has to be re-encrypted in a single migration, and a half-finished rotation is a consistent state rather than an outage. It also creates the sharpest operational edge in the system. Deleting a key-encryption secret while any row still names that version makes those rows permanently unreadable. Not "restore from backup" unreadable. There is no backup of a key-encryption key, by design, because a backup of it is a second copy of the thing whose scarcity is the point. Those users would have to reconnect every service by hand. So the rule is absolute: a `KEK` version is retired only after the last row referencing it has been re-wrapped. ## What we do not do - **No plaintext in logs.** Errors are scrubbed for credentials and secrets before they are written. A connector that fails with a 401 logs the 401, not what it sent. - **No credential on your Mac.** The app receives numbers. The key lives on the server and the service you connected is called from there. - **No 403 on someone else's resource.** Ask the API for a connection that is not yours and you get a 404. A 403 would confirm it exists, which is a disclosure in itself. Enumeration finds the gaps between "not found" and "not allowed". - **No cross-tenant decrypt path.** Not "we check the user id in the handler". The cryptography checks it. ## Being honest about the edges Custom metrics are a _containment_ boundary, not a sandbox. The runner on your Mac has no ambient `fetch`, refuses anything but HTTPS, blocks private and loopback addresses, caps response sizes, and will only call hostnames the metric declared up front. That stops a plugin from quietly exfiltrating the secrets it was handed, and it is why a bare `*` in `allowedHosts` is refused. It is not a hypervisor, and we would rather say so than imply a guarantee we cannot make. Questions, or something here that looks wrong? [Write to us](mailto:support@notchmetrics.com). Our [privacy policy](/privacy) covers what we keep and for how long. ## Questions **How does Notch Metrics encrypt my provider API keys?** Each connection gets its own data key, which encrypts the credential. A key-encryption key held outside the database, as a platform secret, encrypts that data key. The database stores ciphertext and a wrapped key, and nothing that can unwrap it. **What is AAD in AES-GCM, and why does it matter for stored credentials?** Additional authenticated data is bound into the authentication tag rather than encrypted. Decryption fails unless the exact same bytes are supplied. Notch Metrics binds in your user id, the integration, the provider and the key version, so a ciphertext copied into another account fails the tag check instead of decrypting into it. **Can a leaked database expose my Stripe key?** No. A database dump contains AES ciphertext and wrapped data keys, with no key material to unwrap them. The key-encryption key is a platform secret held entirely outside the database. **Why does the Notch Metrics API return 404 instead of 403?** Because a 403 confirms the resource exists. Asking the API for a connection belonging to another user returns 404, so enumeration cannot find the gap between "not found" and "not allowed". **Are Notch Metrics custom metrics sandboxed?** No. A custom metric is contained, not sandboxed. The runner has no ambient fetch, refuses anything but HTTPS, blocks private and loopback addresses, caps response sizes, and calls only the hostnames the metric declared. It still runs as you, with your permissions. --- # Introducing Notch Metrics URL: https://notchmetrics.com/blog/introducing-notch-metrics Published: 22 September 2026 Updated: 11 October 2026 Author: Notch Metrics Tags: product, launch Every founder keeps the same tab open somewhere. A Stripe dashboard, a Baremetrics chart, a spreadsheet pulling from three places at once. It gets checked twelve times a day, and every check costs a context switch, a tab, five seconds of loading, and then the slow drift from "what is MRR" into "let me just look at churn by cohort". The number was never the problem. Getting to it was. ## Why put metrics in the MacBook notch? A MacBook notch is a few hundred pixels of glass that every Mac app pretends is not there. It sits above the menu bar, dead centre, in the exact place your eyes already pass through on the way to anything else. It is too small to hold a dashboard. That is what makes it the right place to hold a number. Notch Metrics puts your widgets there. Collapsed, it is a thin strip: today's revenue, MRR, active subscribers, whatever you chose. Hover, and it expands to the full set. Move the cursor away and it folds back to the shape it had. There is no dock icon and nothing in the menu bar. The app has one surface, and that surface exists only while you are looking at it. ## Connect once, on the web Your API keys never touch your Mac. Connecting a service opens your browser through a short-lived link. You paste the key, or approve an OAuth prompt, and the credential goes into our API encrypted with a key unique to that one connection. The Mac app receives numbers. That split is deliberate. A desktop app holding a live Stripe secret key is a laptop you cannot afford to lose, and people lose laptops. Revenue comes from Stripe, Polar, Paddle, Dodo Payments, Lemon Squeezy and RevenueCat. Sales come from Shopify. Product analytics come from PostHog, Plausible, Amplitude and Google Analytics. Search comes from Google Search Console, video from YouTube, repository numbers from GitHub. Each provider reports in its own currency, and the conversion happens on the way out, so a notch showing three providers shows one total. [Every source, and every number each one reports](/sources). ## When a provider does not have your number Some numbers do not come from a SaaS product. Rows in a Postgres table, a count from an internal admin API, visitors from a self-hosted analytics box. Those become custom metrics: one JavaScript module on your own machine that returns a number. The code, the keys and the results stay local. The runner has no `fetch`, only a hardened HTTP helper bound to the hostnames you listed, and secrets live in the macOS Keychain. Write one by hand, or point Claude at our [MCP server](https://www.npmjs.com/package/@notchmetrics/mcp) and describe the number you want. A custom metric is contained, not sandboxed. It names the hosts it may reach and nothing else is reachable, but it runs as you, like a script you wrote. Read what your agent writes. ## Get it Notch Metrics runs on Apple silicon, macOS 14 and later. Seven days of everything, with no card. [Download it](/download), connect one service, and close the tab you have had open since 2023. ## Questions **What is Notch Metrics?** Notch Metrics is a macOS app that puts business numbers in the MacBook notch. Revenue, MRR, subscribers and more sit in the camera housing. Hover to expand them, move away and the notch looks untouched. It runs on Apple silicon, macOS 14 and later. **Which services does Notch Metrics connect to?** Fourteen connectors are live. Revenue comes from Stripe, Polar, Paddle, Dodo Payments, Lemon Squeezy and RevenueCat, sales from Shopify, analytics from PostHog, Plausible, Amplitude and Google Analytics, search from Google Search Console, video from YouTube, and repository numbers from GitHub. **Does Notch Metrics store my Stripe API key on my Mac?** No. You connect a service on the web, and the credential is encrypted with a key unique to that one connection. The Notch Metrics server calls Stripe and hands your Mac a normalised number. The Mac app never holds a provider key. **How much does Notch Metrics cost?** $9 a month, or $49 a year. Every connector is included on both plans, with no per-integration charge. The seven-day trial needs no card. **Can Notch Metrics show a number that has no connector?** Yes. Write a custom metric: one JavaScript module that returns a number, running on your own Mac with its secrets in the Keychain. A coding agent can write it for you over the Notch Metrics MCP server. --- # What are active users? URL: https://notchmetrics.com/library/active-users Published: 11 October 2026 Author: Notch Metrics Tags: active-users, analytics, glossary Kind: glossary **Active users is the number of distinct people who did something in a period.** The two words doing the work are "distinct" and "something", and both are decisions rather than facts. ## De-duplication, and the arithmetic everybody gets wrong Active users is de-duplicated across whatever period it covers. Somebody who used your product on three days in a week is **one** weekly active user and three daily active users. So: ``` WAU != sum(DAU for 7 days) MAU != sum(DAU for 30 days) ``` The sum is always larger, and it is inflated by exactly how much your users come back, which is the retention you were presumably measuring separately. This is why a daily chart's bars never add up to the weekly headline above them. That is correct. Any tool whose bars do add up is showing you daily actives seven times rather than weekly actives once. If you want both, that is two queries. ## DAU over MAU is the stickiness ratio The standard use of the pair. A product with 3,000 DAU and 10,000 MAU has a ratio of 0.3, meaning the average user shows up about nine days a month. Rough benchmarks worth knowing and not trusting: a messaging product lives above 0.5, a business tool sits around 0.2 to 0.3, and something you open once a month is at 0.05 and is not broken, it is just that kind of product. The ratio is useful because it is dimensionless. It compares you to yourself across growth, which raw counts do not. ## "Active" is whatever you decided This is the part no benchmark can fix. **Opened the app** is the loosest definition and the one most tools default to. It counts somebody who launched your product, looked at it, and did nothing. **Performed a meaningful action** is the honest one and it is work. You have to define what a meaningful action is, and that definition is the product strategy. **Was reachable** is the accidental one. If your app sends a heartbeat event on a timer, everybody with it installed is active forever, and your DAU is a measure of installs. A team that quietly moves from the second to the first gets a flattering chart and loses the ability to compare against last quarter. ## Devices or people A second decision. Most analytics tools distinguish a device identifier from a resolved person after identity merging. Count devices and the same human on a phone and a laptop is two active users. Count people and they are one. Neither is wrong, but a number that silently changed from one to the other, usually during an authentication change, is a step in the chart that was never behaviour. ## Active users in Notch Metrics Four connectors report it, each using the provider's own de-duplication: [PostHog](/sources/posthog/active-users), [Amplitude](/sources/amplitude/active-users), [Google Analytics](/sources/google-analytics/active-users) and [RevenueCat](/sources/revenuecat/active-users). Each page says what that provider counts. The RevenueCat one is worth a special look: its window is fixed at 28 days by RevenueCat and ignores whatever range the widget is set to, which the page states rather than letting a control appear to work when it does not. [How to query active users from the PostHog API](/blog/query-active-users-from-the-posthog-api) ## Questions **What is the difference between DAU, WAU and MAU?** The period each de-duplicates over: one day, seven days and thirty days. They are not multiples of each other, because somebody active on three days in a week is one weekly active user and three daily ones. **Why is my weekly active user count lower than the sum of daily counts?** Because active users is de-duplicated across the whole period. The sum of seven daily counts is inflated by exactly how much your users come back, which is the retention you were measuring separately. **What is a good DAU over MAU ratio?** It depends entirely on the product. A messaging app lives above 0.5, a business tool sits around 0.2 to 0.3, and something people genuinely open once a month is at 0.05 and is not broken. The ratio is useful mainly for comparing you against yourself over time. **What counts as active?** Whatever you decided. Opening the app is the loosest definition and the default in most tools. Performing a meaningful action is the honest one and requires defining what that is. If your app sends a heartbeat on a timer, everybody with it installed is active forever. **Do active users count devices or people?** Depends on the tool and the setting. The same human on a phone and a laptop is two users if you count device identifiers and one if you count a resolved person after identity merging. A number that silently switches between the two produces a step in the chart that was never behaviour. --- # What is ARR? URL: https://notchmetrics.com/library/arr Published: 11 October 2026 Author: Notch Metrics Tags: arr, glossary, saas Kind: glossary **Annual recurring revenue is monthly recurring revenue multiplied by twelve.** That is the entire definition, and resisting the urge to make it more interesting is most of the discipline. ## Why bother with it at all ARR and [MRR](/library/mrr) carry exactly the same information, so the choice between them is about audience rather than accuracy. MRR is the operating number. It moves visibly week to week, which makes it the one to watch while you are building. ARR is the conversational number. Investors, acquirers and benchmarks all speak in it, and "we are at $480k ARR" lands differently from "we are at $40k MRR" despite being identical. Use whichever your reader thinks in. Never put both on the same chart. ## The three ways ARR gets inflated ARR is easy to overstate without saying anything untrue, which is why it is worth knowing the moves. **Annualising a good month.** Taking December's revenue, multiplying by twelve, and calling it ARR. December included Black Friday one-off sales, which are not recurring, and the figure is a forecast rather than a run rate. **Counting one-off revenue.** Setup fees, consulting, lifetime deals and one-time purchases are real money and they are not recurring. An ARR that includes them is answering a different question. **Counting bookings as revenue.** A signed annual contract starting next quarter is a booking. Adding it to ARR today reports revenue you have not begun to earn. All three produce a number that is bigger and less useful. The test is simple: if everything stopped changing today, would this amount arrive over the next twelve months? If not, it is not ARR. ## ARR and cash are different things A business at $120k ARR selling annual plans may have collected most of that already. The same business selling monthly has collected a twelfth of it. Identical ARR, completely different runway. ARR describes the book. It says nothing about when the money arrives, which is the thing that decides whether you make payroll. ## Why Notch Metrics reports MRR Every revenue connector reports [MRR](/library/mrr) rather than ARR: [Stripe](/sources/stripe/mrr), [Polar](/sources/polar/mrr), [Paddle](/sources/paddle/mrr), [Dodo Payments](/sources/dodopayments/mrr), [Lemon Squeezy](/sources/lemonsqueezy/mrr) and [RevenueCat](/sources/revenuecat/mrr). The reason is that MRR moves at a speed you can see. In the notch, a figure that changes visibly during a good week is worth more than one that needs a calculator to feel different. Multiply by twelve when somebody asks. ## Questions **What does ARR stand for?** Annual recurring revenue. It is monthly recurring revenue multiplied by twelve, and nothing else. The two carry identical information, so the choice between them is about audience rather than accuracy. **Is ARR just MRR times 12?** Yes. Anything smoothed, forecast or annualised differently is not ARR, it is a projection wearing a metric's name. **Can I include one-off sales in ARR?** No. Setup fees, consulting, lifetime deals and one-time purchases are real money and they are not recurring. The test is whether the amount would arrive over the next twelve months if everything stopped changing today. **Is ARR the same as cash in the bank?** No. A business at $120k ARR selling annual plans may have collected most of it already; the same business selling monthly has collected a twelfth. Identical ARR, completely different runway. **Should I report ARR or MRR?** MRR while you are building, because it moves visibly week to week. ARR when talking to investors, acquirers or benchmarks, because that is the unit they think in. Never put both on the same chart. --- # What is average order value? URL: https://notchmetrics.com/library/average-order-value Published: 11 October 2026 Author: Notch Metrics Tags: aov, glossary, ecommerce Kind: glossary **Average order value is total sales divided by the number of orders over the same period.** Two numbers, one division, and a surprising amount of ways to get it wrong. ## The calculation most tools get wrong There are two ways to compute AOV over a week and only one of them is right. **Weighted.** Sum the week's sales, sum the week's orders, divide. **Unweighted.** Compute each day's AOV, then average the seven values. The second treats a Sunday with two orders as equal in weight to a Black Friday with two thousand. One quiet day with a single expensive order can move it by a third. ``` Right: total_sales / total_orders Wrong: mean(daily_sales / daily_orders) ``` This is also why a correctly drawn AOV chart looks inconsistent: each point is that day's own ratio, and the headline is the period's ratio. They are not supposed to match. A tool whose headline equals the mean of its points is doing the wrong calculation. ## Use the same filter on both sides The easiest bug to write here. Filter the sales sum to exclude refunded orders, forget to filter the order count, and AOV quietly drops. Whichever rule you pick, apply it to numerator and denominator: - **Include refunded orders in both.** AOV is the average value of orders placed. - **Exclude from both.** AOV is the average value of orders kept. Both are defensible. Only mixing them is not. ## What moves AOV, and what to do about each **Price increases** move it evenly across every order. Expected, and the easiest to read. **Bundles and upsells** move it only for the orders that take them, so the mean rises while the median order does not change at all. If AOV is up and nothing feels different, check the distribution before celebrating. **One wholesale order** moves it for a single day and then it falls back. If a single order can visibly move your AOV, the mean is the wrong statistic and you want the median. ## AOV is a ratio, so read it with volume AOV rising while orders fall is a smaller, richer customer base. That might be the strategy or it might be a problem, and the ratio alone cannot say which. Always place sales and orders beside it. Three numbers explain each other; one explains nothing. ## AOV in Notch Metrics [Shopify average order value](/sources/shopify/average-order-value) is reported as sales divided by orders over the whole window, with each sparkline point being that day's own ratio. The page says so, because the apparent inconsistency is a correct calculation and not a bug. Place it beside [sales](/sources/shopify/revenue) and [orders](/sources/shopify/orders). [How to calculate AOV from Shopify orders](/blog/calculate-average-order-value-from-shopify-orders) ## Questions **How do you calculate average order value?** Total sales divided by the total number of orders over the same period. Do not average the daily ratios: that weights a quiet Sunday equally with Black Friday, and a single expensive order can move it by a third. **Why does my AOV headline not match my AOV chart?** Because each chart point is that day's own ratio and the headline is the period's total sales divided by its total orders. They are not supposed to match, and a tool whose headline equals the mean of its points is calculating it wrongly. **Should refunded orders count in AOV?** Either, as long as both sides match. Include them in sales and in the order count and AOV is the average value of orders placed. Exclude from both and it is the average value of orders kept. Filtering one and not the other is the common bug. **Is average order value the same as average revenue per customer?** No. AOV divides by orders; revenue per customer divides by customers. A customer who orders four times contributes four orders and one customer, so the two diverge as repeat purchasing rises. **What should I use instead of AOV if one order can move it?** The median order value. If a single wholesale order visibly moves your mean, the mean is the wrong statistic for that business. --- # Baremetrics alternatives URL: https://notchmetrics.com/library/baremetrics-alternatives Published: 11 October 2026 Author: Notch Metrics Tags: baremetrics, alternatives, saas Kind: alternatives Prices checked: 11 October 2026 ## TL;DR | Alternative | Price | Replaces | | ------------------ | ----------------------------------- | ----------------------------------------- | | **Notch Metrics** | **$9/mo or $49/yr** | The daily glance, across fourteen sources | | MRRDock | Free, MIT licensed | The MRR figure | | Stripe Dashboard | Free | The detail and the source of truth | | Baremetrics itself | Free to $180K ARR, then from $49/mo | Cohorts, LTV, forecasting | Prices checked on 11 October 2026, on each vendor's own pricing page. ## What does Baremetrics actually cost? Worth getting right before looking for an alternative, because the answer has changed and most third-party articles quote old figures. Baremetrics is free while it tracks up to $180K ARR. Above that, Launch is $49 a month to $360K ARR, Growth starts at $69 a month to $3.6M ARR, and Scale starts at $409 a month above that. Pricing is set by the annual recurring revenue it tracks, not by seats, so inviting your team costs nothing. Annual billing can take up to 35% off, and Launch is month to month only. Two add-ons sit on top of any plan at $129 a month each: Payment Recovery, which can instead be billed at 10% of recovered revenue, and Cancellation Insights. So the honest summary is that Baremetrics is free for a small business and scales with success. People look for alternatives for two different reasons, and they need different answers. ## Which problem are you solving? **"It costs too much at my revenue."** You want something cheaper that still does analysis. That is a narrow field, and the real answer is usually that you do not need the analysis as often as you are paying for it. **"I only ever look at one number."** You want a glance tool, and you have been paying for a cohort engine to answer a question a menu bar could answer. This is by far the more common case. --- ## 1. Notch Metrics If what you actually do with Baremetrics is open it, read MRR, and close it, this is the replacement. Notch Metrics puts your numbers in the MacBook notch, the strip of glass around the camera. Hover and they are there. Move away and the notch looks untouched. No dock icon, no menu bar item, no tab. It goes wider than revenue, which is the part Baremetrics does not do at all. [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Lemon Squeezy](/sources/lemonsqueezy), [Dodo Payments](/sources/dodopayments), [RevenueCat](/sources/revenuecat) and [Shopify](/sources/shopify) for money, then [Plausible](/sources/plausible), [PostHog](/sources/posthog) and [Google Analytics](/sources/google-analytics) for traffic, [Search Console](/sources/google-search-console) for search, and [GitHub](/sources/github) and [YouTube](/sources/youtube) besides. Fourteen connectors, all included, no per-integration charge. Your provider keys never reach your Mac. They are encrypted on the server with a key unique to each connection, and the app receives normalised numbers. What it does not do is cohort analysis, LTV or forecasting. That is deliberate: the notch is a few hundred pixels and a cohort chart does not fit in it. **Price.** $9 a month or $49 a year, every connector included. Seven-day trial, no card. **Requires.** Apple silicon, macOS 14 or later. [Download Notch Metrics](/download) · [Every source](/sources) ## 2. MRRDock Free and MIT licensed, and the cheapest honest answer if the only number you want is MRR. It reads Stripe, RevenueCat, Paddle, Lemon Squeezy, Polar, Dodo Payments and Gumroad, converts them into one currency, and shows the total in the menu bar. No account, no server, keys in the macOS Keychain, GET requests only. Its MRR rules are published, which is more than most: yearly divided by twelve, percentage coupons applied, usage-based prices flagged rather than guessed. Compared against Baremetrics it has no history analysis and no recovery tooling, and its keys sit on your laptop rather than a server. **Price.** Free, MIT licensed. **Requires.** macOS 14 or later. ## 3. The Stripe Dashboard Free, and it already has most of what people check Baremetrics for. Stripe Billing shows MRR, churn and subscriber counts natively, and Sigma will answer almost any question in SQL if you are willing to write it. What Stripe will not do is span providers. If your money arrives through Stripe and RevenueCat and a Shopify store, Stripe knows about a third of your business, which is the gap every tool on this page exists to fill. **Price.** Free. Sigma is billed separately on query volume. **Requires.** A browser. ## 4. Staying on Baremetrics Worth saying because it is often right. If you genuinely use cohort retention, LTV by segment, scenario planning or dunning recovery, nothing cheaper replaces them properly, and the free tier to $180K ARR means a lot of businesses are not paying anything yet. The pattern that actually works for most founders is both: Baremetrics open once a month for the questions that need history, and something glanceable for the question you ask twenty times a day. ## So which one **You open Baremetrics to read MRR and close it.** Notch Metrics, and get your traffic and search numbers in the same glance. **You want that for free and only care about MRR.** MRRDock. **You are under $180K ARR.** Baremetrics is free. Stay. **You run cohort and LTV analysis properly.** Keep Baremetrics and add a glance tool for the daily number. [See what Notch Metrics costs](/#pricing) ## Questions **How much does Baremetrics cost in 2026?** Free while it tracks up to $180K ARR. Launch is $49 a month to $360K ARR, Growth starts at $69 a month to $3.6M ARR, and Scale starts at $409 a month above that. Payment Recovery and Cancellation Insights are $129 a month each. Annual billing can take up to 35% off. Checked 11 October 2026. **Is there a free alternative to Baremetrics?** For the MRR figure, yes. MRRDock is free and MIT licensed and reads seven payment platforms into one menu bar total. The Stripe Dashboard is also free and shows MRR, churn and subscriber counts natively. Neither does cohort analysis, LTV or forecasting. **What does Baremetrics do that a menu bar app cannot?** Cohort retention, LTV by segment, scenario planning, failed-payment recovery and cancellation feedback. Those need stored history and a full screen. A glance tool answers what your MRR is right now and deliberately stops there. **Why is Baremetrics priced on my revenue?** Because it is priced by the annual recurring revenue it tracks rather than by seats, so you can invite your whole team without the price changing. As your tracked revenue crosses a band, the plan steps up. **Can Notch Metrics replace Baremetrics?** For the daily glance, yes, and across more than revenue: fourteen connectors covering payments, analytics, search, video and GitHub, for $9 a month or $49 a year. For cohort analysis, LTV and forecasting, no. Those are a different job and the notch is a few hundred pixels. --- # Best Stripe MRR trackers for Mac URL: https://notchmetrics.com/library/best-stripe-mrr-menu-bar-apps-for-mac Published: 11 October 2026 Author: Notch Metrics Tags: mrr, mac, comparison Kind: comparison Prices checked: 11 October 2026 ## TL;DR | | Tool | Price | Where it lives | Best for | | --- | ----------------- | ----------------------------------- | ----------------- | ------------------------------------------------ | | 1 | **Notch Metrics** | **$9/mo or $49/yr** | The MacBook notch | Several sources, and numbers beyond revenue | | 2 | MRRDock | Free, MIT licensed | Menu bar | One MRR total, open source, no cost | | 3 | Orbl | Free app, Premium $39 once | Wallpaper | Revenue in context with the rest of your desktop | | 4 | Baremetrics | Free to $180K ARR, then from $49/mo | Browser | Cohorts, LTV and churn analysis | | 5 | Stripe Dashboard | Free | Browser and phone | The authoritative detail | Prices checked on 11 October 2026, on each vendor's own pricing page. This is about where your revenue number lives when you are not looking for it. All five read the same Stripe account. What differs is how much of your day it costs to see the figure, and what else it can tell you. ## How do you choose an MRR tracker for Mac? Three questions settle it. **How many places does your money arrive?** One Stripe account is a solved problem and almost anything here works. Stripe plus RevenueCat plus a Shopify store is a different problem, and most tools answer only part of it. **Do you want revenue, or do you want your numbers?** An MRR tracker shows MRR. If you also want visitors, search clicks and open issues in the same glance, that is a wider tool. **Where does your Stripe key end up?** On your Mac, on somebody's server, or nowhere because the tool is read-only through a restricted key. All three are defensible. Knowing which one you chose is not optional. --- ## 1. Notch Metrics The MacBook notch is a few hundred pixels of glass above the menu bar that every other Mac app pretends is not there. Notch Metrics puts your numbers in it. Collapsed, a thin strip. Hover the camera housing and it expands over whatever you were doing. Move away and it folds back. There is no dock icon and nothing in the menu bar, which matters more than it sounds: the menu bar is already full, and macOS hides the newest items when it runs out of room. Fourteen connectors, every one included on both plans with no per-integration charge. [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments), [Lemon Squeezy](/sources/lemonsqueezy), [RevenueCat](/sources/revenuecat) and [Shopify](/sources/shopify) for money. [Plausible](/sources/plausible), [PostHog](/sources/posthog), [Amplitude](/sources/amplitude) and [Google Analytics](/sources/google-analytics) for traffic. [Search Console](/sources/google-search-console), [YouTube](/sources/youtube) and [GitHub](/sources/github) for everything else. Keys are the real difference. You connect on the web and the credential is encrypted with a key unique to that one connection, bound to your account and the provider. The server calls Stripe and hands your Mac a number, so the Mac app never holds a Stripe key at all. A lost laptop yields nothing. And when a number has no connector, a coding agent writes a [custom metric](/blog/writing-a-custom-metric) over MCP: one JavaScript module running on your own machine, secrets in the Keychain. **Price.** $9 a month, or $49 a year. Seven-day trial, no card. **Requires.** Apple silicon, macOS 14 or later. [Download Notch Metrics](/download) · [Every source](/sources) ## 2. MRRDock Free, MIT licensed, and genuinely good at the one thing it does. MRRDock aggregates MRR from Stripe, RevenueCat, Paddle, Lemon Squeezy, Polar, Dodo Payments and Gumroad into a single menu bar figure, converted into one currency. It is the only tool here whose source you can read before pasting a Stripe key into it, which is a real argument. Keys live in the macOS Keychain, it issues only GET requests, and there is no server or account. Its arithmetic is documented and conservative: yearly divided by twelve, percentage coupons applied, fixed-amount coupons deliberately not applied, usage-based prices flagged rather than guessed. That last choice is the right one and most tools do not make it. Where Notch Metrics differs is scope. MRRDock answers "what is my MRR right now" and stops there, by design. It will not tell you your Search Console clicks or your Shopify orders, and its keys sit on the laptop rather than on a server. **Price.** Free, MIT licensed, no paid tier. **Requires.** macOS 14 or later, Apple silicon or Intel. ## 3. Orbl Orbl puts revenue on your wallpaper rather than in the menu bar, alongside weather, focus and a GitHub contribution graph. It is a different idea and it suits people who want their numbers in context rather than as a figure in a strip. It is local: read-only keys in the macOS Keychain, called directly from your Mac, no server and no account. Premium unlocks the revenue integrations and is a one-time purchase, which is a fair point to make about a category full of subscriptions. The trade is visibility. A wallpaper is behind your windows, and most people with a Mac full of windows see their desktop rarely. The notch is in front of everything, always, which is the whole reason Notch Metrics uses it. **Price.** Free app. Premium $39 as a one-time purchase, listed as a launch price rising to $49. **Requires.** macOS. ## 4. Baremetrics The analytics tier rather than the glance tier. Baremetrics does cohorts, LTV, churn analysis, forecasting, failed-payment recovery and cancellation insights, which is a different job from showing you a number. It is free while it tracks up to $180K ARR for you, which makes it a genuinely reasonable starting point. Above that it is priced on the revenue it tracks: Launch is $49 a month to $360K ARR, Growth starts at $69 a month to $3.6M ARR, and Scale starts at $409 a month above that. Payment Recovery and Cancellation Insights are $129 a month each on top, or 10% of recovered revenue for the first. Your billing data lives on their servers, which is the price of the analysis. A tool that computes cohorts has to hold the history. Use it for the quarterly question. Use something on this page for the daily one. **Price.** Free to $180K ARR, then from $49/month, scaling with tracked revenue. Annual billing can take up to 35% off. **Requires.** A browser. ## 5. The Stripe Dashboard Free, authoritative, and the thing every other tool on this page reads from. The mobile app sends a notification on each payment, which is the closest Stripe itself gets to a glance. It is also a browser tab that takes several seconds to load, which is the entire reason this category exists. Keep it for the detail. **Price.** Free. **Requires.** A browser, or the Stripe iOS or Android app. ## Also in this category Three more Mac apps come up regularly and their current prices were not verified on the check date above, so none are quoted here. **MRRTracker** is a focused single-number menu bar app. **IndieBar** packs several revenue metrics into one menu bar dropdown. **CatBar** is purpose-built for RevenueCat and is the natural pick if your revenue is app subscriptions rather than Stripe. ## So which one **You ship on one platform and want to spend nothing.** MRRDock. It is free, it is open source, and for a single Stripe account it does exactly the job. **You want revenue and everything else in one glance.** Notch Metrics. The fourteen connectors are the point, and so is the fact that your Stripe key never reaches the laptop. **You want your desktop to feel calm and informative.** Orbl, if you actually see your wallpaper. **You need cohorts and forecasting for a board meeting.** Baremetrics, and keep a glance tool for the other twenty-nine days of the month. The honest version is that most people want two: something free or cheap that answers the daily question without a tab, and something heavier for the quarterly one. [See what Notch Metrics costs](/#pricing) ## Questions **What is the best way to track Stripe MRR on a Mac?** Put it somewhere you already look. Notch Metrics shows Stripe MRR in the MacBook notch for $9 a month or $49 a year, MRRDock shows it in the menu bar for free under an MIT licence, and Orbl puts it on your wallpaper with a $39 one-time Premium purchase. All three read the same Stripe account. **Is there a free Stripe MRR tracker for Mac?** Yes. MRRDock is free and MIT licensed, and reads Stripe, RevenueCat, Paddle, Lemon Squeezy, Polar, Dodo Payments and Gumroad into one menu bar figure. Orbl's base app is free, though its revenue integrations need the $39 Premium purchase. Baremetrics is free while it tracks up to $180K ARR. **Is it safe to give a Mac app my Stripe key?** Use a restricted key with read access to Charges and Subscriptions, which cannot move money even if it leaked. Beyond that, ask where the key ends up. MRRDock and Orbl keep it in your Mac's Keychain. Notch Metrics never sends a key to the Mac at all: it is encrypted on the server and the app receives only numbers. **How much does Baremetrics cost?** Baremetrics is free while it tracks up to $180K ARR. Launch is $49 a month to $360K ARR, Growth starts at $69 a month to $3.6M ARR, and Scale starts at $409 a month above that. Payment Recovery and Cancellation Insights are $129 a month each. Prices checked on 11 October 2026. **Can I track revenue from more than just Stripe?** Yes, with several of these. Notch Metrics connects Stripe, Polar, Paddle, Dodo Payments, Lemon Squeezy, RevenueCat and Shopify, plus analytics and search sources. MRRDock covers seven payment platforms plus any HTTPS endpoint of your own. **Menu bar, notch or wallpaper, which is better?** The menu bar is conventional and already crowded, and macOS hides the newest items when it runs out of room. A wallpaper is only visible when your windows are not. The notch sits in front of everything, always, which is why Notch Metrics uses it. --- # What is bounce rate? URL: https://notchmetrics.com/library/bounce-rate Published: 11 October 2026 Author: Notch Metrics Tags: bounce-rate, analytics, glossary Kind: glossary **Bounce rate is the share of sessions that contained exactly one page view.** The visitor arrived, looked, and left without loading a second page. ## The thing it cannot see Time. A session with one page view is a bounce whether the visitor left after two seconds or read for twenty minutes. That is not a limitation of any particular tool, it is what a page-view-based definition can know. Without a second page load or an engagement event, the browser never reports how long somebody stayed. So the metric conflates two opposite outcomes: somebody who bounced off a page that was wrong for them, and somebody who got exactly what they came for on the first page and had no reason to click. ## Which is why high is often fine Three cases where a high bounce rate is the page working. **A blog post.** Search brings somebody to one article, they read it, they leave. That is the article doing its job. **A documentation page.** They came for a specific answer, found it, went back to their editor. A low bounce rate here would mean people cannot find things. **A contact or pricing page as a landing page.** One page, one decision. The case where it is genuinely bad is a page whose whole job is to move somebody onwards. There a bounce is a failure, and the number is actionable. Segment before you judge. A site-wide bounce rate averages a blog that should bounce with a funnel that should not, and the average describes neither. ## GA4 changed the definition Worth knowing if you are comparing across a migration. Universal Analytics bounce rate was the single-page-view definition above. GA4 bounce rate is **one minus engagement rate**, where an engaged session lasted at least ten seconds, had two or more page views, or included a conversion. So a GA4 bounce is a session that did none of those. The two numbers are not comparable and should never share an axis across the migration date. GA4's is arguably the better definition, because the ten-second rule gives it a crude view of the time the old one could not see. ## Where the number moves without behaviour changing **A cookie banner that fires an event.** If a consent interaction registers as activity, bounces fall overnight with nobody reading more. **Anchor links.** A same-page anchor is not a page view, so a table of contents does not rescue a bounce rate. Correct behaviour, and it catches people who expected otherwise. **A single-page app with no route tracking.** If your router does not fire a page view on navigation, every session has exactly one and your bounce rate is near 100%. That is an instrumentation reading, not a traffic reading, and it is the first thing to check when the figure looks absurd. ## Read it with volume Bounce rate falling while visitors fall is not an improvement, it is a smaller and more committed audience. Falling while visitors rise is the genuinely good case. ## Bounce rate in Notch Metrics [Plausible bounce rate](/sources/plausible/bounce-rate) uses the single-page-view definition and is reported as a ratio, bucketed in your Plausible site's own timezone. Place it beside [unique visitors](/sources/plausible/visitors) and [pageviews](/sources/plausible/pageviews). [How to read bounce rate from Plausible](/blog/read-bounce-rate-from-plausible) ## Questions **What counts as a bounce?** In most analytics tools, a session with exactly one page view: the visitor arrived and left without loading a second page. Time on the page is not part of the definition, so a two-second visit and a twenty-minute read are both bounces. **Is a high bounce rate bad?** Not necessarily. On a blog post, a documentation page or a pricing page, somebody arriving, getting what they came for and leaving is the page working. It is genuinely bad only on a page whose job is to move somebody onwards. **Is GA4 bounce rate the same as Universal Analytics bounce rate?** No. GA4 bounce rate is one minus engagement rate, where an engaged session lasted ten seconds or more, had two or more page views, or included a conversion. Universal Analytics counted single-page-view sessions. The two should never share an axis. **Why is my bounce rate nearly 100%?** Usually a single-page app whose router does not fire a page view on navigation, so every session has exactly one. That is an instrumentation reading rather than a traffic reading. **Do anchor links reduce bounce rate?** No. A same-page anchor is not a page view, so a table of contents does not affect the figure. --- # What is churn rate? URL: https://notchmetrics.com/library/churn-rate Published: 11 October 2026 Author: Notch Metrics Tags: churn, glossary, saas Kind: glossary **Churn is what you lose.** Customers who cancel, or the revenue they took with them, over a period. Churn rate expresses that loss as a fraction of what you had. The count is easy. The rate is where everybody disagrees. ## The denominator is the argument There is no single correct churn rate, because there is no agreed denominator. | Denominator | What the rate then means | | ------------------------ | ------------------------------------------------------------------ | | Customers at the start | The classic. Ignores anybody who joined and left inside the period | | Start plus new | Counts fast churners. Always a lower number | | Average of start and end | Smoother, harder to explain | | MRR at the start | Revenue churn, not customer churn. A different metric | All four are defensible and all four give different numbers. Pick one, write it down beside the code, and never change it quietly. A churn rate whose definition moved is worse than no churn rate, because the trend line still looks real. ## Customer churn against revenue churn They answer different questions and can point in opposite directions. **Customer churn** counts people. Losing ten customers on your cheapest plan is a high customer churn month. **Revenue churn** counts money. Losing one enterprise account is a high revenue churn month and barely registers in customer churn. **Net revenue churn** subtracts expansion revenue from existing customers. If upgrades outweigh cancellations it goes negative, which is the shape every subscription business wants and most never reach. Reporting one and calling it "churn" without saying which is how two people in the same company end up arguing about a number they both read correctly. ## Voluntary and involuntary churn The distinction that is actually actionable. **Voluntary churn** is somebody deciding to leave. Fixing it means fixing the product, the price or the fit, which is slow. **Involuntary churn** is a card expiring, a bank declining, a payment retrying until the provider gives up. The customer never decided anything. For many businesses it is 20% to 40% of all churn, and it is mostly recoverable with better dunning. They also arrive at different speeds. Involuntary churn lags the failure that caused it by however long your retry schedule runs, so changing that schedule moves your churn series with no change in customer behaviour at all. ## When a cancellation counts A subscription cancelled with the period left to run is still paying. Count it on the day somebody clicked and churn spikes while revenue does not move for a year. Count it when the subscription actually ends and today's churn tells you nothing about today's cancellations. The second lines up with your active subscriber count, which is why it is the better default. ## Churn in Notch Metrics Three connectors report it as a count, bucketed by when the subscription ended: [Stripe](/sources/stripe/churned-subscribers), [Polar](/sources/polar/churned-subscribers) and [Paddle](/sources/paddle/churned-subscribers). A count rather than a rate, deliberately. The denominator everybody means by "churn rate" differs by company, and guessing which one you meant would produce a number you could not defend. [How to measure Stripe subscriber churn](/blog/measure-stripe-subscriber-churn) ## Questions **How do you calculate churn rate?** Divide customers lost in a period by a denominator, which is where the disagreement is. Customers at the start, customers at the start plus new, the average of start and end, and MRR at the start are all defensible and all give different numbers. **What is the difference between customer churn and revenue churn?** Customer churn counts people and revenue churn counts money, and they can point in opposite directions. Losing ten customers on your cheapest plan is high customer churn; losing one enterprise account is high revenue churn and barely registers in the other. **What is negative churn?** Net revenue churn below zero, which happens when expansion revenue from existing customers outweighs what cancellations took away. It is the shape every subscription business wants and most never reach. **What is involuntary churn?** Churn caused by a failed payment rather than a decision: an expired card, a declined bank, a retry schedule that gives up. For many businesses it is 20% to 40% of all churn and it is mostly recoverable with better dunning. **When should a cancellation count as churn?** When the subscription actually ends, not when somebody clicks cancel. A subscription cancelled with the period left to run keeps paying, so counting it on the click makes churn spike while revenue does not move for a year. --- # What is click-through rate? URL: https://notchmetrics.com/library/click-through-rate Published: 11 October 2026 Author: Notch Metrics Tags: ctr, seo, glossary Kind: glossary **Click-through rate is clicks divided by impressions.** How often the people who saw you chose you. ## It is a ratio over the period, not an average of days The arithmetic trap that applies to every ratio. ``` Right: total_clicks / total_impressions Wrong: mean(daily_clicks / daily_impressions) ``` The weighted version lets a Monday with ten thousand impressions count for more than a Sunday with ten, which is correct. The unweighted version treats them as equals, and one quiet day can move the figure visibly. This is also why a correct CTR chart and its headline do not match. Each point is that day's own ratio; the headline is the period's. That is not a bug and it is worth saying in any interface that shows both. ## Why a falling CTR is often good The least intuitive thing about this metric, and the reason it is the most misread number in search. CTR falls when a page starts ranking for broader terms. More impressions at worse positions means a smaller share convert, while the page earns **more clicks in total**. The percentage fell and the outcome improved. Three more things lower CTR with nothing going wrong: - **Google answering the query on the results page.** Outside your control. - **A rich result above you** taking the clicks that would have been yours. - **Ranking for the first time on a new page**, which starts at zero and climbs. ## When a falling CTR is genuinely bad When impressions and position are both steady and clicks fall. That isolates the one variable you control: what your result says. A page at position two with a low CTR is a snippet problem. The title or the meta description is not answering the query somebody typed, and that is directly fixable. ## Never read CTR alone A rate with no volume beside it has no direction. | CTR | Impressions | What it means | | ---- | ----------- | ------------------------------------- | | Down | Up | Ranking more broadly. Usually good | | Down | Flat | Something changed in the result. Look | | Up | Down | Smaller, better-matched audience | | Up | Up | The good one | Place impressions and average position next to it, always. ## CTR outside search The same ratio appears in email, ads and in-app prompts, and the same rules apply. It is clicks over whatever was shown, and it is meaningless without knowing how many were shown. The one difference worth knowing is that search CTR is heavily determined by position in a way email CTR is not. Half of the variation in a page's CTR is explained by where it ranks, which makes comparing two pages' CTRs without comparing their positions nearly worthless. ## CTR in Notch Metrics [Search Console click-through rate](/sources/google-search-console/ctr) is reported as a ratio over the window rather than the mean of daily rates. Place it beside [clicks](/sources/google-search-console/clicks), [impressions](/sources/google-search-console/impressions) and [average position](/sources/google-search-console/average-position), because those three together are the only way to read it. [How to get average position and CTR from Search Console](/blog/get-average-position-and-ctr-from-search-console) ## Questions **How is click-through rate calculated?** Clicks divided by impressions over the period. Use the period totals rather than averaging the daily rates, which would weight a day with ten impressions equally with a day of ten thousand. **Is a falling CTR bad?** Often not. CTR falls when a page starts ranking for broader terms, so more impressions at worse positions convert at a lower rate while total clicks rise. It also falls when Google answers the query on the results page or a rich result sits above you. **When is a falling CTR a real problem?** When impressions and average position are both steady and clicks fall. That isolates the one variable you control, which is what your result says. A page at position two with low CTR is usually a title or description problem. **Why does my CTR headline not match the chart?** Because each chart point is that day's own ratio and the headline is the period's clicks divided by the period's impressions. They are not supposed to match. **Can I compare CTR between two pages?** Only alongside their positions. Roughly half the variation in a page's search CTR is explained by where it ranks, so comparing two pages' CTRs without comparing their positions is nearly worthless. --- # Are free MRR trackers for Mac worth it? URL: https://notchmetrics.com/library/free-mrr-trackers-for-mac Published: 11 October 2026 Author: Notch Metrics Tags: mrr, mac, free Kind: alternatives Prices checked: 11 October 2026 ## TL;DR | Option | Price | What you give up | | --------------------- | ------------------- | ------------------------------------- | | MRRDock | Free, MIT licensed | Keys on your laptop, revenue only | | Orbl (base app) | Free | Revenue integrations need $39 Premium | | Baremetrics free tier | Free to $180K ARR | Price steps up as you grow | | Stripe Dashboard | Free | It is a browser tab | | **Notch Metrics** | **$9/mo or $49/yr** | Nothing on this list, for a price | Prices checked on 11 October 2026. ## Is a free MRR tracker good enough? Usually, yes, and that is worth saying plainly on a page published by somebody who sells one. If you take money through a single Stripe account and the only thing you want is an MRR figure you can see without a tab, MRRDock is free, open source, and does exactly that. No trial, no upgrade prompt, no seat limit. The question is not whether free works. It is which of three things you are quietly paying with instead. ## What free costs you **Your key sits on the laptop.** Every free Mac MRR tracker is local by design, which is their privacy story and also means a live provider key is on the machine you carry around. Restricted read-only keys reduce the damage a long way. They do not remove it. **One kind of number.** Free trackers track revenue. Not your Search Console clicks, not your Shopify orders, not your open issues. If you want those too, you are running four tools or opening four tabs, which is the problem you started with. **Somebody's spare time.** An open-source menu bar app is maintained because its author still cares. That is often for years, and sometimes it is not. MRRDock is signed, notarised and auto-updating, which is better than most, and it is still one person. None of those make free the wrong choice. They are just the invoice. --- ## 1. MRRDock The best free option, without much competition. Free and MIT licensed, no paid tier. It reads Stripe, RevenueCat, Paddle, Lemon Squeezy, Polar, Dodo Payments and Gumroad, converts them into one display currency, and puts the total in the menu bar. It also takes a custom HTTPS endpoint, which is a neat way to keep the real key on a server you control and feed the app a JSON blob instead. Keys live in the macOS Keychain. The app issues GET requests only. There is no account and no server. Its MRR rules are documented, which matters more than it sounds: yearly divided by twelve, percentage coupons applied, fixed-amount coupons deliberately not applied, usage-based prices flagged rather than guessed. A tool that tells you what it could not price is more trustworthy than one that quietly returns a confident wrong number. **Price.** Free, MIT licensed. **Requires.** macOS 14 or later, Apple silicon or Intel. ## 2. Orbl's free tier Orbl's base app is free with every built-in widget, and it puts information on your wallpaper rather than in a bar. The revenue integrations are behind Premium, which is $39 as a one-time purchase, listed as a launch price rising to $49. So the free tier is a free desktop widget app, not a free revenue tracker. Worth being clear about, because the one-time price is genuinely the fairest thing about it and it is still a price. **Price.** Free app. Premium $39 once for the revenue integrations. ## 3. The Baremetrics free tier Free while it tracks up to $180K ARR, which covers a lot of businesses that think of themselves as too small for it. You get the full subscription analytics, trial insights, email reports and segments. That is far more than any menu bar app, in exchange for your billing data living on their servers and a price that starts at $49 a month once you cross $360K ARR. If you are under the threshold, this is the most capable free option on the page by a distance. **Price.** Free to $180K ARR, then from $49/month. ## 4. The Stripe Dashboard Free, authoritative, and already shows MRR and churn in Stripe Billing. The mobile app pings you on each payment. Its only failing is the one this whole category exists for: it is a tab, and a tab is a decision every time. **Price.** Free. --- ## When paying is the cheaper decision Three cases, honestly. **Your money arrives in more than two places.** Running a free tracker for Stripe, opening RevenueCat for the apps and Shopify for the store is three habits. One tool that holds all of them is worth a subscription in recovered attention alone. **You want more than revenue.** Visitors, search clicks, repository issues and subscriber counts are all numbers you check. A revenue tracker gets you one of them. **You do not want a key on your laptop.** This is the one that is hard to fix for free, because the free model depends on being local. That is where [Notch Metrics](/) sits: $9 a month or $49 a year, fourteen connectors with no per-integration charge, and keys that are encrypted on the server and never reach the Mac at all. The numbers live in the MacBook notch, so there is nothing to open. If none of those three apply to you, use MRRDock. It is good, it is free, and recommending it costs us nothing we were owed. [See what Notch Metrics costs](/#pricing) · [Every source](/sources) ## Questions **What is the best free MRR tracker for Mac?** MRRDock. It is free and MIT licensed with no paid tier, reads Stripe, RevenueCat, Paddle, Lemon Squeezy, Polar, Dodo Payments and Gumroad into one menu bar total, and keeps keys in the macOS Keychain with no account or server. **Is Orbl free?** The base app is free with every built-in widget, but the revenue integrations are behind Premium, which is $39 as a one-time purchase listed as a launch price rising to $49. Checked 11 October 2026. **Is Baremetrics free?** It is free while it tracks up to $180K ARR, with full subscription analytics, trial insights and email reports included. Above that, Launch is $49 a month to $360K ARR. **What is the catch with free MRR trackers?** Three things. Your provider key sits on the laptop you carry around, they track revenue and nothing else, and they are maintained by one person's spare time. None of those make free the wrong choice, but they are the invoice. **When is a paid MRR tracker worth it?** When your money arrives in more than two places, when you want more than revenue in the same glance, or when you do not want a provider key on your laptop. That last one is hard to solve for free, because the free model depends on being local. --- # What is MRR? URL: https://notchmetrics.com/library/mrr Published: 11 October 2026 Author: Notch Metrics Tags: mrr, glossary, saas Kind: glossary **Monthly recurring revenue is the value of your active subscriptions, normalised to one month.** It is a snapshot of the book as it stands, not a total of what arrived in the last thirty days. ## How MRR is calculated Take every active subscription, convert its price to a monthly amount, and add them up. | Billing interval | Monthly contribution | | ---------------- | ----------------------------------- | | Monthly | The amount | | Annual | Divided by twelve | | Quarterly | Divided by three | | Weekly | Multiplied by 52, divided by 12 | | Daily | Multiplied by 365.25, divided by 12 | The weekly row is the one people get wrong. A month is 4.345 weeks on average, not four, so multiplying a weekly price by four undercounts it by about eight per cent. ## What MRR is not **Not last month's revenue.** A month with many annual renewals brings in enormous cash and does not change MRR at all, because the book did not grow. **Not a sum over a window.** MRR is what you are owed per month right now. Asking for "MRR over the last 90 days" is asking a question with no answer. **Not ARR divided by anything other than twelve.** ARR is MRR times twelve, by definition. Anything smoothed or annualised differently is a forecast wearing a metric's name. ## Why two MRR figures disagree Every provider and every tool makes a handful of choices, and every choice moves the number a per cent or two. - **Trials.** Counted, or not? Most exclude them, because a trial is revenue nobody has agreed to. - **Past due.** A card is failing and dunning is retrying. Still revenue, or already lost? - **Discounts.** Percentage coupons usually applied. Fixed-amount coupons often not, because prorating one across a multi-item subscription needs data most APIs do not return. - **Tax.** A merchant of record collects VAT that is not yours. Including it inflates MRR by whatever your customers' rates average. - **Usage-based prices.** Not priceable in advance. Guessed, or flagged? None of these has a right answer. What matters is writing yours down, keeping it, and being able to say what your figure counts when somebody asks. ## MRR in Notch Metrics Seven connectors report it, each using their provider's own definition where one exists: [Stripe](/sources/stripe/mrr), [Polar](/sources/polar/mrr), [Paddle](/sources/paddle/mrr), [Dodo Payments](/sources/dodopayments/mrr), [Lemon Squeezy](/sources/lemonsqueezy/mrr) and [RevenueCat](/sources/revenuecat/mrr). Each page states exactly what that provider's figure includes, because MRR is always somebody's definition and the whole point is knowing whose. [How to calculate MRR from the Stripe API](/blog/calculate-mrr-from-the-stripe-api) ## Questions **What does MRR stand for?** Monthly recurring revenue. It is the value of your active subscriptions normalised to one month, as a snapshot of the book right now rather than a total of what arrived recently. **How do you convert an annual plan to MRR?** Divide by twelve. An annual plan at $1,188 contributes $99 of MRR. A quarterly plan is divided by three, and a weekly plan is multiplied by 52 and divided by 12, because a month averages 4.345 weeks rather than four. **Is MRR the same as last month's revenue?** No. A month with many annual renewals brings in a lot of cash without changing MRR, because the book did not grow. MRR is what you are owed per month right now; revenue is what arrived in a window. **Why do two tools report different MRR for the same account?** Because each makes choices about trials, past-due subscriptions, discounts, tax and usage-based prices. None of those has a single right answer. What matters is writing your definition down and keeping it. **Should trials count towards MRR?** Usually not. A trial is revenue nobody has agreed to pay yet, so most providers and tools exclude them. Lemon Squeezy's own active subscriber count is a notable exception that counts trials in. --- # Revenue dashboards for indie hackers URL: https://notchmetrics.com/library/revenue-dashboards-for-indie-hackers Published: 11 October 2026 Author: Notch Metrics Tags: indie-hackers, revenue, guide Kind: comparison Prices checked: 11 October 2026 ## TL;DR | If you | Use | Price | | ---------------------------------------------- | ----------------- | ------------------- | | Take money in one place, want one number | MRRDock | Free, MIT | | Take money in several places, check constantly | **Notch Metrics** | **$9/mo or $49/yr** | | Are under $180K ARR and want analysis | Baremetrics | Free to $180K ARR | | Want the full detail, occasionally | Stripe Dashboard | Free | Prices checked on 11 October 2026. ## What should a solo founder actually track? Fewer numbers than you think, and more kinds of them. A business run by one person has no reporting layer and no weekly metrics meeting. The numbers do their work by being glanced at, which means the right set is the one that fits in a glance and the right frequency is whatever costs you nothing to check. Four categories cover almost every indie business: **Money.** [MRR](/library/mrr) if you sell subscriptions, [revenue](/sources/stripe/revenue) if you sell one-offs, usually both. **Demand.** Visitors and where they came from. A revenue number with no traffic number beside it tells you what happened and never why. **Search.** [Clicks and impressions](/sources/google-search-console/clicks), because for most indie products search is the whole acquisition channel and it moves on a timescale you will miss if you only look monthly. **Whatever your product's health actually is.** Open issues for a developer tool. Active users for an app. Subscribers for a channel. This is the one nobody can pick for you. Notice that only the first is revenue. A revenue dashboard that is only a revenue dashboard answers a quarter of the question. ## How often is too often? The usual advice is to check less. That is almost right and slightly wrong. The cost of checking is not the looking, it is the loading: the tab, the login, the chart redraw, and then the fifteen minutes you spend reading churn by cohort because you were already there. Reduce the cost to nothing and the frequency stops mattering. A number you glance at twenty times a day without opening anything has cost you nothing and taught you the shape of your week. The same number behind a login twenty times a day has cost you an afternoon. So: make it free to look, then look as much as you like. --- ## What to use ### Notch Metrics Built for exactly this shape of business. The numbers live in the MacBook notch, the strip of glass around the camera. Hover and they are there, move away and the notch looks untouched. No dock icon, no menu bar item, no tab. Fourteen connectors, every one included: [Stripe](/sources/stripe), [Polar](/sources/polar), [Paddle](/sources/paddle), [Dodo Payments](/sources/dodopayments), [Lemon Squeezy](/sources/lemonsqueezy), [RevenueCat](/sources/revenuecat), [Shopify](/sources/shopify), [Plausible](/sources/plausible), [PostHog](/sources/posthog), [Amplitude](/sources/amplitude), [Google Analytics](/sources/google-analytics), [Search Console](/sources/google-search-console), [YouTube](/sources/youtube) and [GitHub](/sources/github). That list is the argument. An indie business in 2026 usually has money in two places, traffic in one, search in another and a repository somewhere, and every one of those is a separate dashboard. For anything with no connector, a coding agent writes a [custom metric](/blog/writing-a-custom-metric) over MCP: one JavaScript module on your own Mac, secrets in the Keychain. **Price.** $9/month or $49/year. Seven-day trial, no card. [Download Notch Metrics](/download) ### MRRDock If your business genuinely is one Stripe account and one number, this is free and open source and you should use it. Keys in the Keychain, no server, MIT licensed, and its MRR rules are published. **Price.** Free. ### Baremetrics Free while it tracks up to $180K ARR, which covers most indie businesses outright. Full subscription analytics, trial insights and segments, in a browser. Above the threshold it starts at $49 a month and scales with tracked revenue. Right for the monthly question. Wrong for the daily one, because it is a tab. **Price.** Free to $180K ARR, then from $49/month. ### The Stripe Dashboard Free and authoritative. Keep it for the detail and for anything you need to be certain about. **Price.** Free. ## The mistake worth avoiding Picking tools by how much they can show you. A solo founder's constraint is attention, not data. The product that shows you forty metrics has given you thirty-five opportunities to spend an afternoon on something that was not going to change the outcome. Pick four numbers. Put them where looking costs nothing. Go back to work. [See what Notch Metrics costs](/#pricing) · [Every source](/sources) ## Questions **What numbers should an indie hacker track?** Four categories cover almost everything: money as MRR or revenue, demand as visitors, search as clicks and impressions, and one product-health number such as active users or open issues. Only the first is revenue, which is why a revenue-only dashboard answers a quarter of the question. **How often should I check my revenue?** As often as you like, once looking costs nothing. The expensive part is not the glance, it is the tab, the login and the fifteen minutes you then spend on churn by cohort. Reduce the cost to zero and the frequency stops mattering. **What is the best revenue dashboard for a solo founder?** It depends on how many places your money arrives. One Stripe account and one number: MRRDock, free and open source. Several sources plus traffic and search: Notch Metrics, $9 a month or $49 a year with fourteen connectors included. **Do I need a paid analytics tool as an indie hacker?** Often not. Baremetrics is free while it tracks up to $180K ARR, which covers most indie businesses, and the Stripe Dashboard already shows MRR and churn. Pay when you need cohort analysis regularly or when checking several sources has become a daily habit. **How many metrics should I watch?** About four. A solo founder's constraint is attention rather than data, and a tool showing forty metrics has given you thirty-five chances to spend an afternoon on something that was not going to change the outcome. --- # RPM against CPM URL: https://notchmetrics.com/library/rpm-vs-cpm Published: 11 October 2026 Author: Notch Metrics Tags: rpm, cpm, youtube, glossary Kind: glossary **RPM is revenue per mille: your earnings divided by every view, times a thousand. CPM is cost per mille: what advertisers paid, divided by monetised ad impressions, times a thousand.** They sound alike, they are quoted interchangeably, and they measure opposite sides of the same transaction. ## The two differences that matter | | RPM | CPM | | ----------- | -------------------------------------------- | --------------------------------- | | Denominator | **Every view** | **Monetised ad impressions only** | | Numerator | Your revenue, **after** the platform's share | Advertiser spend, **before** it | Both differences push the same way, which is why CPM is always substantially higher than RPM on every channel. **The denominator.** Not every view carries an ad. Viewers with ad blockers, viewers on Premium, videos that were not monetised, and plenty of ordinary playbacks produce no ad impression at all. The RPM denominator is therefore much larger than the CPM one. **The numerator.** CPM is what the advertiser was billed. RPM is what reached you after the platform took its cut. ## Why this matters practically The most common mistake in creator revenue discussions is comparing your RPM against somebody's quoted CPM and concluding you are being underpaid. You are comparing a post-revenue-share figure over all views against a pre-revenue-share figure over ad impressions. The gap is structural. It is not a sign of anything. If you want to compare yourself to somebody, compare RPM to RPM. If you want to understand advertiser demand in your niche, CPM is the right number and your own revenue is not. ## RPM is the one worth watching Because it is the only one that answers "what is a view worth to me". A rising CPM with a falling RPM means advertisers are paying more and fewer of your views are carrying ads, which is a format or audience change rather than a market one. A rising RPM is money. RPM also moves for reasons that have nothing to do with ad rates: longer videos carry more ad slots, a different audience country changes rates entirely, and a shift towards Shorts usually lowers it sharply. ## Compute it over the window RPM is a ratio, so it has the same trap as every ratio. ``` Right: (total_revenue / total_views) * 1000 Wrong: mean(daily_revenue / daily_views * 1000) ``` A quiet day with one high-value view produces an enormous daily RPM and should contribute almost nothing to the period figure. Averaging the points weights it equally with a day of ten thousand views, and the result jumps around for no reason. ## RPM in Notch Metrics [YouTube RPM](/sources/youtube/rpm) is computed over the whole window rather than averaged across daily rates, which is how YouTube Studio reports it. The page says plainly that it is not CPM. Place it beside [estimated revenue](/sources/youtube/revenue), [ad revenue](/sources/youtube/ad-revenue) and [views](/sources/youtube/views). [How to read estimated revenue and RPM from YouTube Analytics](/blog/read-estimated-revenue-and-rpm-from-youtube-analytics) ## Questions **What is the difference between RPM and CPM?** RPM is your revenue after the platform's share, divided by every view. CPM is what advertisers paid before the platform's share, divided by monetised ad impressions only. Both differences push the same way, which is why CPM is always much higher. **Why is my RPM so much lower than CPM?** Because not every view carries an ad, so the RPM denominator is far larger, and because RPM is what reached you after the platform took its cut while CPM is what the advertiser was billed. The gap is structural, not a sign of anything wrong. **Which should I track, RPM or CPM?** RPM. It is the only one that answers what a view is worth to you. CPM is the right number for understanding advertiser demand in your niche, and it tells you nothing about your own earnings. **Why did my RPM drop without my CPM changing?** Usually a format or audience change rather than a market one. Shorter videos carry fewer ad slots, a shift towards Shorts lowers RPM sharply, and a change in audience country changes rates entirely. **How should RPM be calculated over a week?** Total revenue divided by total views, times a thousand. Not the average of the daily RPM values: a quiet day with one high-value view produces an enormous daily figure and should contribute almost nothing.