See CAC and payback per channel by joining 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.

Runs
1st of the month at 9:00
Posts
Stat tile

From your apps to your board

How the skill works: your agent reads spend from each ad platform from Google Ads; advertising from Meta Ads; new paying customers from Stripe; analytics from Google Analytics; each customer's first touch from PostHog; google ads customer id, meta ad account id, posthog project id or ga4 property id, gross margin (%) from What you tell it, 1st of the month at 9:00, and gives you blended cac and the report in the chat.

Reads

  • Google AdsSpend from each ad platform
  • Meta AdsAdvertising
  • StripeNew paying customers
  • Google AnalyticsAnalytics
  • PostHogEach customer's first touch
  • What you tell itGoogle Ads customer ID, Meta ad account ID, PostHog project ID or GA4 property ID, Gross margin (%)
reads
Your agentClaude Code, Codex, Cursor or Gemini1st of the month at 9:00
posts

You get

  • Blended CACA stat tile on your board, with its trend
  • The report in the chatEvery run, with or without Briefkin

What you get

  • 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".

The card on the right is a sample. Yours shows your own numbers and updates in place every run.

What lands on your board

acme-webBlended CACLast 30 days$163-1% vs previous period, bettervs last report
Google Ads CAC$212
Meta CAC$340
Google payback 7 moMeta payback 11 mo9 unattributed
An example with made-up numbers. Your agent posts your real ones.

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

  1. 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, 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. 2List the new paying customers

    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. 3Look up each customer's first touch

    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. 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). 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. 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-channel and post blended CAC with its history.

  6. Post it to the board

    With BRIEFKIN_TOKEN set, 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.

Agent
Data
Schedule
  1. 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.

    shellcurl -fsSL https://briefkin.dev/skills/stripe-cac-and-payback-by-channel/install | sh
  2. Run it

    Ask Claude Code:

    promptRun the stripe-cac-and-payback-by-channel skill

    Or paste the prompt instead of installing:

    promptWork 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.
  3. Put it on a schedule

    1st of the month at 9:00, from cron on any machine where Claude Code is signed in:

    crontab0 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.

  4. Send it to your board

    Connect Briefkin once with curl -fsSL https://briefkin.dev/install | sh and 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_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.

Connecting Google Ads

  1. Google Ads MCP server, Google's open-source, read-only server (google-ads-mcp, source). Run it locally with pipx 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 are list_accessible_customers, search for GAQL and get_resource_metadata (docs).
  2. 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 login with the adwords scope) or an OAuth client, and set GOOGLE_PROJECT_ID. Add GOOGLE_ADS_LOGIN_CUSTOMER_ID when 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

  1. 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 documents claude 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.
  2. Meta Ads CLI. Meta's command-line tool installs with pip install meta-ads, reads the token from ACCESS_TOKEN, and returns insights with meta ads insights get --fields spend,impressions,ctr,cpc, which suits scheduled runs from Claude Code or Codex.
  3. 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_read permission. The skill reads META_ACCESS_TOKEN and META_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

  1. Stripe MCP server, Stripe's official remote server at https://mcp.stripe.com. Add Stripe from Claude's connector directory, or run claude 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.
  2. The Stripe CLI. Run stripe login once, then stripe subscriptions list --status active --limit 100 or stripe invoices list --status open. The CLI defaults to test mode, so add --live for real data.
  3. API with a restricted key. Create a restricted key (rk_live_...) in the Dashboard under Developers > API keys, store it in STRIPE_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

  1. Google Analytics MCP server, Google's own open-source server (analytics-mcp on PyPI, source). It runs locally with pipx run analytics-mcp, offers run_report, run_realtime_report and get_account_summaries, and Google labels it experimental.
  2. 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_CREDENTIALS at its JSON key. The skill also reads GA4_PROPERTY_ID.
  3. 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.json gives 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

  1. 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).
  2. Query API with a personal API key. Create one in your PostHog account settings, store it in POSTHOG_API_KEY with POSTHOG_PROJECT_ID and POSTHOG_HOST (https://us.posthog.com or https://eu.posthog.com), and give it the query:read scope, plus insight:read or feature_flag:read if 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.

Watch blended cac on your board, run after run

Skills are free to use with any agent. Briefkin is where their results land: one board for every report, number and question your agents send, with a ping when something needs you.

Free during the open beta. No card.