One morning I opened Facebook Ads Manager to check the Newton campaign. It told me I'd gotten 8 customers (Purchases). Then I opened Stripe to look at the real money — it had already charged 15 people. Nearly half were missing. At first I figured Facebook was just slow to report, but two days later the number hadn't budged. So I told Tim (my AI agent) to go figure out what was happening — and the problem wasn't that Facebook was slow. My own tracking was getting blocked and thrown away, right at the customer's browser.

Why Facebook was undercounting

When you run Facebook Ads, you drop a tiny piece of code called the Pixel onto your site. When a customer lands on a page, buys something, signs up — the Pixel fires an event back to Facebook saying "this person did this action, and they came from this ad." Facebook uses that to decide which ads are worth the money and which to shut off.

The problem is the Pixel runs entirely in the customer's browser (client-side), and these days that's incredibly easy to block:

  • Ad blockers — tons of people run ad-blocking extensions, and the Pixel is the very first thing they kill.
  • Safari / iOS (ITP) — Apple's Intelligent Tracking Prevention strips out almost all third-party tracking. A huge slice of buyers are on iPhones, so a huge slice of events just vanish.
  • Dropped connections — a customer pays, the page bounces to "success," but their network hiccups for a split second before the Pixel fires. Event gone.

Put simply: the single most important event — Purchase — happens to be the one that has to "travel the farthest" to reach Facebook (through the customer's browser, past the ad blocker, past ITP), so it's the one that gets dropped most often. Meanwhile Stripe, which charges the card server-side, never misses. That's why the two numbers were so far apart.

The fix: also fire the event from the server (CAPI)

Facebook has a thing called the Conversions API (CAPI). Instead of firing events only from the customer's browser, you also fire them straight from your own server into Facebook. The server-side path can't be killed by an ad blocker or ITP, because it's machine-to-machine — it never touches the customer's browser at all.

So Tim wrote a helper called facebook_capi() inside Newton's backend. Every time someone completes a signup — the same way I had Tim close a Stripe webhook gap earlier — Stripe's webhook (the call that says "payment succeeded") triggers the server to fire a Purchase event into Facebook immediately, without waiting for the customer's browser to do it.

Sounds easy, right? There's a trap right here.

The trap: fire from both paths = double-counting

Once you fire Purchase from both the browser (Pixel) and the server (CAPI) for the same customer — if both events reach Facebook, you've just turned "1 customer" into "2 Purchases."

Instead of fixing the undercount, you've created an overcount. The number's still wrong — just wrong in the other direction.

The way Facebook designed this to be solved is deduplication with event_id. The idea: both the Pixel and CAPI fire the same event with the same ID. When Facebook sees a duplicate event_id, it knows "ah, this is one event that arrived via two paths" and keeps only one.

The rule is that the event_id has to come from something genuinely unique to that customer. So Tim used stripe_sub_id (the subscription number Stripe generates at signup) as the Purchase event_id — it can never collide, and both the browser side and the server side already know it:

# Browser side (Pixel) — fires when the success page loads
fbq('track', 'Purchase', {value: 29, currency: 'USD'},
    {eventID: 'sub_1Q8xZ...'});   // ← event_id = stripe_sub_id

# Server side (CAPI) — fires when the Stripe webhook arrives
facebook_capi(
    event_name = "Purchase",
    event_id   = "sub_1Q8xZ...",   # ← exactly the same ID
    value = 29, currency = "USD",
)
# Facebook sees the duplicate event_id → merges to 1 Purchase

The guiding principle: over-fire and dedup beats under-counting — because an overcount is fixable with dedup, but a lost event is gone forever, you can't get it back. So Tim applied the same pattern to every important event, not just Purchase:

  • Purchase — deduped with stripe_sub_id
  • InitiateCheckout (clicked to start paying) — deduped with session_id
  • newton_pageview (landed on the sales page) — deduped with an event_id the frontend generates and sends down both paths

The result: the two numbers converged

A few days after shipping, the Purchase count in Facebook Ads Manager and the real number in Stripe started moving toward each other. From a gap of nearly half, it shrank to a tiny difference (which is normal — there's always a small delay between two systems).

This isn't just "prettier numbers." It matters because Facebook uses your conversion data to optimize ad delivery. Feed it broken, missing data and it optimizes for the wrong people, chasing folks who'll never buy and burning your budget. Once the tracking got accurate, Facebook's optimizer started hitting the target better — every dollar of ad spend started working harder. Same deal as when I had Tim run my Facebook ad campaigns end to end.

What I took away from this one

1. Dashboard numbers aren't always the truth. I almost shut down a campaign that was actually profitable, just because I trusted the number Facebook reported. The lesson: always compare the ad platform's number against the real money (Stripe / your bank). If they're more than 10–20% apart, your tracking has a leak.

2. Anything that runs through the customer's browser can't be trusted 100%. Ad blockers, ITP, dropped connections, tabs closed before the page finishes loading — there are a thousand ways a client-side event disappears. Anything important needs a server-side backup.

3. Dedup is the heart of dual-path tracking. If you're going to fire from both client and server, you have to line up the event_id from the start, or you'll fix the undercount and walk straight into an overcount. The key is to pick something genuinely unique to that transaction as the ID — never roll a random number.

4. This kind of work is never "urgent" but it quietly bleeds money every day. A tracking leak doesn't crash your site. No customer calls to complain. It just quietly makes you decide wrong about your ads, every single day. That's exactly the kind of job I love handing to Tim, because it's the kind people keep putting off until they've already lost a pile of money. The same trap bit me from the other end when I had Tim auto-reply to ad comments with the signup link — 48 people got the link but only 5 signed up, proof that pretty engagement numbers lie until you trace the whole funnel.

I didn't touch a line of code

For this whole thing, all I did was tell Tim "Facebook is reporting almost half the Purchases Stripe has — go see what's going on." Tim took it from there — compared both numbers, figured out it was client-side tracking getting blocked, knew about CAPI, set up dedup with event_id, applied it across every event, and tested until the numbers met. I just sat behind the screen approving and watching the result.

This is the part I love most about having a personal AI agent — the deep technical work you'd normally have to hire a dev for, or just let bleed because you never have time, gets handled and closed out in a single day.

If you want an AI agent like this too — one that lives on your own private server, looks after your site, watches your tracking, and quietly closes the leaks you'd never spot — Newton is open for signups now. It sets up in about 10 minutes and gives you a personal AI agent that works exactly like Tim does. Go take a look at the Newton page.

— Pond