Track trial-to-paid conversion by acquisition source

Build the trial-to-paid funnel split by acquisition source. Surface the channel that converts 8x better than the rest.

Updated · published

markdown for agents →

difficulty intermediate · time to value 3 minutes · execution on demand

Start from this

Build the trial-to-paid funnel split by acquisition source. Which channel converts trials best?

Why this matters

Most acquisition dashboards show traffic. The metric that actually matters is conversion-by-source. Reddit might be 1% of your traffic but 8% of your paid signups; LinkedIn might be 30% of traffic and 2% of paid. Spending $5k on more LinkedIn ads is then provably wrong.

Mixpanel has this view but you set it up once per source and re-build it every quarter. GA4 has UTMs but no funnel UI. Most teams have a Notion doc with stale numbers and a Slack message asking “wait, where do our actual paying customers come from?”

Agentry first checks whether the saved signal map, current verified onboarding state, and live event properties can actually support the question. Then HogQL groups your funnel by acquisition_source in one query; a public dashboard is only a follow-up if you explicitly ask for a browser-safe view.

What you get

  • A table of acquisition sources × trial-start × paid-conversion × conversion rate
  • The channel with the best conversion rate (not just the most volume)
  • Optional browser-safe output when you explicitly ask for an embeddable weekly review

Walk through it

You

Build the trial-to-paid funnel split by acquisition source. Which channel converts trials best?

Agent

Let me check your event vocabulary first.

Endpoint: GET /v1/projects/:project_id/event-names
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fevent-names&method=get
Use query parameters, not a JSON body.
Query/filter concept:
  matching: "trial|signup|subscription|paid"
{
  "server_emitted": [],
  "analytics_events": [
    { "event": "trial_started", "count": 4280, "last_seen": 1783651200 },
    { "event": "signup_completed", "count": 4310, "last_seen": 1783651210 },
    { "event": "subscription_started", "count": 412, "last_seen": 1783651250 },
    { "event": "subscription_canceled", "count": 31, "last_seen": 1783651290 }
  ],
  "wildcards": []
}
Agent

Good — trial_started and subscription_started are plausible edges. I still need to verify that acquisition_source exists on the trial event and that both events use the same stable identity before running the comparison.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "WITH trials AS (SELECT distinct_id, properties.acquisition_source AS source, min(timestamp) AS trial_ts FROM events WHERE event = 'trial_started' AND timestamp >= now() - INTERVAL 90 DAY GROUP BY distinct_id, source), paid AS (SELECT distinct_id, min(timestamp) AS paid_ts FROM events WHERE event = 'subscription_started' AND timestamp >= now() - INTERVAL 90 DAY GROUP BY distinct_id) SELECT t.source AS source, count(DISTINCT t.distinct_id) AS trials_count, count(DISTINCT p.distinct_id) AS paid_count, round(count(DISTINCT p.distinct_id) * 100.0 / count(DISTINCT t.distinct_id), 1) AS conv_pct FROM trials t LEFT JOIN paid p ON t.distinct_id = p.distinct_id AND p.paid_ts > t.trial_ts AND p.paid_ts < t.trial_ts + INTERVAL 14 DAY GROUP BY source ORDER BY trials_count DESC"
{
  "rows": [
    { "source": "google_organic", "trials_count": 1820, "paid_count": 142, "conv_pct": 7.8 },
    { "source": "linkedin_ads",   "trials_count": 980,  "paid_count": 41,  "conv_pct": 4.2 },
    { "source": "direct",         "trials_count": 720,  "paid_count": 71,  "conv_pct": 9.9 },
    { "source": "reddit",         "trials_count": 142,  "paid_count": 31,  "conv_pct": 21.8 },
    { "source": "twitter",        "trials_count": 312,  "paid_count": 18,  "conv_pct": 5.8 },
    { "source": "producthunt",    "trials_count": 280,  "paid_count": 88,  "conv_pct": 31.4 }
  ]
}
Agent

In this illustrative result, Product Hunt and Reddit have higher observed trial-to-paid rates. The samples differ substantially, and conversion alone does not establish CAC, payback, incrementality, or channel profitability.

This by-source conversion query is custom HogQL, not a built-in public-query blueprint. I can keep it in a protected internal dashboard by running the analytics/query call server-side. If you want a browser-safe public URL, I should first add a real trial-to-paid query blueprint and then publish that blueprint.

Agent

Protected result ready. The next step is to review spend, cohort quality, retention, attribution, and uncertainty before changing any campaign.

The output

Trial → Paid by Acquisition Source (90d, 14-day conversion window)

SOURCE             TRIALS   PAID   CONV%   SAMPLE NOTE
producthunt           280     88   31.4%   smaller cohort
reddit                142     31   21.8%   small cohort
direct                720     71    9.9%   attribution may mix sources
google_organic      1,820    142    7.8%   largest observed cohort
twitter               312     18    5.8%   smaller cohort
linkedin_ads          980     41    4.2%   spend/payback not measured

Volume vs quality split:
  Volume leaders:  google_organic, linkedin_ads
  Quality leaders: producthunt, reddit, direct

Review before action:
  - add spend and payback before judging paid-channel economics
  - compare later retention and revenue using the same identity/cohort rules
  - validate attribution and sample stability before changing budgets

Public view:
  Use Agentry's returned public_url after publishing a real query blueprint.

Setting it up

You need two events plus a UTM-style source on the trial event:

// On signup completion (server-side, after email verify):
await fetch(`https://api.agentry.sh/v1/analytics/`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.AGENTRY_SERVER_API_KEY}`,
    "Content-Type": "application/json",
    "User-Agent": "myapp/1.0",  // REQUIRED — Cloudflare 403s default UAs
  },
  body: JSON.stringify({
    event: "trial_started",
    distinct_id: user.email,
    properties: {
      acquisition_source: user.utm_source ?? "direct",  // ← the key field
      acquisition_campaign: user.utm_campaign,
      referrer: user.initial_referrer,
    },
  }),
});

// On Stripe webhook (subscription.created → trial → active OR direct paid):
await fetch(`https://api.agentry.sh/v1/analytics/`, {
  method: "POST",
  headers: { /* same */ "User-Agent": "myapp-stripe/1.0" },
  body: JSON.stringify({
    event: "subscription_started",
    distinct_id: stripeCustomerEmail,
    properties: { plan: subscription.items.data[0].plan.nickname, amount: subscription.items.data[0].plan.amount },
  }),
});

To capture utm_source on signup, snapshot it from the landing page into a first-party cookie (or localStorage) and read it on signup:

// On any landing page hit:
const params = new URLSearchParams(window.location.search);
if (params.get("utm_source")) {
  localStorage.setItem("acq_source", params.get("utm_source")!);
  localStorage.setItem("acq_campaign", params.get("utm_campaign") ?? "");
}
// On signup form submission, read back from localStorage and POST with the trial_started event.

Variations

  • “Same funnel but for the last 30 days only — has anything shifted post-launch?”
  • “Compare trial-to-paid by GEO (country) — are non-US trials converting differently?”
  • “For Product Hunt specifically, what’s the path-to-paid? Show the average # of sessions before paying.”
  • “Build a separate dashboard for paid ad channels only (LinkedIn, Google ads, Facebook). Compare CPA × conversion %.”

Adapt this workflow in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.