agentsclimarketplace

Unity monetization

Skill tea-x-random/unity-game-skills/skills/unity-monetization

Claude Agent Skills for building casual iOS games in Unity 6 — orchestration, MCP Editor control, generative 2D/3D/audio assets, graphics, UI, monetization, QA & release.

Install
npx -y skills add tea-x-random/unity-game-skills --skill unity-monetization

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Integrate ads and monetization into Unity 6 iOS games. Use for Google AdMob/GoogleMobileAds, Unity Ads, interstitials, rewarded ads, test units/TestFlight, frequency caps and cadence policies, natural-break placements, SDK-agnostic facades, LevelPlay/AppLovin MAX mediation decisions, OpenUPM, asmdef/DLL references, EDM4U/CocoaPods, cloudProjectId, ATT/NSUserTrackingUsageDescription, SKAdNetworkItems, non-personalized ads, pending-approval/no-fill, or INTERNAL_ERROR setup failures. Covers initialization, load/show callbacks, main-thread marshalling, per-platform IDs, post-build plist work and unit-testable count-or-time ad policy. Pair with analytics/liveops to tune retention and ARPDAU.

SKILL.md

23.7 KB, as published. Nobody here has run it

Unity Monetization

Wire ads into a casual iOS game without breaking the build or lying about whether ads serve. The deliverable is an AdsManager that no-ops safely off-device (behind an SDK-agnostic Ads facade), WHEN-to-show factored into a pure unit-tested cadence policy, and a build that carries whatever provisioning the SDK needs — plus an honest statement of what is provisioned vs coded. Built on unity-mcp-bridge (Editor actions) and cross-linked to unity-qa-release (the CocoaPods / pod-install / .xcworkspace build mechanics).

The path we ship now is Google AdMob (com.google.ads.mobile via OpenUPM) — see "Google AdMob (iOS)" below. The Unity Ads direct material that follows is still proven and kept as the alternative path (and as the source of the INTERNAL_ERROR / LevelPlay-conflict lore).

Doctrine (these override convenience)

  1. "Code compiles" ≠ "ads serve." Never claim ads work without seeing one render on-device. A clean build, a valid Game ID, and green listeners prove nothing about delivery. The only proof is an ad frame on a real device (or TestFlight). Until then say "integration wired, delivery unverified."
  2. A brand-new ad account/app/unit is almost certainly NOT serving real ads yet. Freshly created Unity Ads / AppLovin / ironSource accounts sit in "pending approval". AdMob is the same in a sharper form: a new AdMob app/unit returns no-fill for real ads until Google approves the app (often only once it's live / linked to a store listing) and the unit warms up (hours to a couple days). Flipping UseTestAds true→false too early makes ads disappear — that's expected, not a bug. Google test unit IDs always serve regardless of approval. When a valid ID yields no ad, suspect provisioning/no-fill first, not C#. Code must handle no-fill gracefully (log + retry next opportunity, never block gameplay).
  3. Pick ONE ad path. Mixing leaves conflicting CocoaPods. We evaluated three in one project; friction order was LevelPlay (com.unity.services.levelplay, mediation, account pending) → AppLovin MAX (.unitypackage, account pending) → Unity Ads direct (com.unity.ads), which is the path we shipped on. Two SDKs pulling the same pod at different versions breaks pod install (see the LevelPlay trap below).
  4. The iOS Xcode project is REGENERATED every build. Anything you'd hand-edit in Xcode (Info.plist ATT/SKAdNetwork keys, entitlements) must be re-applied by a [PostProcessBuild] editor script, or it's gone next build. Never hand-edit the generated project.
  5. Bootstrap ads on device only, and no-op until configured. The manager must do nothing in the Editor / off-device and nothing until a real Game ID + placement exist, so the rest of the game (and tests) never depends on an ad SDK being present or reachable.
  6. Gameplay never references the ad SDK — go through a tiny Ads facade. A static facade (Ads.NotifyGameFinished()Ads.GameFinishedHandler?.Invoke()) that the concrete AdsManager registers at launch keeps gameplay assemblies SDK-free and makes swapping ad networks a one-file change. Off-device the handler is null → no-op. (The reference project swapped Unity Ads → AdMob by rewriting only AdsManager.cs.)
  7. WHEN to show is a pure, unit-tested policy — not logic buried in the MonoBehaviour. Put frequency capping in an engine-free class with injected time so it's deterministic and testable; the MonoBehaviour owns the real clock + SDK. Separate the decision from the commit (MarkShown only on real display) so a not-yet-loaded ad doesn't reset the counters.

Confirm the Editor is reachable (mcpforunity://editor/state, ready_for_tools==true) before driving any of this through MCP; for the CocoaPods build itself, route to unity-qa-release.

2025-2026 casual monetization strategy (hybrid by default)

The single-network mechanics below are still how you wire ads — but the strategy they serve has consolidated. For casual / hybrid-casual on iOS, hybrid monetization (IAP + ads) is now the default, not an upsell: ~72%+ of developers run both, because only ~1.8% of F2P players ever make an IAP — so ads monetize the ~98% who never pay, while IAP (including a "remove ads" purchase) monetizes the rest. A typical mature mix lands around ~40-50% IAP / ~50-60% ads (game growth advisor, gamigion hybrid-casual overview). Plan for both revenue streams from the start; a pure-ads or pure-IAP casual game is leaving most of the table.

  • Rewarded video is the core casual ad format — and the highest-eCPM format (Rewarded > Interstitial > others). Design natural rewarded placements players opt into: continue/revive, double-coins, free-spin (TopOn 2025 H1 report). The injectable-time cadence policy below generalizes to rewarded triggers too.
  • More ads ≠ more revenue. Over-serving hurts both retention and eCPM, so don't chase a fixed cadence number — tune frequency by measuring ARPDAU and retention, not by a hardcoded cadence (tap-nation KPIs). Cross-reference unity-analytics-liveops for ARPDAU, retention funnels, and remote-config-driven ad-cadence A/B tests.
  • Mediation reality (context for the LevelPlay vs MAX vs direct decision below). AppLovin MAX dominates ad mediation — ~73% of top-downloaded games and ~55% of top-grossing; LevelPlay/ironSource is more competitive on grossing (~25%) than on downloads; AdMob sits ~11-13% (gamebiz consulting newsletter). This doesn't override our friction-ordered pick (we shipped Unity Ads direct, AdMob now) — it's the market backdrop: MAX is where most of the ecosystem optimizes fill/eCPM.
  • ATT shifted ad revenue toward Android; measure iOS without IDFA. Post-ATT, iOS is now ~43% of mobile-game ad revenue vs Android ~57%, and iOS attribution must rely on SKAdNetwork / AdAttributionKit, not IDFA (Tenjin 2026 ad-mon report). This is why the non-personalized / no-ATT posture below is viable, and why the SKAdNetwork plist work is load-bearing rather than optional.

Google AdMob (iOS) — the path we ship now

GMA com.google.ads.mobile 11.2.0 (Unity 6000.5.0f1, built-in pipeline). Full annotated integration in references/admob-ios.md; the load-bearing gotchas:

  • Install via OpenUPM. Add a scoped registry package.openupm.com (https://package.openupm.com) with scopes com.google.ads.mobile + com.google.external-dependency-manager, then add "com.google.ads.mobile": "11.2.0" and "com.google.external-dependency-manager": "1.2.187" to dependencies. Resolution needs the Editor focused to process the manifest change and can drop/timeout the MCP bridge — reconnect by clicking the Editor.
  • The GMA runtime is PRECOMPILED DLLs, not an asmdef. GoogleMobileAds.dll, .Core.dll, .Common.dll, etc.; the only asmdef is GoogleMobileAds.Editor (editor code). So you cannot put "GoogleMobileAds" in your asmdef's references array (that's for other asmdefs — it warns "unresolved reference"). With overrideReferences:false (the default) the auto-referenced DLLs resolve on their own — the fix is to remove the bad reference, and using GoogleMobileAds.Api; compiles. (YourGame.Game.asmdef references only YourGame.Core + Unity.TextMeshPro.)
  • Marshal v11 callbacks yourself. MobileAds.RaiseAdEventsOnUnityMainThread is obsolete in v11 (works, but warns). Instead wrap each callback body — Load completion, OnAdFullScreenContentClosed, OnAdFullScreenContentFailed (all arrive on a background thread) — in GoogleMobileAds.Common.MobileAdsEventExecutor.ExecuteInUpdate(() => { ... }). The executor is initialized during MobileAds.Initialize, so marshal inside the Initialize callback too.
  • App ID + ATT string via the settings asset; GMA writes the plist. Set GoogleMobileAdsIOSAppId (serialized adMobIOSAppId, e.g. ca-app-pub-XXXX~YYYY) in Assets/GoogleMobileAds/Resources/GoogleMobileAdsSettings.asset (a GoogleMobileAds.Editor.GoogleMobileAdsSettings ScriptableObject). GMA's iOS post-processor then writes GADApplicationIdentifier, NSUserTrackingUsageDescription (only if set), and ~50 SKAdNetworkItems into the built Info.plist — your own [PostProcessBuild] must NOT touch those keys (no collision; contrast Unity Ads, where you write SKAdNetwork yourself). There's no LoadInstance(); create the asset by reflection (ScriptableObject.CreateInstance(type)AssetDatabase.CreateAsset → set props → save).
  • Non-personalized ads = simplest privacy posture. var req = new AdRequest(); req.Extras.Add("npa", "1"); Do NOT request ATT; clear UserTrackingUsageDescription so NSUserTrackingUsageDescription isn't written (the reference project also strips it defensively in PostBuild.cs). No ATT prompt → declare "no tracking" in the privacy manifest + App Privacy questionnaire. Most users decline ATT anyway, so personalization uplift is small for a casual game and the review path is much simpler.
  • iOS pods via EDM4U. GoogleMobileAdsDependencies.xmlGoogle-Mobile-Ads-SDK ~> 13.4; GoogleUmpDependencies.xmlGoogleUserMessagingPlatform 3.1.0. EDM4U writes both into Build/iOS/Podfile; the env-stripped pod install + .xcworkspace build lives in unity-qa-release › fastlane/CocoaPods.
  • No-fill is normal pre-launch — see Doctrine 2. Keep UseTestAds=true (or register an AdMob test device) for TestFlight QA; Google's iOS test interstitial is ca-app-pub-3940256099942544/4411468910.

Shipped files: Assets/<YourGame>/Scripts/Game/Net/Ads.cs (facade), .../Net/AdsManager.cs (AdMob backend, added by AppRoot), Assets/<YourGame>/Scripts/Core/AdCadence.cs (cadence) + Tests/EditMode/AdCadenceTests.cs.

Ad cadence as a pure, testable policy

Factor WHEN-to-show out of the SDK shell into an engine-free policy class (YourGame.Core/AdCadence.cs, 19 green EditMode tests). Full detail in references/ad-cadence-policy.md:

  • Dual OR trigger: show when (games-since-last-ad ≥ N) OR (seconds-since-last-ad-or-app-open ≥ T) — count monetizes quick back-to-back games, time monetizes one long session. Example values: GamesPerAd=2, MinSessionGapSeconds=300 (inclusive boundary). These are starting points, not gospel: the right cadence is the one your ARPDAU-vs-retention data supports, tuned via remote-config A/B — not a hardcoded number (see unity-analytics-liveops).
  • Injected time: the policy takes now (seconds) as an argument; the MonoBehaviour supplies Time.realtimeSinceStartupAsDouble. Fully deterministic → plain unit tests, no Play Mode / SDK.
  • Decision vs commit: RegisterGameFinishedAndShouldShow(now) decides; MarkShown(now) resets both triggers and is called only when an ad actually displays. If the ad isn't loaded you skip MarkShown, so the "owed ad" shows as soon as one is ready (counter keeps climbing). MarkAppOpen(now) resets only the time baseline. Promote this "pure injectable-time policy + thin SDK shell" as the default for frequency capping — it generalizes to rewarded/banner cadence.

Alternative path: Unity Ads direct (com.unity.ads)

  • Package: com.unity.ads 4.17.0 — "Advertisement Legacy" in Package Manager. Namespace UnityEngine.Advertisements. The asmdef that calls it must reference UnityEngine.Advertisements.
  • API (4.17): Advertisement.Initialize(gameId, testMode, IUnityAdsInitializationListener) → on OnInitializationComplete, Advertisement.Load(adUnitId, IUnityAdsLoadListener) → on OnUnityAdsAdLoaded, Advertisement.Show(adUnitId, IUnityAdsShowListener). One class can implement all three listener interfaces.
  • Game IDs are per-platform. Separate iOS and Android Game IDs from the dashboard. testMode=true serves test ads and works on TestFlight — keep it true until launch so you're not serving/clicking live ads pre-release.
  • There is NO manual bundle-id field. The SDK reports the bundle id at runtime; you don't paste it anywhere. Don't go looking for a place to set it.
  • Unity Ads 4.17 ships a CocoaPod. The iOS SDK comes as the UnityAds CocoaPod via EDM4U — so adding this package turns the build into a CocoaPods build (Podfile + pod install + build the .xcworkspace, not the bare .xcodeproj). It is NOT pod-free. The pod-install/xcworkspace specifics (env-stripped pod install to dodge Gem::MissingSpecError, building the workspace in gym) live in unity-qa-release › fastlane/CocoaPods. (An early code comment claimed "no CocoaPods, SDK ships inside the package" — that was wrong for 4.17; trust the build, not the comment.)

See references/unity-ads-direct.md for the full annotated AdsManager listener lifecycle.

The INTERNAL_ERROR trap — it's project setup, not code

OnUnityAdsFailedToLoad(..., UnityAdsLoadError.INTERNAL_ERROR, ...) with a valid Game ID and a real placement is almost always project association, not a code bug. The Unity project must be linked to the cloud project that owns the Game ID, and Unity Ads must be enabled at the project level so the build ships the association. Three things to verify:

  1. AdvertisementSettings.enabled == true + SetGameId(RuntimePlatform.IPhonePlayer, id) (the dashboard binding). In ProjectSettings/UnityConnectSettings.asset this is UnityAdsSettings: m_Enabled: 1, m_GameIds: { iPhonePlayer: <id> }, m_GameId: <id>. A build that ships with m_Enabled: 0 / no Game ID is a prime INTERNAL_ERROR cause — that exact gap was the suspected culprit in a real debugging session, and the fix was to flip m_Enabled to 1 and write the iOS Game ID.
  2. cloudProjectId must actually be baked into the build. The Editor can show the project "bound" while cloudProjectId in ProjectSettings/ProjectSettings.asset on disk is empty (it was blank here). Editor-bound ≠ baked. Verify it's non-empty on disk / in the build, not just green in the UI — otherwise the build has no link to the cloud project that owns the Game ID.
  3. Confirm the dashboard project itself is provisioned (Doctrine 2): placement exists, project not pending. INTERNAL_ERROR on a brand-new project often just means "not provisioned yet."

Symptom signature: valid IDs, clean compile, listeners firing, yet every load fails INTERNAL_ERROR on device → walk this list before touching C#.

The LevelPlay / ironSource pod conflict (remove it fully)

If you ever started down LevelPlay, its leftovers actively sabotage Unity Ads direct. Assets/LevelPlay/ ships an ironSource Dependencies.xml that EDM4U reads to inject IronSourceUnityAdsAdapterUnityAds 4.18.1, which collides with com.unity.ads' UnityAds ~> 4.17.0. Two pin versions for the same pod → pod install breaks → no build. Going direct means removing it completely:

  • Remove com.unity.services.levelplay from Packages/manifest.json and the Packages/packages-lock.json entry.
  • Delete the Assets/LevelPlay/ folder (its Dependencies.xml is the actual pod injector).
  • Watch for residue that "creeps back": a LEVELPLAY_DEPENDENCIES_INSTALLED scripting define on the iPhone target and cloudServicesEnabled flags — scrub these so the generated Podfile is just UnityAds.

After removal, regenerate the build and confirm the Podfile contains only the Unity Ads pod before pod install.

Integration patterns

SDK-free gameplay via the Ads facade; the manager no-ops off-device. Gameplay calls a tiny static Ads.NotifyGameFinished()Ads.GameFinishedHandler?.Invoke(); the concrete AdsManager registers the handler at launch and holds the SDK. Off-device / before init the handler is null → safe no-op, and no gameplay code references the ad SDK. The manager is created device-side — the AdMob AdsManager is added as a component by AppRoot (the Unity Ads variant used [RuntimeInitializeOnLoadMethod(AfterSceneLoad)] guarded on platform + a non-placeholder Game ID). Hold one preloaded interstitial and preload the next on close/fail.

Frequency-capped interstitials at a natural break, decided by the cadence policy. Delegate WHEN-to-show to AdCadence (the dual count-OR-time trigger above; example GamesPerAd=2, MinSessionGapSeconds=300), counting finishes even when no ad is ready so the owed ad shows next time. Trigger on the replay tap, not on the result screen itself — so the ad never covers the win/lose score or status UI — and never on launch or mid-game. For example, have the result/replay flow call your ads facade's NotifyGameFinished() only when replaying after a finished game, never from the cold-start menu. Casual retention is fragile; under-show rather than over-show.

iOS ATT + SKAdNetwork via PostProcessBuild (Unity Ads path) — but AdMob writes them for you. On the AdMob path do NOT hand-write these: GMA's own iOS post-processor writes GADApplicationIdentifier, NSUserTrackingUsageDescription (only when set), and ~50 SKAdNetworkItems from the settings asset, so your [PostProcessBuild] must leave those keys alone (the reference project's only removes NSUserTrackingUsageDescription for the no-ATT posture). The rest of this bullet applies to Unity Ads direct, where you own those keys. Re-apply the Info.plist keys every build from a [PostProcessBuild] script (this project's is Assets/<YourGame>/Editor/PostBuild.cs), wrapped in #if UNITY_IOS so it only compiles with the iOS module + iOS target active:

  • NSUserTrackingUsageDescription (Info.plist string) — required copy for the ATT prompt; present the prompt before any IDFA-based tracking.
  • SKAdNetworkItems (Info.plist array of SKAdNetworkIdentifier dicts) — the ad network's SKAdNetwork IDs for attribution; get the current list from the network (Unity Ads / your mediator) and write each as a dict entry.
  • Inject both with UnityEditor.iOS.Xcode.PlistDocument reading/writing Info.plist in the post-build callback — same mechanism the existing script uses for ITSAppUsesNonExemptEncryption. Don't hand-edit Xcode; it won't survive the next build.

The privacy-manifest (PrivacyInfo.xcprivacy) side — required-reason APIs, IDFA declaration when you actually track — is owned by unity-qa-release › App Store readiness; ads make the IDFA/tracking declarations non-trivial, so revisit that checklist once a real ad path is live.

See references/ios-att-skadnetwork.md for the PostProcessBuild Info.plist snippet.

On-device diagnosis (ads fail only on device)

Ad SDKs fail where you can't attach a debugger — on the device, below the safe area. Surface the SDK lifecycle (init → load → show, plus the exact error+message) into the on-device debug HUD / OnGUI pattern from unity-mcp-bridge. This project's AdsManager.Status string carries "init ok — loading" / "LOAD FAIL: INTERNAL_ERROR …" / "shown (COMPLETED)" etc. straight to the HUD, which is how you tell a provisioning failure (INTERNAL_ERROR) from a code path that never ran (status stuck at "not started" → bootstrap guard skipped it).

Final response format

Lead with integration wired vs delivery verified — keep them separate. Then:

  • Wired (with evidence): package added; the SDK reference resolves (asmdef reference for Unity Ads / removed bad reference for AdMob's auto-referenced DLLs); init→load→show implemented and main-thread-marshalled; App ID / Game ID set; cadence policy + EditMode tests green; Ads facade in place. AdMob: App ID in GoogleMobileAdsSettings.asset, npa=1, GMA post-processor owns the plist keys. Unity Ads: project-level enable + Game ID baked, cloudProjectId non-empty on disk, LevelPlay residue gone, ATT/SKAdNetwork post-build present.
  • Pending / unverified: account/app/unit approval + warm-up status (AdMob no-fill is normal pre-launch), whether an ad actually rendered on-device, CocoaPods build (route to unity-qa-release).
  • Never write "ads work" without an on-device ad frame. "App ID set / clean compile" is wiring, not delivery — and on a fresh AdMob unit, only test ads will render until approved + warmed up.

Field notes & lessons

  • Switched the shipped path from Unity Ads to Google AdMob (com.google.ads.mobile 11.2.0 + com.google.external-dependency-manager 1.2.187 via the OpenUPM scoped registry); clean compile + archived/uploaded TestFlight build. New references/admob-ios.md and references/ad-cadence-policy.md. Key proven facts: GMA runtime ships as precompiled DLLs (only GoogleMobileAds.Editor.asmdef exists) → you cannot list "GoogleMobileAds" in an asmdef's references; remove it and the auto-referenced DLLs resolve (overrideReferences:false). MobileAds.RaiseAdEventsOnUnityMainThread is obsolete in v11 → marshal each background-thread callback with MobileAdsEventExecutor.ExecuteInUpdate. App ID (GoogleMobileAdsIOSAppId/adMobIOSAppId) set in GoogleMobileAdsSettings.asset (created via reflection; no LoadInstance()); GMA's iOS post-processor writes GADApplicationIdentifier + NSUserTrackingUsageDescription + ~50 SKAdNetworkItems so your own PostProcessBuild must not. Pods via EDM4U: Google-Mobile-Ads-SDK ~> 13.4 + GoogleUserMessagingPlatform 3.1.0. Non-personalized posture: AdRequest + npa=1, no ATT, cleared UserTrackingUsageDescription (the reference project strips NSUserTrackingUsageDescription defensively). New AdMob app/unit = no-fill until approved + warmed up (test unit IDs always serve; ca-app-pub-3940256099942544/4411468910). Added the SDK-agnostic Ads facade (gameplay never references the SDK; network swap = one file) and a pure, injectable-time AdCadence policy (Core assembly) — dual count-OR-time trigger, decision-vs-commit MarkShown-on-display + owed-ad, 19 green EditMode tests (AdCadenceTests). Files: Game/Net/Ads.cs, Game/Net/AdsManager.cs, Core/AdCadence.cs; AdsManager added by AppRoot, triggered from ResultOverlay via Ads.NotifyGameFinished().
  • Initial skill. Unity Ads direct (com.unity.ads 4.17, UnityEngine.Advertisements); per-platform Game IDs; testMode test ads on TestFlight; 4.17 ships a UnityAds CocoaPod via EDM4U (build pipeline → unity-qa-release). INTERNAL_ERROR = project setup (AdvertisementSettings.enabled + SetGameId + cloudProjectId baked, dashboard provisioned), not code (commits d6c507c, 163b3ba). LevelPlay/ironSource pod conflict (UnityAds 4.18.1 vs 4.17) — remove fully (9f8ee7d). Frequency-capped interstitial on replay, device-only no-op manager (d57d7f6, 202e2e2). ATT + SKAdNetwork re-applied via [PostProcessBuild] because the iOS project regenerates each build. Doctrine: never claim ads serve without an on-device render; new ad accounts sit pending.

Gives 0 of the 12 instructions most analytics metrics skills give

Counted across 368 of the 369 authors here whose files we hold, read 2026-08-06

  • read product marketing context before asking questionsin 18 of 368, across 12 files
  • use lowercase with underscores for event namesin 16 of 368, across 6 files
  • track events for decisions not vanity metricsin 15 of 368, across 5 files
  • use object-action format for event namesin 15 of 368, across 8 files
  • produce a tracking plan documentin 14 of 368, across 4 files
  • Call RUBE_SEARCH_TOOLS first to get current schemasin 13 of 368, across 2 files
  • establish consistent event naming conventions before implementingin 10 of 368, across 4 files
  • Verify dimension and metric compatibility before reportingin 9 of 368, across 2 files
  • Encrypt data at rest and in transitin 9 of 368, across 3 files
  • use snake_case for event namesin 9 of 368, across 5 files
  • monitor technical health during the testin 9 of 368, across 5 files
  • use consistent property namesin 8 of 368, across 4 files

Said here and by no other author read

  • verify ad rendering on a real device before delivery
  • report integration as delivery unverified until rendered
  • state provisioning versus coding status honestly
  • suspect no-fill before the code for missing ads
  • handle no-fill gracefully without blocking gameplay
  • use only one ad network SDK to avoid conflicting dependencies

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.