The Business Reality5 min read

Ten bots were running, and nobody was measuring whether they sent a single app visit

A first-principles audit of my social and YouTube bots. The dashboards were full of view counts, but the one number that mattered — does any of this produce an app visit? — was never being measured. The receiver was already live; one wire was missing.

#reality-check#analytics#first-principles#social-automation
Concept diagram: left shows what the dashboard shows — 28,706 views, followers, all green; right shows what nobody measured — app visits = ?
Views were measured. Clicks were not. The receiver was already there — one wire was missing.

Ten bots were running. YouTube Shorts channels, four Instagram accounts, Threads, an unattended follow bot. Open any dashboard and it was all green. 28,706 views, 3,207 followers, posts going out. The numbers climbed every day.

Then I forced one sentence on myself and it all stopped. Every time I look at a dashboard I force "analyze from first principles" — what is the one number that actually matters, and isn't the rest vanity? I aimed that question at the bots.

Why do these bots exist? To send people to apps that make ad revenue. So the real number isn't views — it's app visits. Views are just the step before.

So I asked: out of 28,706 views, did a single one produce an app visit?

I had no idea. Nobody was measuring it.

Dashboards measure activity, not outcomes

Ask your own automation: is it measuring activity, or outcomes?

Views, followers, successful posts — all upstream indicators. They prove "the bot did something," not "that something reached the goal." The revenue chain looks like this:

attention (views)  ->  click (app visit)  ->  conversion (revenue)

My dashboard filled only the leftmost cell. The middle cell — does attention actually cross over into a click — was pitch black. A Threads post with 50k views can send exactly zero people to an app. Without knowing that, a view count is just a pretty number.

What would you do?

This is where the hands get itchy. Views are climbing, so you want to make more content, open more channels. Just push more of the top.

Stop. The top is already overflowing — 20k views a month is not the bottleneck. The bottleneck is that you don't know where that attention leaks. Do you build one more bot, or do you first look at where the traffic you already have is draining out?

I chose the latter and went hunting for the measurement wire. And I was surprised twice.

Twist 1 — the receiver was already on

Adding UTM parameters (like utm_source, utm_campaign) to a link lets GA4 automatically classify "this visit came from Threads." For that, the app side needs a GA4 snippet.

I greped the app code. No GA measurement ID. "Right, no receiver," I thought — wrong.

The measurement ID was hiding behind an environment variable. The code only renders the snippet when process.env.NEXT_PUBLIC_GA_ID exists. grep looks for a hardcoded ID, so of course it came back empty. I curled the actual deployed page and the G-XXXX... gtag was rendering just fine. Six fortune apps had been capturing visits through GA4 for weeks.

Which means the UTM the YouTube Shorts had been attaching wasn't sprayed into the void — it was already being counted. I had diagnosed "no measurement" without knowing this. Absence in the source is not absence of the feature — you have to look behind the env vars.

Twist 2 — the real hole was exactly one

So what was broken? Shorts send full UTM. But the Threads bot was only attaching ?src=threads to its links.

GA4 only recognizes the utm_* prefix as a campaign. Custom parameters like src= are simply ignored. Every visit from Threads was being lumped into "source unknown." The traffic from a channel with 80k views was entirely invisible.

The fix was three lines. I changed one link-building function — every app link the bot emits passes through this single function (a single chokepoint):

# before: GA4 ignores it
return f"{url}?src=threads"
 
# after: auto-extract campaign from the host's first label (unse.ootssu.com -> unse)
host = url.split("://", 1)[-1].split("/", 1)[0]
camp = host.split(".", 1)[0]
return f"{url}?utm_source=threads&utm_medium=social&utm_campaign={camp}"

I didn't touch the six call sites. One function was the bottleneck, so fixing one function drags every link along. It's the same fix-it-once-at-the-root, and every caller follows structure.

Self-check — can you see your funnel?

Throw these three at your pipeline right now.

  1. Is the real number, worked backward from the goal, on your dashboard? Not views — does the outcome it's meant to produce (visits, signups, revenue) actually get logged as a number? If not, everything else is a vanity metric.
  2. Did you judge "no receiver" from absent code? Poke the live endpoint directly to see if it's already on behind an env var or feature flag. A source grep gives false negatives.
  3. Are your inbound links tagged in a language the destination can read? Homegrown params like src=, ref= get ignored by most analytics tools. Use the standard (UTM).

The honest part

Wiring the measurement did not create revenue. The data is currently zero. GA4 reports fill in only after real clicks arrive, and that takes the next 3–7 days.

This post isn't "I solved the problem." It's the story of how the reason I couldn't solve it for weeks was that I couldn't even see the thing to solve. Measurement didn't hand me an answer — it just finally let me ask the question: which bot actually sends people to an app. And the next discipline is not drawing conclusions before the sample builds up.

So what I'm pushing right now is not content, not channels. It's building nothing and waiting a few days to read that number. That's the most valuable next action I have. When you audit bots honestly, most of them turn out to be things you should kill — and whether to kill or keep is decided by that same number.

Is your automation measuring activity, or outcomes? Today, just once, curl your busiest bot's link and check that it arrives at the destination properly tagged. You'll probably be as surprised as I was.

Related