← All reports
Superseded 2026-09-25: the removal did ship, in v32 (live 2026-09-21) — no .sale activity on v32–v36. This page is the 18 Sep snapshot of v26.2–v29. See the 10 Oct revert decision document →

The no-ads .sale packs are still being sold

Evidence only — no revenue modelling. GA4 raw export analytics_538412813 · generated 2026-10-10

On v26.2, v28 and v29 the .sale ad-removal packs are tapped, paid for, and the reward is handed out. 24 completed purchases and 12 in-game reward payouts across those builds, the most recent on 2026-09-18.

It happens for players who installed on that version and for players who installed on an earlier one — the split is in the first table.

1 · Sale-pack activity by app version and install cohort

tapped = purchase_click, the player opening the offer. paid = in_app_purchase, Play billing them. rewarded = earn, the game handing over the entitlement. Any one of them being non-zero means the pack exists on that build; all three means it works end to end.

BuildPlayer installed…TappedPaid RewardedUsersLast seen
v26.2install version unknown02022026-09-16
v26.2installed ON this version14168212026-09-15
v26.2installed on an EARLIER version12122026-09-11
v28install version unknown01012026-09-16
v28installed ON this version52162026-09-17
v29installed ON this version11232026-09-18

v27.2 returns no rows at all — that is not evidence of removal. That build only ever reached 7 devices, for a single day; there is no sample to find anything in. Treat v28 and v29 as the releases that matter.

On v28 and v29 the buyers are players who installed on those builds. That is expected rather than surprising: these packs only ever sell in a player's first few days, so a long-standing player would not buy one on any version. v26.2 is where both cohorts show up.

The query that produced the table above — copy and re-run it:

WITH fo AS (                        -- which version did each player install on?
  SELECT user_pseudo_id uid, ANY_VALUE(app_info.version) install_version
  FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
  WHERE event_name = 'first_open' GROUP BY uid
),
sale AS (
  SELECT app_info.version app_version, user_pseudo_id uid, event_name,
         PARSE_DATE('%Y%m%d', event_date) day,
         COALESCE(
           (SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id'),
           (SELECT value.string_value FROM UNNEST(event_params) WHERE key='position')
         ) sku
  FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
)
SELECT s.app_version,
  CASE WHEN f.install_version IS NULL        THEN 'install version unknown'
       WHEN f.install_version = s.app_version THEN 'installed ON this version'
       ELSE 'installed on an EARLIER version' END cohort,
  COUNTIF(s.event_name='purchase_click')  tapped,
  COUNTIF(s.event_name='in_app_purchase') paid,
  COUNTIF(s.event_name='earn')            rewarded,
  COUNT(DISTINCT s.uid) users,
  MAX(s.day) last_seen
FROM sale s LEFT JOIN fo f ON f.uid = s.uid
WHERE s.sku LIKE '%.sale'
  AND s.app_version IN ('26.2','27.2','28','29')
GROUP BY app_version, cohort
ORDER BY app_version, cohort

2 · The Play storefront says the same — checked live, prices included

GA4 only ever sees the client. The other half of "is it removed" is the storefront itself, so this was pulled straight from the Play Developer API (androidpublisher v3, monetization.onetimeproducts.list) rather than read off a dashboard. All 95 products came back, 16,530 product×region price rows, and every one of the three .sale packs is ACTIVE and AVAILABLE with its price unchanged:

Product IDPurchase optionUS price StateUS availabilityRegions available
skip.ad.30days.salebuy-no-ads-30-days-saleUSD 4.99ACTIVEAVAILABLE174
skip.ad.annual.salebuy-no-ads-annual-saleUSD 4.99ACTIVEAVAILABLE174
skip.ad.permanent.salebuy-no-ads-permanent-saleUSD 10.99ACTIVEAVAILABLE174
skip.ad.10daysbuy-no-ads-10-daysUSD 4.99ACTIVEAVAILABLE174
skip.ad.30daysbuy-no-ads-30-daysUSD 10.99ACTIVEAVAILABLE174
skip.ad.3daysbuy-no-ads-3-daysUSD 2.99ACTIVEAVAILABLE174
skip.ad.annualbuy-no-ads-annualUSD 10.99ACTIVEAVAILABLE174
skip.ad.permanentbuy-no-ads-permanentUSD 29.99ACTIVEAVAILABLE174

The pull was also diffed against the copy committed on 2026-09-16, field by field across all 16,530 rows: zero products added, zero removed, zero price changes, zero state changes. Nothing has been deactivated or unpublished on the store side.

Re-pull the live catalogue yourself:

uv run --with google-api-python-client --with google-auth --with pyyaml \\
  python dashboard/pull_play_catalogue.py \\
    --key ~/.config/play-sa.json --out schema/play_catalogue.yaml

Worth noting skip.ad.annual.sale in that table: it is ACTIVE and priced on the store, but it has never once been tapped or bought in the entire GA4 history — the client has never surfaced it. Being live on Play and being live in the game are two different things, which is exactly why both were checked.

3 · Every single sale-pack event on v28 and v29

Not a sample and not an aggregate. This is the complete list, 15 events, nothing filtered out.

When (UTC)RunningInstalled onEvent SKUUSDCountry
2026-09-16 13:25v28v28purchase_clickskip.ad.permanent.saleSwitzerland
2026-09-16 13:26v28v28earnskip.ad.permanent.saleSwitzerland
2026-09-16 13:26v28v28iap_purchase_successskip.ad.permanent.saleSwitzerland
2026-09-16 13:26v28v28first_purchaseskip.ad.permanent.saleSwitzerland
2026-09-16 13:52v28v28in_app_purchaseskip.ad.permanent.sale$10.89Switzerland
2026-09-16 19:21v28v28purchase_clickskip.ad.permanent.saleHong Kong
2026-09-16 22:40v28?in_app_purchaseskip.ad.permanent.sale$10.99United States
2026-09-17 05:17v28v28purchase_clickskip.ad.permanent.saleUnited States
2026-09-17 06:16v28v28purchase_clickskip.ad.permanent.saleSouth Korea
2026-09-17 13:35v28v28purchase_clickskip.ad.30days.saleIndonesia
2026-09-17 13:38v28v28in_app_purchaseskip.ad.permanent.sale$13.84Spain
2026-09-17 21:00v29v29in_app_purchaseskip.ad.permanent.sale$13.29Poland
2026-09-17 22:16v29v29earnskip.ad.permanent.saleAustralia
2026-09-17 22:16v29v29earnskip.ad.30days.saleAustralia
2026-09-18 10:13v29v29purchase_clickskip.ad.permanent.saleGermany

Only in_app_purchase carries a USD value; the client-side events (purchase_click, earn, iap_purchase_success, first_purchase) have no money field at all, which is why that column is blank on those rows.

The query that produced the event log above:

WITH fo AS (
  SELECT user_pseudo_id uid, ANY_VALUE(app_info.version) install_version
  FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
  WHERE event_name = 'first_open' GROUP BY uid
)
SELECT FORMAT_TIMESTAMP('%Y-%m-%d %H:%M', TIMESTAMP_MICROS(e.event_timestamp)) ts,
       e.app_info.version app_version,
       IFNULL(f.install_version,'?') install_version,
       e.event_name,
       REPLACE(COALESCE(
         (SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='product_id'),
         (SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='position')
       ),'com.idle.tiny.','') sku,
       ROUND(e.event_value_in_usd,2) usd, e.geo.country country
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*` e
LEFT JOIN fo f ON f.uid = e.user_pseudo_id
WHERE e.app_info.version IN ('27.2','28','29')
  AND COALESCE(
    (SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='product_id'),
    (SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='position')
  ) LIKE '%.sale'
ORDER BY ts

4 · The quick version to paste into BigQuery

The two queries above scan the whole export (~2.5 GiB each). This one filters on _TABLE_SUFFIX first, so it prunes to the recent shards and reads about 60 MB — cheap enough to re-run whenever you want to check.

WITH e AS (
  SELECT app_info.version app_version, event_name, user_pseudo_id,
         PARSE_DATE('%Y%m%d', event_date) day,
         COALESCE(
           (SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id'),
           (SELECT value.string_value FROM UNNEST(event_params) WHERE key='position')
         ) sku
  FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
  WHERE _TABLE_SUFFIX >= '20260916'      -- v28 launch day; widen to look further back
)
SELECT app_version, sku, event_name,
       COUNT(*) events, COUNT(DISTINCT user_pseudo_id) users, MAX(day) last_seen
FROM e
WHERE sku LIKE '%.sale'
GROUP BY app_version, sku, event_name
ORDER BY app_version, sku, event_name

5 · Two things that will bite you when you re-run these