| name | ppp-pricing |
| description | Set per-territory PPP-adjusted subscription prices in App Store Connect and Google Play via the `ppp-pricing` toolkit (`set_ppp_pricing.py`). Covers credential setup, dry-run/apply workflow, snapshot/restore, and Apple's commitment-band gotcha. TRIGGER when a user is configuring or adjusting subscription prices for an iOS/Android app and wants per-country pricing (PPP, regional discounts, "make India cheaper"); when they mention App Store Connect `subscriptionPrices`, Play Console `regionalConfigs`, `convertRegionPrices`; when they hit `INVALID_PRICE_TOO_HIGH` or "Monthly with 12-Month Commitment" issues; or when a project contains a `pricing.json` matching the toolkit's schema. SKIP when the user is pricing one-shot IAPs (consumables/non-consumables — different APIs); pricing on stores other than App Store Connect / Google Play; doing pure FX conversion without PPP intent; or just asking conceptual questions about PPP without an app to apply prices to. |
ppp-pricing
This skill drives the ppp-pricing toolkit: a Python script that sets PPP-adjusted subscription prices across App Store Connect and Google Play in one shot. It pairs with the /ppp-pricing slash command, which walks the same workflow interactively.
The canonical reference is the toolkit's README.md. This file is a routing layer — load the README for any topic not summarized here.
When to use this skill
Use this skill the moment a user is doing one of:
- Setting per-country subscription prices for the first time on an iOS or Android app.
- Adjusting an existing PPP price table (e.g. tuning India down further, lifting Switzerland).
- Debugging a price-apply that failed with
INVALID_PRICE_TOO_HIGH, lower than the required minimum, or out_of_range.
- Restoring prices from a snapshot after a bad apply.
- Adding a new subscription product to an existing PPP setup.
Do NOT use this skill for one-shot in-app purchases — those use a different App Store Connect endpoint and a different Play Console schema. The toolkit is subscriptions-only.
Locate the toolkit
The script lives in a cloned ppp-pricing repo. Look for it in the current project, in sibling directories, or in common dev-folder locations (~/code, ~/src, ~/dev). If you cannot find it, ask the user where they cloned it. Treat the absolute path as <TOOLKIT> for the rest of the run.
Required setup (one-time)
- App Store Connect API key —
.p8 file, key ID, issuer ID. Generate in App Store Connect → Users and Access → Integrations → App Store Connect API.
- Google Play service account JSON — generate in Play Console → Setup → API access; grant the service account
Manage orders and subscriptions + View financial data on the relevant app.
- Wire credentials via either env vars or
~/.config/ppp-pricing/credentials.json. See README → "Setup" for the exact keys.
- Python 3.10+ with
pip install -r <TOOLKIT>/requirements.txt.
pricing.json in the project root — copy <TOOLKIT>/pricing.example.json, fill in ios_bundle_id, android_package, and one entry per subscription product (with iOS subscription ID, Android product + base plan ID, and base_usd).
Subscription products must already exist in both stores. The script sets prices, it does not create products.
The workflow
Always follow this order. Skipping steps loses safety.
1. Dry-run
python <TOOLKIT>/set_ppp_pricing.py --config <pricing.json>
Prints a per-country preview table per product. Read it. Sanity-check that the base territory matches base_usd, that low-PPP markets are visibly cheaper, that charm rounding looks normal, and that there are no $0.10 or 5× outliers.
2. Apply iOS first
python <TOOLKIT>/set_ppp_pricing.py --config <pricing.json> --platform ios --apply
iOS is the smaller blast radius and the more likely failure mode. Run it first. A pre-apply snapshot is written automatically to ~/.local/share/ppp-pricing/snapshots/<bundle_id>/ios_<timestamp>.json.
3. Apply Android
python <TOOLKIT>/set_ppp_pricing.py --config <pricing.json> --platform android --apply
One subscriptions.patch per product. Snapshot also automatic.
4. Diff for spot-check
python <TOOLKIT>/diff_ios_prices.py --config <pricing.json> --snapshot <ios-snapshot>.json
python <TOOLKIT>/diff_android_prices.py --config <pricing.json> --snapshot <android-snapshot>.json
Surface the diff to the user. Confirm 2–3 markets manually in the consoles (US, IN, BR is a good sample).
5. Restore (if needed)
python <TOOLKIT>/set_ppp_pricing.py --restore <snapshot.json>
Auto-detects platform from the snapshot's contents.
Sequencing rule: weekly before yearly
The script processes products in the order they appear in pricing.json, with a 60s sleep between. Apple validates yearly prices against the weekly via a hidden ~8×–12× ratio per territory. Put weekly (or your cheapest product) first so its price propagates before yearly's ratio check runs.
The Apple commitment-band gotcha
If --apply fails on yearly territories with INVALID_PRICE_TOO_HIGH, the cause is almost always the "Monthly with 12-Month Commitment" pricing model on the yearly product in App Store Connect. Apple auto-derives a monthly slot price and locks the yearly into a band; PPP-discounted yearly targets in low-PPP markets (India, Brazil, Turkey) fall below that band.
Fix: in App Store Connect, change the yearly subscription's pricing model to "Annual upfront only". Apple won't let you change the model on a live SKU — a fresh product may be required.
Apple's error names are inverted from natural reading:
| Apple says | Reality |
|---|
INVALID_PRICE_TOO_HIGH / exceeds the allowed commitment | Price is below Apple's floor |
lower than the required minimum / TOO_LOW | Price is above Apple's ceiling |
The script's binary-search recovery loop reads both phrasings and walks in the right direction. If errors persist after recovery converges, it's the pricing-model problem — not a transient API issue. Stop applying and fix the model first.
If yearly territories still fail after the model fix, the long-term solution is to add a monthly product to the subscription group. Apple's validation then walks weekly → monthly → yearly stepwise, giving the yearly far wider pricing flexibility.
Customization
- Tier table: edit
<TOOLKIT>/ppp_tiers.json. Keys are coefficients (multipliers on base_usd), values list the ISO 3166-1 alpha-2 country codes at that tier. Countries not listed get _default_coefficient.
- Tier philosophy: the shipped tiers are loosely Steam/Spotify/RevenueCat-style, NOT raw World Bank PPP. Raw PPP under-prices VPN-arbitrage-prone markets and misses high-cost-of-living markets that aren't actually rich. Tune for your audience.
- Product subset:
--products weekly,yearly to limit a run to specific entries by name.
- Skip live FX:
--no-live-fx uses the script's hardcoded FX table.
- Skip snapshot:
--no-snapshot (not recommended).
CLI flags
python <TOOLKIT>/set_ppp_pricing.py [options]
--config PATH pricing.json (required unless --restore)
--platform {ios,android,both} default: both
--apply actually push (default: dry-run)
--no-snapshot skip pre-apply snapshot
--no-live-fx skip live FX fetch
--products NAMES comma-separated names to filter
--restore PATH restore from a snapshot JSON
References
- Full workflow + setup walkthrough:
<TOOLKIT>/README.md
- Apple pricing-model quirks: README → "Apple's Subscription Pricing Quirks"
- Google Play quirks (
convertRegionPrices, regionsVersion, regionalConfigs wholesale replace): README → "Google Play Pricing Quirks"
- Architecture (Apple price-point ID synthesis, currency derivation, snapshot/restore): README → "Architecture & design notes"
- Slash command companion:
<TOOLKIT>/.claude/commands/ppp-pricing.md