Measured on our own account

We pointed our own reporting at our own ad account. It was wrong in seven ways.

Every reporting tool says it is accurate. Almost none publish the check. We held ours back from anyone else until it could reproduce, channel by channel, what Apple and Meta themselves report for our own spend — and until it would say stopped when nothing was delivering. It took seven fixes to get there. The sixth arrived hours after this page first went up, and the seventh the next morning — both from the check we had just built, not from anyone reading the page. All seven are below, with what each one would mean for your account.

Window 2026-07-23 to 2026-08-20 · reconciled 2026-08-21 · first published 2026-08-21 · figures are ours, in JPY. They are small: ¥51,689 of spend across two channels. The point of this page is not the size of the spend, it is the class of error — every one of these scales.

What the platforms say, and what we now say

Spend now matches both at the window level and day by day, across the 21 days the reconciliation covers — it did not when this page went up, and defect six is why. Install counts do not match at all, and are not supposed to: each number below is the count of a specific system, labelled with how it counted. We no longer collapse them into one.

Counted bySpend · installs · CPIHow it was counted
Apple Search Ads¥44,382 · 112 · ¥396Apple's own reporting API, for the campaigns we ran
Meta¥7,307 · 8 · ¥913the ad platform's own reported installs
AppsFlyer (MMP)¥7,307 · 16 · ¥457click attribution through an MMP, same spend, different counting

The last two rows are the same money. Two systems, two counts, 2.0x apart — and both of those CPIs are correct for what they measure. On Search Ads the equivalent gap was 1.04x. Averaging them would produce a number that is neither measurement, and adding them would double-count events that happened once.

The seven defects

  • 1. Another product's ad spend was sitting inside this product's report — ¥6,411 of it.

    Why: The daily figures were read from the ad account total. One ad account can carry campaigns for several products, and the account total does not know which is which.

    Fixed by: Read the per-campaign rows and keep only the campaigns belonging to the product being reported on.

    On your account: If you run more than one app or brand out of one ad account, check whether your reporting splits by campaign or by account. The account total is the number most tools reach for first, because it is the one the API hands you.

  • 2. Deduplication ran backwards, so the oldest report won. Meta spend read ¥6,121 instead of ¥7,307.

    Why: Reports arrive repeatedly for the same day and get better as the platform settles. We iterated newest-first and overwrote each day as we went, which leaves the last write — the oldest file — in place.

    Fixed by: Let the newest report win. Fixing only this brought the total onto the platform's own figure.

    On your account: Ad platforms restate recent days. Any pipeline that stores dailies has to decide which restatement wins, and the wrong answer here is silent: the totals stay plausible, they are just stale.

  • 3. Search Ads installs were parsed and then thrown away. The count shown was modelled, not measured.

    Why: The parser read the install figure and stored only the spend. Installs were then apportioned across channels using the channel mix from a single, most recent report — one report's mix, applied to every day in the window.

    Fixed by: Store the measured per-channel installs and stop apportioning for channels that report their own.

    On your account: A borrowed ratio applied across a window looks like data. Ask of any per-channel install count whether it was measured on that day or spread from somewhere else.

  • 4. With zero campaigns delivering, the dashboard headline still read "Running automatically across channels, inside the guardrails."

    Why: The headline was chosen from spend inside the window. Spend inside a 30-day window says nothing about whether anything is delivering today — every campaign had been paused and the window still held their spend.

    Fixed by: Carry delivery state into the dataset and say it plainly when nothing is live.

    On your account: This is the failure worth checking for in whatever you use. A dashboard that cannot say 'stopped' will keep reassuring you for as long as the window holds old spend.

  • 5. For one and the same ¥7,307, two systems reported 8 installs and 16 installs.

    Why: Neither is a bug. The platform reports its own SKAN/modelled count; the MMP reports click attribution. They are different measurements of the same events.

    Fixed by: Stop collapsing a channel into one CPI. Print every authority that has an install count, side by side, and label how each was counted.

    On your account: The size of that gap is not a constant. On our Search Ads spend the two counts sat 1.04x apart; on Meta, 2.0x. If you only ever look at one of the two systems, this difference is structurally invisible to you.

  • 6. Our window total sat ¥86 from the platform's — 0.2% — while the same days underneath it were off by ¥6,310, or 14.5%. On the worst single day, 2026-08-12, we said ¥972 and the platform said ¥1,885. The errors ran in both directions and cancelled.

    Why: The collector asked the ad platform for days in UTC. The account's own reporting is cut on its organisation timezone, nine hours away. Every day was carrying nine hours that belonged to its neighbour, so the errors ran in both directions and cancelled almost exactly once you summed a window. Neither side was obliged to mention it: the timezone travels in the request, and the report that comes back does not print it.

    Fixed by: Re-fetch the window from the platform in the account's own timezone and store that as the authoritative daily series. The engine reads it for the days it covers. Fixing the input also filled a day the collector had lost to a timeout, because refetching a window does not care which days are already there.

    On your account: If your reporting and your ad platform agree on the month and disagree on any single day, check the timezone each side asked for before you look at anything else. This class of error is invisible to every check that reconciles totals, which is most of them — and it is the days, not the months, that you make decisions on.

  • 7. The check went red on Meta: our engine had 8 installs where the platform's API returned 10, for the identical ¥7,307 of spend. The entire gap was one day, 2026-08-21, where we had 0 and the platform later had 2 — on ¥0 of spend that both sides agreed on.

    Why: Neither side was wrong. Spend for a day is final that day; installs are not. SKAdNetwork postbacks and multi-day click windows keep arriving after the day closes. Our engine reads a daily snapshot, and the check was reconciling that snapshot against a live API call made the next morning — so the newest day in the snapshot was, by construction, a day the platform had not finished counting.

    Fixed by: End the reconciliation window at the last day the snapshot could have closed, and print why the window ends where it does. We did not widen any tolerance: widening one would let a real disagreement through, and the day we removed is one our engine cannot know.

    On your account: If you reconcile your own numbers against an ad platform's API, the freshest day is not comparable and the mismatch will look like a bug in your pipeline. Check whether the two sides read the same metric at the same age before you change any code. The tell is that spend agrees and conversions do not.

The one that would have cost us money

Defect four is the one to take away. At the moment we measured, the number of campaigns actually delivering was 0 — everything was paused. The dashboard still opened with a sentence about campaigns running inside their guardrails, because that sentence was chosen from spend inside the window, and the window still contained the spend from before the pause. Nothing about it was a lie anyone wrote; it was a lie the window told. A reporting tool that structurally cannot say nothing is running will keep reassuring you for exactly as long as its window holds old spend. That is the check we would run first on whatever you use today: pause everything, and see how long it takes the dashboard to admit it.

What is still not fixed

Our engine now totals ¥51,689 for this window against ¥51,689 from the platforms — a residual of ¥0, or 0.0% — and the days underneath it agree too. That is a better place to be than the one this page described when it went up, and it is not the same thing as being finished. What follows is what this check still does not cover.

  • · The day-by-day agreement rests on a re-fetch of the window from the ad platform, and that re-fetch is a separate moving part from the check it feeds. Our gate turns red if it is more than 72 hours old, because a collector that has stopped does not report that it stopped — it keeps serving the day it stopped on as though it were today's.
  • · This check is weaker than the one it replaced. It used to compare the platform against the whole chain, including the daily files our collector writes. The engine now reads the re-fetched series directly, so for the days that series covers, those files are no longer being read by anything. What this page can claim is that the numbers we collected match what the platform reports — not that every step between them is still being exercised.
  • · We reconciled against the platform's reporting API, not its console. We have not put the two side by side. If they ever disagree, the console is the one you would be looking at.
  • · Delivery state is checked against the ad platforms directly, not carried by the reporting engine itself. The headline is right today because the check is external to it.
  • · This audit covers the two channels we actually spent on. It says nothing about connectors we have not run our own money through.
  • · Because the reconciliation now stops at the last day our snapshot could have closed, the most recent day is never reconciled. That day is the one you would be looking at. We check it against nothing.
  • · We know installs keep arriving after a day closes; we have not measured how long that goes on. We removed one day because that is the one our snapshot structurally cannot know. If a platform restates a day that is already a week old, this check will go red again and the cause will not be the window.

We publish this list because a page of fixed defects with no open ones is not a report, it is a brochure.

Run the same check on your own numbers

Running these checks on your own numbers takes a workspace: you import an Ads Manager export into it and the same engine reads it. If your export and your app store disagree about installs — the disagreement defect five is about — that is the guide on which count belongs in the denominator. If your export is missing a column the check needs, the import names it rather than producing a quiet wrong number.

Or connect the accounts and have the reconciliation run continuously — start a free trial. No credit card to begin.