Case Study 09 · Marketing Operations
A single screen that shows what a lead or a sale actually cost, not what Meta's Ads Manager reports. Built for a founder who only has two questions on a Monday morning, and could not get a clean answer to either from the ad platform itself.
The Problem
If you run Meta ads to generate leads or sales, only two questions matter on a Monday morning: what did a lead cost last week, and what did a sale cost. Ads Manager is built for media buyers, it has two hundred columns, and every column is correct in Meta's terms. But Meta's terms are not the business's terms.
Its "lead" is not the business's lead: it adds together on-Facebook form submissions and website pixel events, so the same parent can count twice. Its "purchase" is not a clean sale: Meta credits an ad with any purchase by anyone who saw or clicked it in the last week, including the ones a sales team phoned in and closed themselves. Its "results" are not one thing: a chat conversation, a form fill and a website signup all sit under the same word, so a blended cost per result tells you nothing. And the business's real numbers live elsewhere entirely, in a store, a CRM, a team chat, reconciling them by hand takes twenty minutes and rarely happens.
A campaign that looks like it returns 35 times its spend gets more budget. A lead that looks like it costs $12 does not get questioned. When both figures are roughly three times too good, every budget decision built on them is wrong, and nobody finds out until the quarter is over.
How Far Off The Numbers Really Are
A real one-day snapshot from a founder-led education business, rounded and anonymised. Same ads, same day. The left is what Meta reported. The right is what the business's own systems showed once asked the right question.
| Holiday programs, purchase ads | Meta reported | What actually happened |
|---|---|---|
| Purchases | 6 | 2, the other 4 were sales-team closes or unrelated orders |
| Revenue | ~$3,960 | ~$780 from ads |
| Cost per sale | ~$18 | ~$55, three times higher |
| Return on spend | 35.7x | ~7x, still excellent, not miraculous |
| Free trial class, instant-form ads | Meta API total | What actually happened |
|---|---|---|
| Form leads | 21 | 8, the other 13 were website pixel events already counted elsewhere |
| Cost per lead | ~$12 | ~$32, matching the platform's own Results column exactly |
Nothing about the ads was wrong. The reporting was. A CEO reading the platform's numbers would have scaled the holiday campaign and left the free class campaign alone. Reading the real numbers, both decisions deserve a second look.
The Brief
The dashboard started from a sticky note, not a data model: readable in sixty seconds on a phone, first thing in the morning, no sideways scrolling. One number per goal, each funnel gets a tab, each tab has one hero metric. Results counted where they happen, leads the way the sales team receives them, orders in the store, never in Meta's pixel. Sales-assisted results stripped out entirely, if a person sold it, the ad does not get the credit. Grouped by funnel, not by campaign, since campaign names change weekly and funnels do not. No new logins, no new tools, no subscription.
| Funnel | The CEO's question | The hero metric |
|---|---|---|
| Messaging ads | Are people starting conversations, and at what price? | Cost per conversation started, plus conversation rate |
| Lead magnet | What does a download cost? | Cost per form lead |
| Free trial class | What does a class booking cost? | Cost per form lead, split by instant form and website |
| Holiday intensives | What does an ads-driven sale cost, and what does it return? | Cost per order and revenue, ads only |
The Dashboard
The dashboard is a single small program running on Cloudflare Workers, the same platform the business already used for its other automations. No database of its own, no analytics product behind it. Each time it is opened it asks three systems for the same date range and does the arithmetic itself.
Spend, impressions, link clicks and every action type per ad set per day, plus how each ad set is set up, read with a never-expiring system user token. The ad set's setup decides which funnel tab it belongs to.
Completed orders in the date range, with products, totals and the customer's phone number. Orders containing a holiday program are the sales for that tab.
An automation already posted one line per sale naming the closer and the customer. Every phone number mentioned is matched against store orders. A match means the sales team sold it, so it is removed from the ads-only figure.
Six tiles across the top of every tab, a daily chart of spend against cost per result, and one table of ad sets sortable by any column. The best-performing quarter of ad sets carries a small coral dot. Paused ad sets are labelled. Ad sets with no spend are hidden. New test ad sets file themselves into the right tab from their name or their setup, so the CEO never has to decode a campaign name.
The Messaging leads tab: hero tiles, funnel switcher, and the daily spend versus cost per lead chart
The ad set table: every column the CEO's decision actually depends on, sortable, with top performers flagged
Built to be boring. Results are cached so the ad platform is asked at most a few times an hour, the last good numbers stay on screen if it is unavailable, and the whole thing is one file that can be changed and redeployed in under a minute.
Why Generic Dashboards Get This Wrong
Every off-the-shelf reporting tool would have shown the flattering numbers with total confidence, because it reads the ad platform and nothing else. It does not know a sales team exists, that a chat conversation is the real lead, or that the store is the source of truth for a sale.
Leads, not merged
The platform merges on-Facebook form leads with website pixel leads into one number. Instant-form ad sets now count on-Facebook leads only, landing-page ad sets count website leads only, matching the platform's own Results column exactly.
Sales, not assisted
The platform credits ads with every purchase by anyone who saw or clicked them in the last week. The dashboard reads real orders from the store and removes any whose phone number appears in the sales team's own channel.
Funnels, not campaign names
Campaign names change weekly and mean nothing to a CEO. Every ad set sorts into a funnel tab by its name first, its setup second, so new tests file themselves without anyone touching the dashboard.
One hero metric per funnel
One blended cost per result tells a CEO nothing. Cost per conversation, cost per form lead, cost per ads-only order, each lives on its own tab, never mixed together.
The Transferable Recipe
Decide the one question per funnel, count the answer where it actually happens, subtract what the ads did not earn, and keep it to a single screen. The same recipe applies to any business buying leads or sales on Meta.
Not clicks, not reach, not "results". Something the business would actually pay for.
The store for sales, the booking tool for bookings, the CRM for qualified enquiries. The ad platform's opinion becomes a comparison column, not the answer.
If a human sold it, it comes off entirely. A clean small number beats a padded big one.
One screen, one glance, one decision. Anything more belongs to the person running the ads, not the CEO reading them.
What Went Wrong During The Build
The dashboard took an afternoon. Almost all of that afternoon went on the six problems below, each one a place a generic tool would have shown a confident, wrong number.
The platform's lead figure merges on-Facebook form leads with website pixel leads, 21 reported, 8 real. Fixed by splitting the count per ad set type so each only counts the kind of lead it was actually built to produce.
Six attributed purchases, three of them phoned in by the sales team. Fixed by counting orders in the store and removing any whose customer phone number appears in the sales team's channel.
A purchase-optimised landing-page ad set landed in the holiday tab because setup rules ran before name rules. Fixed by checking names across all funnels first, only falling back to setup when the name says nothing.
The automated sales posts contain a name and a phone number, nothing else, so matching on order numbers found nothing. Fixed by matching on the last nine digits of the phone number, so every format agrees.
The store's API returned 403 to the first requests, a security layer rejecting calls with no browser user-agent. Fixed by sending a proper user-agent, falling back to query-string authentication, and surfacing the real error text on the tile.
Redeploying and refreshing repeatedly during the build exhausted the hourly API allowance. Fixed with a shared cache, ten-minute reuse, a two-minute floor on manual refresh, and stale numbers shown with a note instead of an error.
The pattern behind all six: each one is a place where a generic tool would have shown a confident, wrong number, because it only reads the ad platform. The fixes all come from knowing the business, that a sales team exists, that a chat is the lead, that the store is the source of truth for a sale, and that closes get posted with a phone number rather than an order ID.
Technical Stack
| Component | Technology | Purpose | Cost |
|---|---|---|---|
| Backend and dashboard | Cloudflare Worker (single JS file, no framework) | Fetches all three sources, reconciles them, and renders the dashboard | Free |
| Caching | Cloudflare KV | Shared cache with ten-minute reuse, stale-while-revalidate on API limits | Free |
| Ad performance data | Meta Marketing API, system user token | Spend, impressions, link clicks, every action type, per ad set per day | Existing |
| Sales data | Online store REST API | Completed orders, products, totals, customer phone number | Existing |
| Sales-assist detection | Team chat bot, read-only | Reads the sales channel's close posts, matched by phone number to store orders | Existing |
Result
Built in an afternoon, using systems the business already had, its ad account, its store, its team chat, its existing hosting. No new vendor, no monthly fee, no data leaving the company. The dashboard runs at a single URL behind a key and is read on a phone before the first coffee.
What it needs in place elsewhere is short: a Meta Business Manager with a system user, read access to wherever real results live, somewhere sales-assisted results are recorded even if it is just a chat channel, and one clear answer from the founder to what the one number per funnel actually is.
The question worth asking any team running paid ads: for each funnel, what did a result cost last week, counted in the business's own system, with sales-assisted results removed? If the answer takes more than a minute to produce, the reporting is working for the ad platform, not for the business.