Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Manage the monetization catalog — subscriptions and one-time products — as files with gplay `subscriptions` and `iap`; the directory is a mirror, so a live product with no file is a delete. Use when putting subscriptions or IAP under version control, editing a product's prices/offers/listings, reviewing catalog drift in CI, promoting a legacy in-app product to the v2 model, or migrating live subscribers to a new price.
.claude/skills/pollyglot-gplay-monetization/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 191% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-06 | ✗→✓ | ▲ Improved | -7% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 98% | 0% |
gplay subscriptions and gplay iap own the monetization catalog as declarative, version-controlled files (ADR-0041). Shared conventions (auth, --package pinning, output, exit codes, --dry-run/--confirm) are in gplay-cli-usage. Both namespaces are [experimental].
| Namespace | What it holds | Default --dir | |---|---|---| | subscriptions | subscriptions, their base plans (config + per-territory prices), their offers, lifecycle state | ./monetization/subscriptions | | iap | one-time products, v2 monetization.onetimeproducts ∪ legacy inappproducts | ./monetization/iap |
Both sit outside the Edit lifecycle (like compliance, device-tiers, recovery, orders), direct package-scoped writes, no editId, so gplay edits begin does not batch them.
pull → edit the .json files → apply --dry-run → apply [--confirm]One <productId>.json per product, holding the API resource verbatim minus server-derived noise. pull then apply with no edits is a guaranteed no-op. Commit the directory; the diff in review is the catalog change.
metadatametadata apply is additive: a locale live online but absent on disk is left alone. Monetization is the opposite. The directory is the complete declared catalog, a live subscription, product or offer with no file is a delete in the plan. A monetization catalog is a closed set whose omissions must be visible; a store listing tree is a partial view.
Consequences to internalize before running anything:
--dir at a fresh/empty directory. apply refuses when thedirectory holds no .json while the app has live products (it would delete them all), but a partially populated directory is a legitimate plan full of deletes. Always pull first.
pull is destructive locally too. It removes stale .json files so thedirectory mirrors Play. It refuses to erase a populated directory when the live catalog reads back empty (a mis-set --package or a scope loss); that refusal is a signal, not an obstacle to work around. Non-.json files are never touched.
to delete one with a published base plan.
| Situation | Gate | Exit without it | |---|---|---| | Plan contains any delete (product or offer) | --confirm | 3 | | iap apply promotes a live legacy product to v2 | --migrate | 3 | | subscriptions prices migrate (reprices live subscribers) | --confirm | 3 | | Creates, patches, state changes | none, they run directly | n/a |
CI=true never auto-confirms. GPLAY_READONLY refuses every apply and migrate outright (exit 4, not resolvable by adding a flag).
State changes are not gated (activate/deactivate are reversible), but they are listed prominently in every plan view because they move buyer availability.
bashgplay subscriptions pull # → ./monetization/subscriptions/*.json # …edit the files… gplay subscriptions apply --dry-run # ONLINE read, prints the plan, changes nothing gplay subscriptions apply # creates/patches/state changes gplay subscriptions apply --confirm # …when the plan also deletes
per-territory regionalConfigs prices) is declared inline under basePlans and patched with the subscription; the API has no create/patch on the sub-resource. Its endpoints only manage state and subscriber price migration.
pull nests each offer underbasePlans[].offers, a file construct the API resource does not carry. apply splits them back out and reconciles them through the offers endpoints under the key productId/basePlanId/offerId. Don't expect that array in an API response.
state: declares lifecycle, reconciled via :activate/:deactivate,never a patch. Declare ACTIVE or INACTIVE. Omitting the field leaves state unmanaged, the metadata stance. An unreachable transition (DRAFT from anything, INACTIVE from DRAFT) is a usage error naming it.
listings, taxAndComplianceSettings,restrictedPaymentCountries, basePlans. The updateMask is exactly the changed managed fields; nothing outside that projection drifts or diffs. archived is not reconciled (deprecated/output-only upstream).
bash# Derive per-region prices from one base price, a computation, no write: gplay subscriptions prices convert --price 4.99 --currency USD --output json
Paste the returned Money objects into a base plan's regionalConfigs, then rehearse with apply --dry-run. --output json is the ConvertRegionPricesResponse verbatim. "Not a write" does not mean offline: convert calls the convertRegionPrices API (today's exchange rates), so it needs a credential and the package axis; it just never mutates anything.
bash# Reprice EXISTING subscribers, money-moving, one base plan per call: gplay subscriptions prices migrate \ --product premium --base-plan monthly \ --region FR --region DE \ --oldest 2026-01-01T00:00:00Z \ --price-increase-type opt-in \ --dry-run # offline preview, lists the gate in "requires" gplay subscriptions prices migrate … --confirm
This is the one deliberate exception to "editing files never touches a live purchaser." apply changes what new buyers pay; migrate changes what existing subscribers pay. An apply diff never triggers a migration; that separation is pinned by a test upstream, so don't expect a price edit to propagate to current subscribers.
--oldest (RFC-3339) migrate, scoped to the--regions you repeat.
--price-increase-type opt-in requires subscribers to accept or churn;opt-out (where Google allows it) applies automatically with notice.
base plan per invocation.
iap)bashgplay iap pull # v2 ∪ legacy → ./monetization/iap/*.json gplay iap apply --dry-run gplay iap apply [--confirm] [--migrate]
pull unions the v2 and legacy surfaces by product ID, and a file's origin is its shape; no gplay-invented marker:
| Field present | Model | |---|---| | sku | legacy inappproducts | | productId | v2 onetimeproducts |
Legacy is inert: gplay never creates, edits or deletes a legacy product, the only gesture is the one-way promotion to v2 (rewrite the file with productId and apply with --migrate; rehearse with --dry-run first). When a legacy file is involved (an unexpected refusal, a promotion to plan, or the question of why pull reads two surfaces), read iap-legacy.md.
--regions-versioncreate/patch require Google's regions version string. gplay pins the current published value (2022/02) and exposes --regions-version to override when Google publishes a new one, a flag, not a config knob, so the pin stays visible in CI logs.
bashgplay subscriptions apply --dry-run --output json # the plan, a gplay-owned shape gplay iap apply --dry-run --output json
apply --output json emits the plan, not an API echo, a recorded ADR-0003 exception, like metadata apply, [experimental] until it graduates: {package, dryRun, changes[], summary{…}, requires[]}, where each change carries op (create/patch/delete/activate/deactivate, plus migrate on iap) and its identity. pull --output json is the API pass-through (the merged ListSubscriptionsResponse, or the composite {"oneTimeProducts":[…],"inappproduct":[…]}), but the files are the real output there.
A drift check is one line, fail the job when the plan is non-empty:
bashgplay subscriptions apply --dry-run --output json | jq -e '.changes | length == 0'
The Discovery snapshot ties no specific Play permission enum to the monetization methods, so gplay's 403 hint points at the surface rather than naming a capability: grant the service account access to the app's monetization setup in Play Console (Users & permissions), then retry. 403 → exit 11, 404 on the package → exit 30 (verify --package or the pin).
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 6,049 | 37,212 | +515% | 1 | 1 | 0% | 1,190 | 3,465 | +191% | 0 | 0 | — |
case-02 | fail→pass | 15,744 | 8,441 | -46% | 1 | 1 | 0% | 2,441 | 3,695 | +51% | 0 | 0 | — |
case-03 | fail→pass | 9,613 | 5,271 | -45% | 1 | 1 | 0% | 1,817 | 3,127 | +72% | 0 | 0 | — |
case-04 | pass→pass | 10,944 | 5,292 | -52% | 1 | 1 | 0% | 1,498 | 3,081 | +106% | 0 | 0 | — |
case-05 | pass→pass | 13,793 | 6,581 | -52% | 1 | 1 | 0% | 2,337 | 3,190 | +36% | 0 | 0 | — |
case-06 | fail→pass | 19,642 | 5,122 | -74% | 1 | 1 | 0% | 3,329 | 3,111 | -7% | 0 | 0 | — |
case-07 | fail→pass | 9,372 | 2,632 | -72% | 1 | 1 | 0% | 1,331 | 2,633 | +98% | 0 | 0 | — |
case-08 | fail→pass | 8,007 | 2,811 | -65% | 1 | 1 | 0% | 1,151 | 2,682 | +133% | 0 | 0 | — |
case-09 | fail→pass | 13,156 | 4,148 | -68% | 1 | 1 | 0% | 1,946 | 2,922 | +50% | 0 | 0 | — |
case-10 | fail→pass | 18,056 | 2,998 | -83% | 1 | 1 | 0% | 2,806 | 2,680 | -4% | 0 | 0 | — |
case-11 | fail→pass | 15,400 | 2,956 | -81% | 1 | 1 | 0% | 2,474 | 2,698 | +9% | 0 | 0 | — |
case-12 | fail→pass | 15,179 | 6,579 | -57% | 1 | 1 | 0% | 2,473 | 3,314 | +34% | 0 | 0 | — |
case-13 | fail→pass | 15,691 | 3,678 | -77% | 1 | 1 | 0% | 2,599 | 2,853 | +10% | 0 | 0 | — |
case-14 | fail→pass | 16,131 | 3,127 | -81% | 1 | 1 | 0% | 2,664 | 2,714 | +2% | 0 | 0 | — |
case-15 | fail→pass | 13,293 | 3,075 | -77% | 1 | 1 | 0% | 1,926 | 2,664 | +38% | 0 | 0 | — |
case-16 | fail→pass | 13,893 | 5,731 | -59% | 1 | 1 | 0% | 2,165 | 3,158 | +46% | 0 | 0 | — |
case-17 | pass→pass | 8,859 | 4,910 | -45% | 1 | 1 | 0% | 1,466 | 2,837 | +94% | 0 | 0 | — |
case-18 | fail→pass | 11,189 | 2,541 | -77% | 1 | 1 | 0% | 1,750 | 2,638 | +51% | 0 | 0 | — |
case-19 | pass→pass | 12,362 | 4,524 | -63% | 1 | 1 | 0% | 1,747 | 2,853 | +63% | 0 | 0 | — |
case-20 | fail→pass | 12,110 | 5,581 | -54% | 1 | 1 | 0% | 1,771 | 3,097 | +75% | 0 | 0 | — |
case-21 | fail→pass | 11,672 | 3,394 | -71% | 1 | 1 | 0% | 1,914 | 2,790 | +46% | 0 | 0 | — |
case-22 | fail→pass | 10,923 | 1,724 | -84% | 1 | 1 | 0% | 1,563 | 2,422 | +55% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +82 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/4/2026 | +77% |
Other measured skills in the registry, with their headline benchmark lift.