Skip to content

Monetization

Status: 🚧 Partial — PayPal create/activate/cancel/suspend webhook flow is live; the three security invariants (timestamp window, idempotency table, production bypass guard) are now implemented + proven live (2026-06-16, dev e123ab5). Remaining Partial: container-provisioning gap on the webhook activation path + region-lifecycle states. (Re-verified 2026-08-21 vs origin/feat 46bce720 — tip delta Admin Soft-ORDER #789/#788 honesty only; Soft-HOLD residuals unchanged.)

SectorWars 2102 is free-to-play with optional subscriptions. The base game (sign-up, single-region play, all core gameplay) is free forever. Real money buys cross-region travel, region ownership, and (future) cosmetic / convenience perks — never a competitive ceiling.

Pay-to-win firewall (ratified 2026-06-21). Paid buys SHAPE, CONVENIENCE, and EXPRESSION; earned-and-free buys POWER. A Galactic-Citizen ship or module may differ from its free counterpart only in grid shape (best-case adjacency ≤ the free anchor's), non-income utility breadth (under the best-3 stacking cap), QoL reachable by free credits+time, and cosmetics. Its competitive ceiling — combat and income — is no higher than a free player reaches. No paid slot or effect carries an income axis (passive_income, mining_efficiency, cargo_bonus_percent, or any income class). A Citizen supercharged slot is permitted but class-locked to a non-combat/non-income utility class, so its multiplier raises utility/QoL, never the combat or income ceiling. Higher paid tiers may grant broader shape / convenience / cosmetic, never a higher competitive ceiling. Enforced in code by a CI income-fence test.

Source files: - services/gameserver/src/services/paypal_service.py — PayPal API integration, plan IDs, webhook handling, _activate_galactic_citizenship. - services/gameserver/src/models/user.pysubscription_tier, subscription_status, paypal_subscription_id columns on User. - services/gameserver/src/models/region.pysubscription_tier, paypal_subscription_id on Region (for Region Owner billing). - services/gameserver/src/models/player.pyPlayer.is_galactic_citizen, Player.can_travel_between_regions. - services/gameserver/src/services/ship_upgrade_service.pyis_galactic_citizen eligibility helper (live User subscription re-read).


1. Subscription tiers

Three tiers are defined in paypal_service.py:SubscriptionTier. The first two are live; the third is reserved for future expansion.

1.1 Free

Price: $0.

Identity: Default for every new account. User.subscription_tier = NULL.

Includes: - Full character creation and play within a single home region. - All core gameplay: trading, combat, planet ownership, colonization, emergent faction reputation, team membership, ranking progression, NPC contracts. - ARIA companion at base consciousness tier. - Access to leaderboards and cross-team chat within the home region. - Travel to Central Nexus as a visitor membership. The Nexus is the universal hub and is not gated by Galactic Citizenship — free-tier players can warp from their home region's gate to Gateway Plaza, trade and socialize in the Nexus, and warp back. This does not grant access to other player-owned regions.

Excludes: - Travel to other player-owned regions. The Player.can_travel_between_regions gate (which permits inbound to any region from the Nexus warp gate network) requires Galactic Citizenship. - Region ownership. - (Future) cosmetic ship skins and ARIA voice packs.

1.2 Galactic Citizen — $5 / month

Identity: User.subscription_tier = "galactic_citizen". PayPal plan ID configured via PAYPAL_GALACTIC_CITIZEN_PLAN_ID env var (services/gameserver/src/core/config.py:73).

When a Galactic Citizenship subscription activates, Player.is_galactic_citizen is set to true (paypal_service.py:_activate_galactic_citizenship, line 412).

Unlocks: - Travel to other player-owned regions via Central Nexus warp gates. Gated by Player.is_galactic_citizen / Player.can_travel_between_regions (models/player.py:203, :337-339) and the live eligibility helper ship_upgrade_service.is_galactic_citizen. Free-tier players can already reach the Nexus itself; Galactic Citizenship is what permits onward warp from the Nexus into another region. There is no live RegionalRole.GALACTIC_CITIZEN / GALACTIC_CITIZEN_BENEFITS path — regional_auth_service.py was deleted (1619912d). - A small weekly in-game credit stipend (target: 2,500 cr/week — see FEATURES/economy/lifecycle.md). - Faster ARIA relationship score growth and elevated initial consciousness tier. - Cosmetic citizenship badge on player profile.

1.3 Region Owner — $25 / month

Identity: Region.subscription_tier = "regional_owner", Region.paypal_subscription_id set, Region.owner_id linked to the User. PayPal plan ID via PAYPAL_REGIONAL_OWNER_PLAN_ID.

Activation: paypal_service.py:_activate_regional_ownership (line 426). On subscription approval, the system either creates a new Region row or transfers ownership of an existing one.

Unlocks: - Ability to create and own a player-owned region (Standard tier ≈1,000 sectors with ±20% variability, range 800–1,200; higher subscription tiers 📐 Design-only widen the upper bound). Schema CHECK constraint widens to 100–1,500 to accommodate the Standard range plus tier headroom.

📐 Design-only widening mechanism (drafted 2026-08-04, WO-CANON-DRAFT-REGION-SIZE-TIER-WIDENING — kernel only, pricing needs Max sign-off before it's canonical). Verified current state first: Region.total_sectors (models/region.py:161) defaults to 500 at the column level and is never explicitly set by paypal_service.py's region-creation path (_activate_regional_ownership) — the actual per-region sector count comes from whichever galaxy-generation pass populates the region afterward, not from the subscription purchase itself. There is currently only ONE paid region-owner tier (SubscriptionTier.REGIONAL_OWNER, flat $25/mo, paypal_service.py:86) — no size-variant sub-tiers exist in the SubscriptionTier enum today, so "higher subscription tiers" describes a NOT-YET-DEFINED ladder, not an existing-but-unwired one.

Proposed kernel (widen, don't replace, the existing single-tier purchase flow): add sibling SubscriptionTier values above REGIONAL_OWNER (e.g. REGIONAL_OWNER_EXPANDED, REGIONAL_OWNER_SOVEREIGN — names/pricing NO-CANON, flagged for Max), each mapping to its own PayPal plan ID (mirrors the existing PAYPAL_REGIONAL_OWNER_PLAN_ID env-var pattern) and to a max_total_sectors ceiling passed into the galaxy-generation call that currently has no tier awareness at all. Gating point: the generation-trigger path reads the purchasing user's resolved tier and clamps/selects the target sector count against that tier's ceiling (never exceeding the schema CHECK's 1,500 hard cap regardless of tier). No code built — this is the mechanism sketch a future WO would implement against; actual tier count, sector-count breakpoints, and pricing are monetization decisions requiring Max's ruling, not something to invent here. - Configure governance_type (autocracy / democracy / council). - Set tax_rate (5% to 25%, constraint in models/region.py:134). - Set starting_credits for new players spawning in the region. - Customize language_pack, aesthetic_theme, traditions (cultural identity). - Receive a 50% share of regional tax revenue — ratified as a first-cut default subject to a future balance-tuning pass once real regional-economy telemetry exists, per DECISIONS.md's region-tax-revenue-share entry (Max carte blanche, 2026-08-07). The percentage is now canonical; the payout mechanism itself (the code path that actually computes and pays out the owner's cut) is still 📐 Design-only / not yet built. - Issue regional bounties from treasury. - Includes Galactic Citizen benefits for the owning user.

1.4 Nexus Premium — $50 / month (future)

Identity: User.subscription_tier = "nexus_premium". Plan ID PAYPAL_NEXUS_PREMIUM_PLAN_ID.

Reserved tier (paypal_service.py:30 annotates "future expansion"). Intended for power users who want Galactic Citizen + a permanent presence in the Central Nexus + enhanced ARIA features. Specifics to be defined.


2. PayPal billing flow

End-to-end topology of a subscription purchase. Source: paypal_service.py:create_galactic_citizen_subscription (line 166) and create_regional_ownership_subscription (line 206).

2.1 Subscription create

Player-client                 gameserver                      PayPal
     │                             │                             │
     │ POST /api/v1/subscriptions/ │                             │
     │   galactic-citizen          │                             │
     │ ─────────────────────────►  │                             │
     │                             │ POST /v1/billing/           │
     │                             │   subscriptions             │
     │                             │ ──────────────────────────► │
     │                             │ ◄────── subscription.id +   │
     │                             │         approval_url        │
     │ ◄─── { approval_url } ─────  │                             │
     │                             │                             │
     │ user redirected to PayPal approval page                   │
     │ ─────────────────────────────────────────────────────────►│
     │ user completes PayPal flow                                │
     │ ◄──── return_url with subscription_id ─────────────────── │

Client return UX (LEG-24): PayPal redirects to return_url / cancel_url (/subscription/success · /subscription/cancelled). The player-client mounts pages/SubscriptionResult.tsx on those routes (reads subscription_id, or PayPal's token / ba_token fallbacks) and shows activation or cancel confirmation. Entitlement flips are still webhook-driven (§2.2) — the result page is confirmation UX, not the source of truth for tier. See ui-flows.md steps 32–34 and cockpit-shell.md §5 Closed note.

2.2 Webhook activation

PayPal calls back to the gameserver webhook endpoint when the subscription state changes. Source: paypal_service.py:handle_subscription_webhook (line 294).

PayPal                          gameserver                   database
   │                                │                            │
   │ POST /webhooks/paypal          │                            │
   │   { event_type:                │                            │
   │     BILLING.SUBSCRIPTION       │                            │
   │     .ACTIVATED }               │                            │
   │ ─────────────────────────────► │                            │
   │                                │ verify signature           │
   │                                │ (validate_webhook_signature)│
   │                                │ ─────────────────────────► │
   │                                │ parse custom_id            │
   │                                │ → "galactic_citizen_<uid>" │
   │                                │ or "regional_owner_<uid>_<region>"
   │                                │                            │
   │                                │ User.paypal_subscription_id│
   │                                │ User.subscription_tier     │
   │                                │ Player.is_galactic_citizen │
   │                                │   = true                   │
   │                                │ ─────────────────────────► │
   │ ◄── 200 OK ────────────────────│                            │

2.3 Feature flag flip

After the webhook commits, gated features become available immediately: - Player.can_travel_between_regions returns true (gates inter-region travel API). - Player.is_galactic_citizen = true (gates Galactic-Citizen-only UI elements). - For Region Owner: Region.status = "active", Region.owner_id set — region governance UI unlocks.

No client-side cache flush is required; the player-client checks these flags on each session and on key UI transitions.


3. Webhook event handling

Source: paypal_service.py:handle_subscription_webhook (line 294). Mapping of PayPal event → action:

PayPal event Handler Effect
BILLING.SUBSCRIPTION.ACTIVATED _handle_subscription_activated Set is_galactic_citizen = true or activate region
BILLING.SUBSCRIPTION.CANCELLED _handle_subscription_cancelled Clear is_galactic_citizen; suspend region (Region.status = "suspended")
BILLING.SUBSCRIPTION.SUSPENDED _handle_subscription_suspended Suspend region access
BILLING.SUBSCRIPTION.PAYMENT.FAILED _handle_payment_failed Log warning; failure-counter for eventual suspension
PAYMENT.SALE.COMPLETED _handle_payment_completed Confirm renewal; reactivate suspended region if applicable

Webhook signature validation is mandatory and binding in production (per ADR-0058 A-D3). Three checks run on every webhook; all must pass before the mutation commits:

  1. Signature verification against PayPal's webhook key (validate_webhook_signature).
  2. Timestamp window: the webhook's event_time must be within 5 minutes of server now(). Older or far-future timestamps are rejected — closes the replay-attack window.
  3. Idempotency: the webhook's event_id is checked against a processed_webhook_events table (UNIQUE INDEX (event_id)). Duplicate delivery (PayPal's at-least-once semantics) returns HTTP 200 without re-applying the mutation.

Validation failure → HTTP 401 (not log-and-allow). The processed_webhook_events insert is in the same transaction as the subscription mutation, so a successful insert means the mutation committed.

Bypass code path. Production binary contains zero bypass code reachable at runtime. The dev/test bypass lives behind a separate config-gated module imported only when settings.environment != 'production'. If the bypass flag is set while environment == 'production', the gameserver fails to start with a BypassFlagInProductionError — fail-fast at import time, not runtime. (code-wins note 2026-06-16: the implemented env var is PAYPAL_SKIP_WEBHOOK_VALIDATION, not PAYPAL_WEBHOOK_BYPASS.)


4. Refund, suspension, termination

4.1 Suspension

Triggered by: - Repeated payment failures (PayPal sends BILLING.SUBSCRIPTION.SUSPENDED after configured retry attempts). - Operator-initiated suspension via admin UI. - Direct PayPal API call: paypal_service.suspend_subscription(subscription_id, reason) (line 266).

Effect: - Region.status = "suspended" for region subscriptions — the region remains in the database, citizens cannot enter/leave, owner cannot configure. - Player.is_galactic_citizen unset for citizenship subscriptions — cross-region travel blocked, but in-game character data is untouched.

A suspended subscription auto-reactivates when payment resumes (_handle_payment_completed, line 393, sets region.status = "active").

4.2 Cancellation

User-initiated: - Player cancels via PayPal account or via in-game UI (which calls paypal_service.cancel_subscription, line 252).

Effect: - Subscription becomes CANCELLED at PayPal. - Webhook fires BILLING.SUBSCRIPTION.CANCELLED. - _handle_subscription_cancelled (line 340) clears Region.paypal_subscription_id, sets region.status = "suspended", or clears player.is_galactic_citizen.

4.3 Refunds

Refund handling is operator-driven — there is no in-app "request refund" button. An operator reviews the case and: 1. Issues the PayPal refund out-of-band via the PayPal merchant dashboard. 2. Cancels the subscription via paypal_service.cancel_subscription. 3. Logs an audit entry via services/audit_service.py.

Target policy: pro-rata refunds within 7 days of charge for Galactic Citizen; pro-rata only on first month for Region Owner; no refund after the first month of a region subscription (since the universe state has been altered by the player's ownership actions).

4.4 Manual GC grant/revoke (admin support tooling)

📐 Design-only, gated behind the Wave-12 RBAC scope rollout (ADR-0093 item 21). Existing grant/revoke Galactic-Citizen service methods on the player model wire as admin.subscriptions.*-scoped admin routes — manual support tooling for comping GC status (e.g. compensating an affected player) or clawing it back (e.g. chargeback/fraud), outside the normal PayPal subscription webhook flow. Sequenced behind item 19's scope rollout; every grant/revoke logs to AdminActionLog per ./admin-ui.md § Security & audit. See ./admin-ui.md § ARIA per-player security-ops UI for the sibling item-10 UI ruling.

4.5 Termination — region lifecycle cascade

Per ADR-0050, region subscriptions follow a 5-state cascade with multiple opportunities for recovery before content is deleted. Galactic Citizen cancellation, by contrast, is simple — it returns the user to free tier without any cascade.

Region lifecycle states (Region.status):

State Trigger What works What's blocked Takeover available?
active Subscription paid Everything Nothing n/a
suspended Payment failure event All gameplay continues New residents from outside cannot join. Owner UI shows payment-recovery prompt. Yes — opens immediately
grace 7 days suspended, payment unrecovered All gameplay continues New construction blocked (no new station builds, no new gate beacons, no region-funded TradeDocks). Visible warning banner. Yes — still open
terminated 30 days from initial suspension, payment unrecovered Region marked for cleanup; 7-day final notice fires All region-content writes blocked No — takeover window closed
generation_corrupt Phase 13 rollback failure n/a All access blocked; ops alert n/a (handled out-of-band)

After terminated, the cleanup cascade runs (asset preservation rules below). 7 days after termination, the Region row + dependent content hard-deletes via CASCADE; audit rows persist via region_id_snapshot.

Total grace from first payment failure to content deletion: ~44 days.

Region-owner takeover

While a region is suspended or grace, any Galactic Citizen subscriber (galaxy-wide, not just current residents) can offer to assume ownership:

POST /api/v1/regions/{id}/takeover

The PayPal flow runs at standard $25/mo Region Owner rate. On payment success, ownership flips atomically (Region.owner_id, Region.paypal_subscription_id, Region.status = active). The old owner, if still GC-subscribed, retains all their assets inside the region as a regular resident.

Concurrent claim serialization (per ADR-0058 A-F3). Multiple offers can race. The takeover handler serializes claims via SELECT FOR UPDATE on the Region row at claim time. First commit wins; losing claims reject with ERR_REGION_TAKEOVER_LOST and refund escrow inside the same transaction — no temporal window where money is held without a corresponding ownership state. TakeoverIntent state machine:

pending  ──claim_accepted──▶  won         (region transfer follows)
pending  ──claim_rejected──▶  lost        (escrow refunded in same tx)
won      ──transfer_done──▶   transferred
won      ──transfer_failed──▶ failed      (escrow refunded; region stays with old owner)

Every transition is atomic.

Asset preservation cascade

When termination fires and no takeover happens, the cleanup orchestrator processes each player with assets in the region. Paying players' belongings are preserved as much as possible. The full table is in ADR-0050; summary:

  • Ships (piloted, parked, hangared): evacuate to Central Nexus with player. Drifting/parked routed to Abandoned Hangar at Starport Prime for free claim.
  • Player-owned stations: relocate as a unit to a destination region. Structure + upgrades + treasury + cargo intact; security tier resets. Cost: 30% of (acquisition + upgrade capital) — Path A debits station treasury (with player-wallet fallback then upgrade-strip then loss-with-credit-comp); Path B prepay during grace preserves treasury intact.
  • Player-owned planets (immovable): lost. Compensation: Genesis devices + credits scaled to citadel level (L1 1B+50k → L5 5A+25M cr). Plus: planet's safe vault contents → Central Nexus Bank with 20% transport loss (Path A) or 100% transfer (Path B prepay) or no loss (Path C manual evacuation during grace).
  • Captured pirate holdings: stations relocate, planets lost per planet rule.
  • Player credits: survive (region-independent).
  • Active contracts: cancelled with refunds.
  • Bounties placed: refunded pro-rata.
  • Ship-construction-in-progress at TradeDock: cancelled per AU2-6 + ADR-0039.
  • Player-built warp gates anchored to/from region: per ADR-0052 SK38 — gate's both endpoints destroyed atomically; gate owner receives 50% of construction cost as a credit refund (deposited to wallet if online, Central Bank if offline). Atomic delete; no orphan half-gates.

Central Nexus Bank

A new player-facing surface at any Starport Prime dock in the Central Nexus, operated by the Galactic Concord. Holds credits + commodities indefinitely on behalf of players. Region-independent. Receives planet-safe transfers and station credit-compensation fallbacks during cascades. Players withdraw at any Starport Prime dock — credits free instantly, commodities at 1 turn per 100 units to ship cargo.

Schema in ../DATA_MODELS/player.md#playercentralbankaccount. Runtime in ../SYSTEMS/region-lifecycle.md.

Withdraw ledger + player UI — shipped. GET /central-bank/balance and the credit/commodity withdraw routes (api/routes/central_bank.py, PR #580, f98c5ba8) are live, gated on Starport Prime docking, and write ENTRY_WITHDRAW_CREDITS/ENTRY_WITHDRAW_COMMODITY ledger entries as designed. Player-client balance/withdraw UI is also shipped (3ab811be, PR #582) — BankPanel mounted from GameDashboard when docked at Starport Prime, backed by centralBankAPI (services/player-client/src/services/api.ts) with focused BankPanel tests. Starport Prime / docking gate rules above are unchanged.

Galactic Citizen termination — 7-day asset-liquidation window

Per ADR-0054, a GC subscription entering payment-failure state opens a 7-day asset-liquidation window for assets the player owns in regions outside their home region.

  • Home region is unaffected. A free-tier player can continue to play normally in their home region indefinitely. Ships, planets, stations, credits in their home region all stay accessible.
  • Foreign-region assets (planets / stations / captured pirate holdings / warp gate endpoints in any region other than the player's home) enter the 7-day liquidation window.
  • ARIA proactively notifies on next login: "Your Galactic Citizen subscription is lapsed. You have 7 days to withdraw assets from regions outside your home region. After that, captured holdings, foreign-region planet safes, and station ownerships enter the standard 30-day abandonment cascade." Per the catalog at ../FEATURES/gameplay/aria-companion.md#aria-narration-hooks-event-catalog entry P-F6. Subscription-upgrade-screen narration (P-F3) surfaces tier benefits at the moment a player views the upgrade UI.
  • GC-bypass transport — the player gets a one-time, free, system-action transport (POST /api/v1/players/me/gc-emergency-relocation) to one of their foreign-region holdings. Consumed once per lapse cycle (renewed on next GC re-subscription).
  • On-site liquidation — once teleported, the player can withdraw planet safes physically (no transport fee), sell stations to NPCs at the standard 50%-acquisition-cost depreciated buyback, or voluntarily surrender holdings (triggers cascade now).
  • After 7 days post-lapse with no action: foreign-region assets enter the standard 30-day abandonment cascade per ADR-0047 and ADR-0050. Cascade compensation deposits to Bank carry access_override=True so the player can withdraw at any port (not just Starport Prime).
  • Re-subscription before day 7 clears the lapse and restores foreign-region access. The GC-bypass transport's "consumed" flag resets on re-subscription.

Runtime details in ../SYSTEMS/region-lifecycle.md § "GC-lapse foreign-asset liquidation window."


5. Pricing rationale

Tier Price Marginal cost Target audience Conversion goal
Free $0 Hosting share Everyone Onboard, retain ≥ 10 hours
Galactic Citizen $5 Trivial Hour-50+ players 5–10% of active players
Region Owner $25 Operator cost dominant (region creation, persistence, governance UI) Hour-300+ committed players 0.5–1% of active players
Nexus Premium $50 (future) Power users, content creators <0.1% (luxury tier)

The price ladder is deliberately steep — Region Owner is 5x Galactic Citizen — because region ownership is a creator role, not a consumer role, and pricing it close to Galactic Citizen would devalue regional politics.


6. Identity-state lookup

A user's combined subscription state can be queried via paypal_service.get_user_subscriptions(user_id) (line 459), which returns all active subscriptions across both Galactic Citizen and any owned regions. The function consults both User.paypal_subscription_id and the joined Region.paypal_subscription_id rows.

For internal feature-gating, services should consult: - User.subscription_tier (string column) — fast in-process check. - User.subscription_status — must equal "active" (or PayPal's "ACTIVE"). - Player.is_galactic_citizen — derived flag for cross-region permissions. - Region.owner_id == user.id AND Region.status == "active" — region ownership check.


7. Audit and compliance

All subscription state changes log to services/audit_service.py. Webhooks include: - Inbound webhook payload (full body) with redacted PayPal access tokens. - Resulting database mutations. - Operator who initiated any direct cancellation/suspension.

Retention: 7 years for billing-related audit entries (financial-services standard).


8. Cross-references