Skip to content

Multi-Account Detection

Status: 📐 Design-only — Entirely unbuilt — doc self-declares 📐 Design-only; no service, model, job, consumer wiring, or admin UI exists anywhere in the codebase. (impl audit 2026-06-16)

📐 Design-only. 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

# services/gameserver/src/services/multi_account_detection_service.py  (target path)

def upsert_cluster(player_ids: set[UUID], signal: ClusterSignal) -> MultiAccountCluster
def flag_account(player_id: UUID, cluster_id: UUID, severity: str, signal: str) -> MultiAccountFlag
def participation_weight(player_id: UUID, surface: str) -> float
def review_queue() -> list[MultiAccountCluster]   # admin-UI fetch
def admin_override(cluster_id: UUID, decision: str, reason: str) -> None

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 Target file
MultiAccountDetectionService services/gameserver/src/services/multi_account_detection_service.py (target)
Cluster + flag models services/gameserver/src/models/multi_account.py (target)
Periodic detection job services/gameserver/src/jobs/multi_account_sweep.py (target) per ADR-0053
Admin review UI services/admin-ui/src/pages/MultiAccountReview/ (target)