What it does
Google Ads and Meta each report a cost per conversion by their own attribution rules, and both happily count a sign-up that never paid. Stripe knows who paid but not which ad brought them. CAC per channel sits in the gap between the two, usually in a spreadsheet rebuilt the week before a board meeting.
Once a month the agent takes spend from both ad platforms, finds every customer whose first paid invoice landed in the window, looks up where each one first arrived in PostHog or Stripe metadata, and divides. Payback uses that first month's revenue and the gross margin you set, so "Meta seems fine" turns into "Meta takes 11 months to earn back what it costs".
How it works
1Pull spend from each ad platform
For the 30 days ending yesterday, run
SELECT metrics.cost_micros FROM customer WHERE segments.date BETWEEN '<start>' AND '<end>'in Google Ads and divide by 1,000,000. For Meta, callGET /act_{ad_account_id}/insights?level=account&fields=spend&time_range={"since":"<start>","until":"<end>"}. Convert both to your Stripe currency if the ad accounts bill in another one.2List the new paying customers
Page through
GET /v1/invoices?status=paid&created[gte]=<start>&created[lt]=<end>and keep invoices withamount_paid > 0. A customer is new whenGET /v1/invoices?customer=<id>&status=paid&created[lt]=<start>&limit=1returns nothing, which also catches trials that converted this month. Keep the customer ID, the lowercasedcustomer_emailand first-month MRR (the subscription's items normalized to a month, after discounts).3Look up each customer's first touch
Use Stripe customer
metadata.utm_sourceandmetadata.utm_mediumfirst if your checkout writes them. For the rest, run HogQL through/api/projects/:id/query/:SELECT properties.email, properties.$initial_utm_source, properties.$initial_utm_medium, properties.$initial_gclid, properties.$initial_fbclid FROM persons WHERE lower(properties.email) IN (<emails>).- 4
4Join on email and assign one channel
Match on lowercased, trimmed email (or the Stripe customer ID, if that is what you pass to
posthog.identify). Agclid, or sourcegooglewith mediumcpc,ppcorpaid, is Google Ads; anfbclid, or sourcefacebook,fb,instagram,igormetawith a paid medium, is Meta; any other source is organic and referral. Customers with no person and no metadata go to Unattributed; if that is over 25% of new customers, say the channel CACs are overstated and why. - 5
5Work out CAC and payback
CAC per channel is its spend divided by the customers it got; blended CAC is all ad spend over all new paying customers. Payback months = CAC / (average first-month MRR of that channel's customers x gross margin, default 80%). Read last month's card with
GET https://briefkin.dev/v1/cards/insight-stripe-cac-and-payback-by-channeland post blended CAC with its history. Post it to the board
With
BRIEFKIN_TOKENset, the agent sends the result as one card that updates in place every run. Without it, you get the report in the chat.
Install and run it
Pick your agent, where the data comes from and how to schedule it; the commands below follow.
Install the skill
One command. It puts the SKILL.md in ~/.claude/skills and in ~/.agents/skills, which Codex, Cursor, Gemini CLI, Copilot CLI and OpenCode read too.
shell
curl -fsSL https://briefkin.dev/skills/stripe-cac-and-payback-by-channel/install | shRun it
Ask Claude Code:
prompt
Run the stripe-cac-and-payback-by-channel skillOr paste the prompt instead of installing:
prompt
Work out my CAC and payback by channel for the last 30 days. Get ad spend from Google Ads (customer-level cost_micros) and Meta (ad account insights, spend). From Stripe, list customers whose first paid invoice falls in the window, with email and first-month MRR after discounts. Find each one's first-touch source in Stripe customer metadata utm_source, or in PostHog person properties $initial_utm_source, $initial_utm_medium, $initial_gclid and $initial_fbclid, matched on lowercased email. Assign each customer to Google Ads, Meta, organic or unattributed. Give me spend, new customers, CAC and payback months (CAC divided by average first-month MRR times an 80% gross margin) per channel, blended CAC (all ad spend over all new paying customers) and the unattributed share. End with the one budget change you would make first.Put it on a schedule
1st of the month at 9:00, from cron on any machine where Claude Code is signed in:
crontab
0 9 1 * * claude -p "Run the stripe-cac-and-payback-by-channel skill"Add it with
crontab -e. A laptop asleep at that time runs it when it wakes if you use launchd or Task Scheduler instead; GitHub Actions works for the API route.Send it to your board
Connect Briefkin once with
curl -fsSL https://briefkin.dev/install | shand every run lands on your board as a stat tile. How setup works
View the SKILL.mdRaw file
SKILL.md---
name: stripe-cac-and-payback-by-channel
description: "Monthly: what each new paying customer cost on Google Ads and Meta, matched on first-touch source, and how many months it takes to earn it back. Use when the user asks for this, or when it runs on a schedule."
---
# CAC and payback by channel: ad spend, first touch and Stripe
Monthly: what each new paying customer cost on Google Ads and Meta, matched on first-touch source, and how many months it takes to earn it back.
Google Ads and Meta each report a cost per conversion by their own attribution rules, and both happily count a sign-up that never paid. Stripe knows who paid but not which ad brought them. CAC per channel sits in the gap between the two, usually in a spreadsheet rebuilt the week before a board meeting.
Once a month the agent takes spend from both ad platforms, finds every customer whose first paid invoice landed in the window, looks up where each one first arrived in PostHog or Stripe metadata, and divides. Payback uses that first month's revenue and the gross margin you set, so "Meta seems fine" turns into "Meta takes 11 months to earn back what it costs".
## Two ways to get the data
Use the API or MCP route when you have an MCP server, a CLI or an API token for the app. Otherwise, if you can drive a browser the user is signed in to (Claude in Chrome, a Playwright or Chrome DevTools MCP server), use the browser route. Say which one you used.
## Route A: API or MCP server
### What you need
- UTM tags on every ad link, and a first-touch source per customer: PostHog identifying people by the same email or Stripe customer ID that Stripe holds, or `utm_source` and `utm_medium` written to Stripe customer metadata at checkout.
- Read-only Google Ads access (customer ID), Meta `ads_read` on the ad account, and a Stripe restricted key with Read on Invoices, Customers and Subscriptions.
- A PostHog personal API key with `query:read`, or Viewer access to GA4 as the weaker fallback described in the FAQ.
If something is missing, say what and stop; do not invent numbers.
### Steps
1. **Pull spend from each ad platform** (Google Ads): For the 30 days ending yesterday, run `SELECT metrics.cost_micros FROM customer WHERE segments.date BETWEEN '<start>' AND '<end>'` in Google Ads and divide by 1,000,000. For Meta, call `GET /act_{ad_account_id}/insights?level=account&fields=spend&time_range={"since":"<start>","until":"<end>"}`. Convert both to your Stripe currency if the ad accounts bill in another one.
2. **List the new paying customers** (Stripe): Page through `GET /v1/invoices?status=paid&created[gte]=<start>&created[lt]=<end>` and keep invoices with `amount_paid > 0`. A customer is new when `GET /v1/invoices?customer=<id>&status=paid&created[lt]=<start>&limit=1` returns nothing, which also catches trials that converted this month. Keep the customer ID, the lowercased `customer_email` and first-month MRR (the subscription's items normalized to a month, after discounts).
3. **Look up each customer's first touch** (PostHog): Use Stripe customer `metadata.utm_source` and `metadata.utm_medium` first if your checkout writes them. For the rest, run HogQL through `/api/projects/:id/query/`: `SELECT properties.email, properties.$initial_utm_source, properties.$initial_utm_medium, properties.$initial_gclid, properties.$initial_fbclid FROM persons WHERE lower(properties.email) IN (<emails>)`.
4. **Join on email and assign one channel**: Match on lowercased, trimmed email (or the Stripe customer ID, if that is what you pass to `posthog.identify`). A `gclid`, or source `google` with medium `cpc`, `ppc` or `paid`, is Google Ads; an `fbclid`, or source `facebook`, `fb`, `instagram`, `ig` or `meta` with a paid medium, is Meta; any other source is organic and referral. Customers with no person and no metadata go to Unattributed; if that is over 25% of new customers, say the channel CACs are overstated and why.
5. **Work out CAC and payback**: CAC per channel is its spend divided by the customers it got; blended CAC is all ad spend over all new paying customers. Payback months = CAC / (average first-month MRR of that channel's customers x gross margin, default 80%). Read last month's card with `GET https://briefkin.dev/v1/cards/insight-stripe-cac-and-payback-by-channel` and post blended CAC with its history.
## Route B: the browser
### What you need
- Signed in to Google Ads, Meta Ads Manager, Stripe (a role that can view invoices and customers) and PostHog, all in this browser.
If something is missing, say what and stop; do not invent numbers.
### Steps
Start at https://ads.google.com/aw/campaigns. The user is signed in already; never type passwords or codes, and ask if a sign-in page appears.
1. **Read Google Ads spend**: Pick the account if Google Ads asks, open Campaigns, set the date range to `Last 30 days` and read the Cost figure in the total row at the bottom of the table.
2. **Read Meta spend**: Open `https://adsmanager.facebook.com/adsmanager/manage/campaigns`, choose the ad account, set the date picker to `Last 30 days` and read Amount spent in the results row under the campaign list.
3. **List new paying customers in Stripe**: Open `https://dashboard.stripe.com/invoices`, filter Status `Paid` and Date to the same 30 days. Note each customer's email and amount, and open the customer to check this is their first paid invoice; a trial that started earlier but paid first this month still counts as new.
4. **Find each customer's first touch**: On the Stripe customer page, read Metadata for `utm_source`. If it is empty, open People in PostHog, search the email, and read Initial UTM source, Initial UTM medium and any initial gclid or fbclid on the Properties tab.
5. **Assign channels and divide**: A gclid or google with a cpc, ppc or paid medium is Google Ads; an fbclid or facebook, instagram or meta with a paid medium is Meta; other sources are organic; no data is Unattributed. CAC is spend over customers; payback is CAC over (average first-month payment x 80%).
Note: Looking up first touch one customer at a time works for a few dozen new customers a month. Past that, export People from PostHog to CSV and join in a spreadsheet, or use the API route. Stripe's invoice list shows amounts after discounts but mixes in annual plans: divide an annual invoice by 12 for the first-month figure.
## The report
- **Blended CAC** for the last 30 days, with the monthly trend.
- **Per channel**: spend, new paying customers, CAC, average first-month MRR and payback months.
- **Unattributed**: how many new customers had no first-touch source, and the likely reason.
- One line on what to do first, for example "Hold Meta budget flat until its landing page converts closer to Google's: same plan, $128 more per customer".
## Post it to Briefkin
When `BRIEFKIN_TOKEN` is set, or `~/.config/briefkin/env` exists (the installer from https://briefkin.dev/docs writes it), also send the result to the user's Briefkin board as one card. Keep the `item_key` the same on every run so the card updates in place, use a new `event_id` per run, and put today's real numbers in place of the example ones. If the request fails, say so once and carry on.
```bash
[ -n "${BRIEFKIN_TOKEN:-}" ] || . ~/.config/briefkin/env
curl -sS -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
-H "authorization: Bearer $BRIEFKIN_TOKEN" \
-H "content-type: application/json" \
-d @- <<'JSON'
{
"v": 1,
"event_id": "stripe-cac-and-payback-by-channel/<date of this run, e.g. 2026-10-16>",
"item_key": "stripe-cac-and-payback-by-channel",
"project": "acme-web",
"source": "CAC and payback by channel: ad spend, first touch and Stripe skill",
"kind": "metric",
"title": "Google Ads pays back in 7 months, Meta in 11",
"metric": {
"label": "Blended CAC",
"value": 163,
"prefix": "$",
"period_label": "Last 30 days",
"better": "lower",
"period_start": "<start of the period, ISO 8601>",
"period_end": "<end of the period, ISO 8601>"
},
"glance": [
{
"type": "Grid",
"props": {
"columns": 2
},
"children": [
{
"type": "Stat",
"props": {
"label": "Google Ads CAC",
"value": 212,
"prefix": "$",
"better": "lower"
}
},
{
"type": "Stat",
"props": {
"label": "Meta CAC",
"value": 340,
"prefix": "$",
"better": "lower"
}
}
]
},
{
"type": "Badges",
"props": {
"items": [
{
"label": "Google payback 7 mo",
"tone": "ok"
},
{
"label": "Meta payback 11 mo",
"tone": "warn"
},
{
"label": "9 unattributed",
"tone": "neutral"
}
]
}
}
],
"widget": [
{
"type": "Table",
"props": {
"columns": [
"Channel",
"Spend",
"New customers",
"CAC",
"Payback"
],
"align": [
"left",
"right",
"right",
"right",
"right"
],
"rows": [
[
"Google Ads",
"$8,480",
40,
"$212",
"7.0 mo"
],
[
"Meta",
"$6,800",
20,
"$340",
"11.2 mo"
],
[
"Organic and referral",
"$0",
25,
"n/a",
"n/a"
],
[
"Unattributed",
"n/a",
9,
"n/a",
"n/a"
],
[
"All channels",
"$15,280",
94,
"$163",
"5.4 mo"
]
]
}
},
{
"type": "Callout",
"props": {
"text": "Customers from both channels start on the same $38 a month at an 80% margin, so the gap is acquisition cost: Meta pays $128 more per customer.",
"tone": "warn"
}
}
],
"size": "md"
}
JSON
```
Widgets come from the Briefkin catalog (https://briefkin.dev/v1/catalog). Without a token, just give the user the report.
Set `period_start` and `period_end` to the window the number covers: the tile names it from them ("Last 7 days", "Today", "This month"). For a count right now (open items, a balance), send `"period_label": ""` so no period shows; for another name, send it as `period_label` (at most 24 characters).
## Run it on a schedule
Suggested: 1st of the month at 9:00 (`0 9 1 * *`). From cron, with the agent signed in on that machine:
```bash
0 9 1 * * claude -p "Run the stripe-cac-and-payback-by-channel skill"
# Codex: codex exec, Gemini CLI: gemini -p, Copilot CLI: copilot -p, OpenCode: opencode run
```
Or use the agent's own scheduler (Claude Code routines, Codex automations, Claude or ChatGPT scheduled tasks): https://briefkin.dev/skills/stripe-cac-and-payback-by-channel#schedule
Source: https://briefkin.dev/skills/stripe-cac-and-payback-by-channel
What your agent needs
UTM tags on every ad link, and a first-touch source per customer: PostHog identifying people by the same email or Stripe customer ID that Stripe holds, or
utm_sourceandutm_mediumwritten to Stripe customer metadata at checkout.Read-only Google Ads access (customer ID), Meta
ads_readon the ad account, and a Stripe restricted key with Read on Invoices, Customers and Subscriptions.A PostHog personal API key with
query:read, or Viewer access to GA4 as the weaker fallback described in the FAQ.
Connecting Google Ads
- Google Ads MCP server, Google's open-source, read-only server (
google-ads-mcp, source). Run it locally withpipx run --spec git+https://github.com/googleads/google-ads-mcp.git google-ads-mcp, or deploy it to Cloud Run for remote clients. Its tools arelist_accessible_customers,searchfor GAQL andget_resource_metadata(docs). - Google Ads API with OAuth. Enable the Google Ads API in a Google Cloud project, sign in with Application Default Credentials (
gcloud auth application-default loginwith theadwordsscope) or an OAuth client, and setGOOGLE_PROJECT_ID. AddGOOGLE_ADS_LOGIN_CUSTOMER_IDwhen you reach client accounts through a manager account, and write customer IDs without dashes.
The adwords scope can't be narrowed, so sign in as a Google Ads user with Read only access; the MCP server has no write tools either way.
Connecting Meta Ads
- Meta Ads MCP server, Meta's official remote server at
https://mcp.facebook.com/ads, with tools for reporting, ad and catalog management, datasets and troubleshooting. You sign in through Facebook Login for Business. For Claude Code, Meta documentsclaude mcp add --transport http --client-id <META_APP_ID> meta-ads https://mcp.facebook.com/ads, using a Meta developer app with the ads MCP use case added; clients that send headers can pass a user access token as a Bearer token instead (get started). Codex and Cursor take the same URL. - Meta Ads CLI. Meta's command-line tool installs with
pip install meta-ads, reads the token fromACCESS_TOKEN, and returns insights withmeta ads insights get --fields spend,impressions,ctr,cpc, which suits scheduled runs from Claude Code or Codex. - Marketing API with a system user token. In Business Settings, create a system user, give it the ad account with View performance access, and generate a token with the
ads_readpermission. The skill readsMETA_ACCESS_TOKENandMETA_AD_ACCOUNT_ID(act_1234567890).
The MCP server can create and edit ads when you grant management permissions. For unattended reports, a system user with only ads_read and View performance can't touch a bid, a budget or an ad.
Connecting Stripe
- Stripe MCP server, Stripe's official remote server at
https://mcp.stripe.com. Add Stripe from Claude's connector directory, or runclaude mcp add --transport http stripe https://mcp.stripe.com/, and sign in with OAuth; clients without OAuth can send a restricted key as a Bearer token. - The Stripe CLI. Run
stripe loginonce, thenstripe subscriptions list --status active --limit 100orstripe invoices list --status open. The CLI defaults to test mode, so add--livefor real data. - API with a restricted key. Create a restricted key (
rk_live_...) in the Dashboard under Developers > API keys, store it inSTRIPE_API_KEY, and give it Read on the resources the report touches: Subscriptions, Invoices, Charges, Customers, PaymentIntents, Balance and Disputes.
Never hand an agent the secret key (sk_live_...); a restricted key with Read where needed and None everywhere else can't refund, cancel or charge anything.
Connecting Google Analytics
- Google Analytics MCP server, Google's own open-source server (
analytics-mcpon PyPI, source). It runs locally withpipx run analytics-mcp, offersrun_report,run_realtime_reportandget_account_summaries, and Google labels it experimental. - Data API with a service account. Enable the Google Analytics Data API in a Google Cloud project, create a service account, add its email as a Viewer on the GA4 property, and point
GOOGLE_APPLICATION_CREDENTIALSat its JSON key. The skill also readsGA4_PROPERTY_ID. - Your own Google login.
gcloud auth application-default login --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform --client-id-file=client.jsongives the MCP server or a script your user's access without a key file.
Request only analytics.readonly and keep the service account at Viewer: none of these reports needs Editor.
Connecting PostHog
- PostHog MCP server, PostHog's official remote server at
https://mcp.posthog.com/mcp. Sign in with OAuth, or let the PostHog Wizard install it in your client; it routes to the US or EU cloud based on your account. Clients without OAuth can send a personal API key made with the MCP Server preset as a Bearer token (docs). - Query API with a personal API key. Create one in your PostHog account settings, store it in
POSTHOG_API_KEYwithPOSTHOG_PROJECT_IDandPOSTHOG_HOST(https://us.posthog.comorhttps://eu.posthog.com), and give it thequery:readscope, plusinsight:readorfeature_flag:readif the report uses them.
Add ?readonly=true to the MCP URL (or send x-posthog-read-only: true) to drop every tool that creates, updates or deletes; on the API, read scopes alone do the same.
Questions
What if many customers have no first-touch source?
They land in Unattributed, usually because they signed up on another device, an ad blocker dropped the PostHog script, or PostHog knows them by a different email. Calling posthog.identify with the Stripe customer ID, or writing utm_source to customer metadata at checkout, fixes most of it. GA4 cannot return one row per person through the Data API, so with GA4 alone the agent counts first purchases by firstUserSource and firstUserMedium and uses those counts instead of the email join, which is less exact.
Why is this CAC higher than cost per conversion in Google Ads or Meta?
Each platform counts conversions inside its own attribution window (Meta defaults to 7-day click and 1-day view), and both can claim the same customer. Here each paying customer belongs to one channel, their first touch, and sign-ups that never paid do not count. If your trial is long, spend from one month mostly buys customers who pay the next, so set a one-month lag in the settings.
Do I need Briefkin to use the CAC and payback by channel: ad spend, first touch and Stripe skill?
No. The skill runs with your own agent and gives you the report in the chat. With a Briefkin token set, each run also posts it to your board as a stat tile, so you can glance at it instead of asking again.
Which agents can run it?
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, OpenCode, Windsurf and Amp all read SKILL.md files. The install command puts it in ~/.claude/skills and ~/.agents/skills, where they look. ChatGPT, the Claude app and Grok take the prompt instead. Pick your agent under "Install and run it" and the commands change to match.
Can it work without an API or MCP server, from my browser?
Yes. Switch the data source to "Your browser" and the steps change to the Google Ads, Meta Ads, Stripe, Google Analytics and PostHog pages themselves: the agent opens ads.google.com/aw/campaigns in a browser you are signed in to and reads the numbers off the page. Claude Code, Codex, Cursor, Claude app and ChatGPT can drive a browser out of the box; Gemini CLI, Copilot CLI and OpenCode need a Playwright or Chrome DevTools MCP server first. It is slower than the API and needs you signed in, so it suits a report you run by hand or from a scheduler on your own computer.