Multi-Account Detection¶
Status: ๐ง Partial โ
MultiAccountCluster/MultiAccountFlagmodels are committed (models/multi_account.py, 183 lines), andmessage_beacon_service.pyalready consumesMultiAccountFlagfor a HARD-severity gate. The detection sweep, signal-scoring, and admin review UI are โ Shipped (multi_account_detection_service.py'supsert_cluster/run_detection_sweep, HARD-paypal/SOFT-IP/SOFT-fingerprint scoring, wired hourly into the scheduler's core loop; full admin CRUD at/admin/multi-account/clusters;MultiAccountReview.tsxadmin-ui page). Soft-tier0.5รdiscount +all_paid_subscribersexemption inparticipation_weightare also โ Shipped (LEG-256 / PR #668 โ tip9dc7ed12). Remaining Partial: nosurfaceargument onparticipation_weight, and planet-scoped soft refinements foreligible_for_contest(see Service contract). (Re-verified 2026-08-21 vs origin/feat46bce720โ tip delta Admin Soft-ORDER #789/#788 honesty only; Soft-HOLD residuals unchanged.)
๐ง Partial. Per ADR-0056. Shared infrastructure that surfaces clusters of accounts likely operated by the same human, then downgrades or blocks their participation in surfaces where alt-rings cause measurable harm: governance voting, station-takeover volume metrics, message-beacon visibility, and faction-rep farming.
Principle: payment is the legitimacy gate¶
The multi-account exploit gets its leverage from being cheap. One human running 10 free-tier alts is a meaningfully different actor from one human paying 10ร $5/mo for 10 Galactic Citizen subscriptions. The first is gaming the system; the second is a paying customer with multiple personas. The detection layer is subscription-tier aware:
- All clustered accounts have active paid subscriptions (Galactic Citizen or Region Owner): no block, no discount. Soft signals still surface for monitoring but do not penalize.
- At least one free-tier account in the cluster: hard signals block, soft signals discount the free account's participation weight.
This means a household of two paying players sharing a payment method gets full participation; a single human running five free alts on the same fingerprint does not.
Cluster signals¶
| Signal | Class | Meaning |
|---|---|---|
| Same payment method on file | hard | Same credit card, PayPal billing agreement, or other PSP-side identifier across multiple accounts. PSP returns a stable hash; we never store raw card numbers. |
| Same active session token across different player IDs | hard | A single browser session authenticated as two players within the same hour. Should never happen legitimately โ session-share or account-sale signal. |
| Same IP address within 24h | soft | Common for shared households, coffee shops, dorm rooms. Cluster signal but not a confidence signal alone. |
| Same device fingerprint hash | soft | Canvas + WebGL + timezone + audio-ctx + UA-CH composite hash. Stable across sessions on the same device. |
| Perfectly-correlated trade timings | soft | Two accounts trading reciprocally within a tight time window, repeated. Behavioural โ runs as a periodic batch. |
| Perfectly-correlated voting patterns | soft | Two accounts voting identically across multiple unrelated proposals. Behavioural โ runs at election close. |
Hard signals trigger immediately on the gated action (team formation, payment update). Soft signals run as a periodic batch every hour; the runtime worker is a Batch-6 service with no need for real-time evaluation โ alt-rings don't form in seconds.
Discount math¶
For every gated participation surface, the per-account multiplier is:
def participation_weight(player_id, surface):
flag = MultiAccountFlag.most_severe_for(player_id)
if flag is None:
return 1.0 # no cluster, full weight
if all_paid_in_cluster(flag.cluster_id):
return 1.0 # paid-tier exemption
if flag.severity == "hard":
return 0.0
if flag.severity == "soft":
return 0.5
Surfaces that consume the weight:
- Regional governance vote:
RegionalVote.weight = membership.voting_power ร participation_weight(player_id, 'governance'). Per../FEATURES/gameplay/regional-governance.md. - Station-takeover volume metric: free-tier accounts in a flagged cluster contribute to volume at the discounted weight. Hard-flagged accounts contribute 0 โ the syndicate-volume exploit collapses.
- Message-beacon visibility: free-tier accounts in a flagged cluster have beacon weight 0ร (their beacons don't count toward the per-sector cap and aren't surfaced). Per
../FEATURES/gameplay/message-beacons.md. - Faction-rep gain: free-tier accounts in a flagged cluster have faction-rep deltas multiplied by the participation weight. Hard flag โ no rep gain at all on cluster-coordinated actions.
Service contract¶
Shipped today โ services/gameserver/src/services/multi_account_service.py:
def participation_weight(db: Session, player_id: uuid.UUID) -> float
def blocks_vote(db: Session, player_id: uuid.UUID) -> bool
def eligible_for_contest(db: Session, player_id: uuid.UUID, planet_id: uuid.UUID) -> bool
participation_weight implements the live discount ladder (ADR-0056 E-V5 / Discount math above): no flag โ 1.0; cluster all_paid_subscribers โ 1.0; most-severe HARD โ 0.0; most-severe SOFT โ 0.5. No surface parameter yet โ callers share one weight. blocks_vote wraps it for the ADR-0056 N-V3 franchise gate (weight == 0.0).
eligible_for_contest is the ADR-0091 ยง8 Amendment A settle_contest surface โ a gated participation-weight query for planet-settlement contests, additive to ADR-0056's detection model. v1: ineligible when participation_weight == 0.0 (HARD, non-exempt); SOFT (0.5) and clear (1.0) remain eligible. The planet_id parameter exists for API-shape parity with the ADR's per-planet-scoped surface (soft-flag tie-loss / no-relief-between-linked-accounts refinement is still unbuilt); the underlying detection remains account-scoped, not planet-scoped, so it's unused today.
โ
Shipped โ MultiAccountDetectionService (services/gameserver/src/services/multi_account_detection_service.py):
def upsert_cluster(...) -> MultiAccountCluster # signal-scored, HARD-paypal / SOFT-IP / SOFT-fingerprint
def run_detection_sweep(db, *, now=None) -> dict # wired hourly via scheduler/core_loop.py (state key multi_account_detection_last_run_at)
class MultiAccountDetectionService: ...
Admin review + decision routes: /admin/multi-account/clusters (list/get/decide with confirm/override/escalate semantics) in api/routes/admin_multi_account.py, surfaced in admin-ui's MultiAccountReview.tsx.
Still unbuilt (Partial residue): surface-scoped weights on participation_weight, and planet-scoped soft refinements on eligible_for_contest (planet_id accepted but unused). Soft-tier 0.5ร + all_paid_subscribers exemption are โ
shipped.
Schema¶
MultiAccountCluster and MultiAccountFlag live in ../DATA_MODELS/gameplay.md.
Admin review surface¶
The admin UI (per ./admin-ui.md) gets a Multi-Account Review page that lists every active cluster with:
- Cluster signals (which heuristics fired) and their severity classes.
- Each account's subscription tier, age, recent activity summary.
- A decision panel:
Confirm(cluster is real, apply discounts),Override(legitimate household / shared connection โ clear flags),Escalate(deeper review). - Audit log of admin decisions for compliance.
Decisions update MultiAccountCluster.admin_decision and propagate to all member MultiAccountFlag rows. An admin override clears flags for that cluster permanently โ re-detection requires a new signal.
Pre-existing alt rings¶
The detection job runs against historical session data on first deploy, generating an initial backlog of clusters. Admin reviews and confirms / overrides each one before any discounts apply. After the backlog is cleared, new clusters surface continuously.
Source map¶
| Concern | File |
|---|---|
MultiAccountDetectionService |
services/gameserver/src/services/multi_account_detection_service.py โ
|
| Cluster + flag models | services/gameserver/src/models/multi_account.py โ
|
| Periodic detection sweep | run_detection_sweep, wired hourly in services/gameserver/src/services/scheduler/core_loop.py โ
(not a standalone job file โ runs inline in the core scheduler loop) |
| Admin review routes | services/gameserver/src/api/routes/admin_multi_account.py โ
|
| Admin review UI | services/admin-ui/src/components/pages/MultiAccountReview.tsx โ
|
| Soft-discount math | participation_weight, services/gameserver/src/services/multi_account_service.py โ
(soft 0.5ร + all_paid_subscribers; no surface arg yet) |
Related¶
- ADR-0056 โ origin ADR for this service.
./monetization.mdโ subscription-tier definitions../admin-ui.mdโ admin app structure.../FEATURES/gameplay/regional-governance.mdโ voting eligibility consumer.../FEATURES/gameplay/message-beacons.mdโ beacon visibility consumer.../FEATURES/gameplay/factions-and-teams.mdโ faction-rep consumer.