Skip to content
Back to skills

Ethical Monetization Patterns

ASecurity

Ethical monetization patterns: a dark pattern audit checklist, a cosmetics-only store, battle pass design, a Godot and GodotSteam IAP purchase flow, a voluntary spending cap, and an explicit list of banned patterns. Use when building a store or reviewing monetization for exploitative design.

  • 65 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 1, 2026
developmentgonodegit

Works with

  • cli

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add HermeticOrmus/claude-code-game-development --skill ethical-monetization-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ethical Monetization Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ethical Monetization Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hermeticormus-ethical-monetization-patterns/badge)](https://www.skillsdirectory.com/skills/hermeticormus-ethical-monetization-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ethical-monetization-patterns
description: "Ethical monetization patterns: a dark pattern audit checklist, a cosmetics-only store, battle pass design, a Godot and GodotSteam IAP purchase flow, a voluntary spending cap, and an explicit list of banned patterns. Use when building a store or reviewing monetization for exploitative design."
---

# Ethical Monetization Patterns

## Dark Pattern Audit Checklist

```
MONETIZATION ETHICS AUDIT
==========================

SECTION 1: Pay-to-Win Assessment
[ ] All spending provides cosmetic/convenience only, no gameplay power
[ ] No stat boosts, ability unlocks, or damage increases tied to spending
[ ] PvP balance unaffected by any purchasable item
[ ] Free players can compete with paying players in all core game modes

SECTION 2: Transparency
[ ] Real currency prices shown prominently (no hiding behind premium currency only)
[ ] Premium currency amounts shown in real money equivalent during purchase flow
[ ] No orphaned premium currency designs (bundles divisible cleanly by item prices)
[ ] "What you get" fully described before payment, no mystery boxes unlabeled

SECTION 3: Psychological Manipulation
[ ] No countdown timers on non-scarce digital goods
[ ] No "only N left" false scarcity on infinitely reproducible items
[ ] No social pressure mechanics ("your friends bought this")
[ ] No progress loss threats to prevent churn
[ ] Loot boxes: are contents and probabilities disclosed? (required in Belgium, Netherlands, China)

SECTION 4: Children's Protections
[ ] Parental control spending limits honored (iOS Screen Time, Google Family Link)
[ ] Purchase confirmation dialog before every transaction
[ ] Opt-in spending limits (monthly cap user can set themselves)
[ ] No targeted advertising based on spending patterns for under-18

SECTION 5: Cancellation and Fairness
[ ] Subscriptions cancel in 2 clicks (no roach motel UI)
[ ] Refund policy clearly stated and honored
[ ] Account deletion removes payment data (GDPR)
[ ] Premium content retained if subscription lapses (no retroactive removal)

RESULT: ___ / 20 checks passed
0-10: Significant ethical issues requiring redesign
11-16: Some concerns to address before launch
17-20: Ethically sound monetization
```

## Cosmetics-Only Store (Path of Exile Model)

```gdscript
# Cosmetics-only item store - no pay-to-win
class_name CosmeticStore extends Node

# Purchasable items: visual only
class CosmeticItem extends Resource:
    @export var item_id: StringName
    @export var display_name: String
    @export var price_real_currency: float  # Show real price, not obscure currency
    @export var item_type: StringName       # "character_skin", "emote", "title", "portrait"
    @export var preview_texture: Texture2D
    # NOT: damage_bonus, stat_modifier, unlock_ability

func purchase(item: CosmeticItem, player_id: int) -> bool:
    # IAP flow - platform-specific
    # On success: write to player's owned_cosmetics save data
    # On failure: show clear error with reason
    return _initiate_platform_purchase(item, player_id)

func apply_cosmetic(player: Player, item_id: StringName) -> void:
    if player.owned_cosmetics.has(item_id):
        player.active_skin = item_id
        # Change visual only - no stats affected
```

## Battle Pass Ethical Design

```gdscript
# Battle pass with ethical constraints enforced in data
class_name BattlePass extends Resource
@export var season_name: String
@export var season_duration_days: int = 90
@export var free_tiers: Array[BattlePassTier]   # Available to all players
@export var paid_tiers: Array[BattlePassTier]   # Available with battle pass purchase

# Tier types allowed per ethical guidelines
class BattlePassTier extends Resource:
    @export var tier_number: int
    @export var reward_type: StringName  # Must be in ALLOWED_REWARD_TYPES
    @export var reward_data: Dictionary

    const ALLOWED_REWARD_TYPES: Array[StringName] = [
        &"cosmetic_skin",
        &"emote",
        &"title_badge",
        &"player_icon",
        &"in_game_currency_small",  # Small amounts of earnable currency
    ]
    # FORBIDDEN types (would make this pay-to-win):
    # &"stat_boost", &"ability_unlock", &"damage_increase", &"rare_equipment"

# Validate pass before publishing
func validate_ethical_design() -> bool:
    for tier in paid_tiers:
        if tier.reward_type not in BattlePassTier.ALLOWED_REWARD_TYPES:
            push_error("Pay-to-win item in battle pass: %s" % tier.reward_type)
            return false
    return true

# Calculate daily XP required to complete at reasonable pace
func daily_xp_required_free() -> float:
    var total_free_xp := free_tiers.reduce(func(acc, t): return acc + t.xp_required, 0)
    var gameplay_days := season_duration_days * 0.8  # Assume 80% of days playing
    return total_free_xp / gameplay_days
    # Target: < 90 minutes of average play per day to complete free track
```

## IAP Purchase Flow (Godot + GodotSteam)

```gdscript
# Steam microtransaction integration
class_name SteamStore extends Node

func initiate_purchase(item_def_id: int, quantity: int = 1) -> void:
    # Show confirmation with real price before initiating
    var price := Steam.getItemPrice(item_def_id)
    _show_purchase_confirmation(item_def_id, price, func(confirmed: bool):
        if not confirmed:
            return
        var purchase_id := Steam.startPurchase([item_def_id], [quantity])
        Steam.purchase_result.connect(_on_purchase_result, CONNECT_ONE_SHOT)
    )

func _on_purchase_result(result: int, purchase_id: int, items: Array) -> void:
    match result:
        1:  # k_EResultOK
            _grant_items(items)
            _show_purchase_success()
        _:
            _show_purchase_failed(result)

func _show_purchase_confirmation(item_id: int, price: float, callback: Callable) -> void:
    # MANDATORY: show real price before purchase
    # MANDATORY: show exactly what the player is buying
    # MANDATORY: provide prominent "Cancel" option equal in size to "Confirm"
    var dialog := ConfirmationDialog.new()
    dialog.title = tr("STORE_CONFIRM_PURCHASE_TITLE")
    dialog.dialog_text = tr("STORE_CONFIRM_PURCHASE_BODY") % [
        Steam.getItemName(item_id),
        "$%.2f" % price  # Always show real currency
    ]
    dialog.confirmed.connect(func(): callback.call(true))
    dialog.canceled.connect(func(): callback.call(false))
    add_child(dialog)
    dialog.popup_centered()
```

## Spending Limit (Voluntary Self-Cap)

```gdscript
# Let players set their own spending limits - ethical responsibility feature
class_name SpendingLimits extends Resource
@export var monthly_limit_cents: int = 0  # 0 = no limit
@export var notify_at_percent: float = 0.8  # Warn at 80% of limit

var _spent_this_month_cents: int = 0
var _month_key: String = ""  # "2026-02" format

func can_purchase(price_cents: int) -> bool:
    _refresh_month()
    if monthly_limit_cents <= 0:
        return true  # No limit set
    return (_spent_this_month_cents + price_cents) <= monthly_limit_cents

func record_purchase(price_cents: int) -> void:
    _refresh_month()
    _spent_this_month_cents += price_cents
    _save()
    if monthly_limit_cents > 0:
        var ratio := float(_spent_this_month_cents) / float(monthly_limit_cents)
        if ratio >= notify_at_percent:
            EventBus.spending_limit_warning.emit(ratio)

func _refresh_month() -> void:
    var now := Time.get_datetime_dict_from_system()
    var current_key := "%04d-%02d" % [now.year, now.month]
    if current_key != _month_key:
        _month_key = current_key
        _spent_this_month_cents = 0
        _save()
```

## Anti-Patterns (Explicitly Banned)

- **Loot boxes without disclosed odds**: Illegal in Belgium and Netherlands. Ethically wrong everywhere. Always show drop rates.
- **Premium currency bundles with remainder**: Bundle 550 gems, items cost 500 - 50 orphaned gems push next purchase. Bundles must be exact multiples of item prices.
- **Countdown timers on digital goods**: "Expires in 3:00:00" on infinitely reproducible virtual goods is a lie that exploits FOMO.
- **Progress reset threats**: "Your account will be deactivated and progress lost if you don't pay" - pure coercion.
- **Pay-to-win in any competitive context**: Buying power in PvP is fundamentally unfair and alienates the player base. Revenue will peak then collapse.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…