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/feat46bce720— 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.py — subscription_tier, subscription_status, paypal_subscription_id columns on User.
- services/gameserver/src/models/region.py — subscription_tier, paypal_subscription_id on Region (for Region Owner billing).
- services/gameserver/src/models/player.py — Player.is_galactic_citizen, Player.can_travel_between_regions.
- services/gameserver/src/services/ship_upgrade_service.py — is_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:
- Signature verification against PayPal's webhook key (
validate_webhook_signature). - Timestamp window: the webhook's
event_timemust be within 5 minutes of servernow(). Older or far-future timestamps are rejected — closes the replay-attack window. - Idempotency: the webhook's
event_idis checked against aprocessed_webhook_eventstable (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-catalogentry 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=Trueso 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¶
- multi-regional.md — what region ownership unlocks at the gameplay level.
- aria.md — ARIA tier benefits for paid subscribers.
- FEATURES/economy/lifecycle.md — subscription perks as economy faucets.
- FEATURES/gameplay/player-journey.md — when in the player lifecycle each tier becomes attractive.