Player-to-player trading¶
Two co-located players can trade directly — credits, cargo, ships, and equipment — through a bilateral trade window with atomic, all-or-nothing settlement. Distinct from station trading (player ↔ market) and the team treasury (intra-team transfers): a galaxy-wide, cross-team peer exchange. Full design — including the ADR-0008 ship registry this work built first — is in ADR-0089.
Status: ✅ Shipped for credits / commodities / ships —
services/gameserver/src/services/player_trade_service.py(969 lines: initiate/accept/decline/confirm/cancel),api/routes/player_trade.py, modelsPlayerTradeSession/PlayerTradeLog/ShipRegistry(models/player_trade.py,models/ship_registry.py), migrationb4e8c2a91f70, and thePlayerTradeablePricereference table (models/player_trade.py:134, tableplayer_tradeable_prices) are all live. All anti-RMT and throttle numbers below are ratified launch starting values (Max, 2026-06-28), tunable once the live economy provides flow data. 📐 Genesis-device / quantum-crystal gated trading below is prescriptive, not shipped — see the note in "What can be traded". (re-verified 2026-08-21 vs Sectorwars210246bce720; PF substance unchanged — credits-only PC desk, commodity/ship offer UI tip-absent.)
Where trades happen¶
Both players must be in the same sector; a trade including a ship additionally requires
both docked at the same port. A player may hold only one open trade session at a time —
enforced server-side via an additive Player.open_trade_session_id column (nullable,
partial-unique), set under the Player row lock at /initiate and cleared on terminal status
under the same lock. Cross-team and cross-faction trading is allowed (a galaxy-wide primitive,
distinct from the intra-team treasury).
What can be traded¶
A fixed whitelist (PLAYER_TRADEABLE), each tradeable quantity with a single canonical owning
row: credits (Player.credits), commodities (a named receiving ship's Ship.cargo), quantum
shards/crystals (Player wallet, gated), genesis devices/drones/mines (Player aggregate
counters — a pool distinct from a ship's onboard scalars), ships (Ship row via the registry),
and uninstalled equipment/upgrades.
A ship trades as an atomic bundle — its onboard cargo, installed equipment/upgrades, quantum charges, and onboard mines/genesis devices move with the hull and cannot be staged separately; its insurance voids on transfer (ADR-0008). Genesis devices and quantum crystals are reputation/acquisition-gated: a receiver below the asset's threshold cannot receive it (no progression-skip), and gated assets appraise at a 2× gate-bypass premium so they trip value caps early. A Carrier with hangared ships may not be traded unless every hangared ship is validated and included in the settlement lock set.
📐 Not implemented. The genesis-device / quantum-crystal gated-asset trading described
above (and the PLAYER_TRADEABLE whitelist name) is prescriptive only — player_trade_service.py
has no distinct genesis-device/quantum-crystal offer slot (the offer shape is
{credits, commodities, ship_id, ships} only), no reputation/acquisition-threshold gate anywhere
in the file, and no PlayerTradeablePrice seed rows for these assets. The generic commodities
dict moves quantities into the receiving Ship.cargo JSONB only — it does not touch the real
Player.genesis_devices / Player.quantum_crystals aggregate counters that genesis_service.py
and combat_service.py actually read/write, so it cannot move a real genesis device or quantum
crystal even mechanically. Credits, arbitrary commodities (moved via ship cargo), and ships are
genuinely shipped; the gated-asset premium is not (impl audit 2026-08-04).
Never tradable: turns (the ADR-0042 cumulative_turn_count is audit-critical), reputation
and medals (account-bound), a player's currently-piloted or last remaining ship, and any
stolen-flagged or under-active-report ship.
The trade window¶
Open → stage → confirm → settle:
- Open — initiator opens a session (single-session gate) targeting a co-located player; target accepts. Short inactivity expiry.
- Stage — each party adds/removes offered items; every change bumps the session version and resets both confirmations ("lock on change"), capturing the staged assets' material attributes into the confirmed snapshot. Any install/uninstall/load/combat-damage on a staged ship also bumps the version.
- Confirm — each party confirms a specific session version; refused server-side if the offer no longer fits, so doomed deals fail before anyone travels.
- Settle — the second confirmation triggers atomic settlement.
- Cancel / expire — either party may cancel; the target may fast-decline; inactivity, leaving the sector, or undocking (ship trades) cancels. A short initiator cooldown follows a cancelled/expired session (anti lock-and-abandon grief).
Settle costs 0 turns. Grief control (not anti-RMT): 20 free trade initiations per day, then a 30-second cooldown between further initiations; a 60-second cooldown follows a cancelled/expired session.
Atomic settlement — the anti-duplication contract¶
A single database transaction: every involved Player and Ship row is locked
(populate_existing().with_for_update(), ordered by str(id) to prevent cross-feature
deadlock with combat/construction); every JSONB mutation calls flag_modified; the terminal
transition is a versioned UPDATE ... WHERE status='OPEN' AND version=:confirmed_version
(idempotent, DB row is the single source of truth); the whole offer is re-validated under lock
(ownership, condition, capacity, co-location/docking, no staged ship is the piloted or only
ship); ownership transfers first (with a ShipRegistry append), then loads/credits, then the
PlayerTradeLog audit row. Any failure rolls the whole transaction back — no partial
transfers, capacity overflow rolls back rather than truncating.
Value appraisal and anti-RMT model¶
A static, admin-maintained PlayerTradeablePrice reference table (ORM model, not a constant
dict — models/player_trade.py:134, backing table player_tradeable_prices, keyed by
asset_key) gives every tradeable asset a server reference value, independent of live
(player-manipulable) station prices and never Ship.current_value. A weekly job reconciles
the table toward galaxy-median market price; deviation beyond 2× is flagged for manual
review, not applied automatically.
- Flat tax (inflation sink) — ~5% of the gross appraised value moved, sunk in credits on every trade including one-way gifts (rounded up, minimum fee; blocked if the giver can't pay the sink).
- Progressive surcharge (the RMT deterrent) — marginal bands on net value sent per rolling 7-day window, summed across all counterparties:
| Net value sent / 7 days | Surcharge |
|---|---|
| 0 – 50,000 cr | 0% (flat sink only) |
| 50,000 – 250,000 cr | +10% |
| 250,000 – 1,000,000 cr | +30% |
| over 1,000,000 cr | +60% |
A flow into a new/low-rep account adds +25% on net value received above 10,000 cr. New/low-rep = account age under 14 days or personal-reputation tier below Neutral.
- Value-windowed caps — rolling 7-day window unless noted:
| Cap | Value |
|---|---|
| Per-account send ceiling (hard block above) | 2,000,000 cr / 7 days |
| Per-account receive ceiling — established account | 1,000,000 cr / 7 days |
| Per-account receive ceiling — new/low-rep account | 50,000 cr / 7 days |
| Per-counterparty sub-limit (slowly decaying) | 250,000 cr / rolling 30 days |
| Multi-account-cluster combined external outflow | 500,000 cr / 7 days |
- Caps and tax apply to every account, all subscription tiers — economic friction, not a
legitimacy block, so ADR-0056's paid-tier "no block" amnesty does not exempt them. A
cluster-level aggregate cap binds at settlement for accounts sharing a detected
MultiAccountClusterid; transitive-chain detection auto-throttles (not merely surfaces) a chain whose net flow exceeds threshold, even for all-paid clusters. - Audit — every settled trade writes an immutable
PlayerTradeLog(both ids, full manifest, appraised value, tax, sector, port, timestamp). - No reputation effect — P2P trades never change faction/personal reputation (prevents rep-farming via alt trades).
PlayerTradeablePrice seed values: credits 1:1; commodities at reference prices
(fuel_ore 15, organics 18, equipment 35 cr/unit per ADR-0082); hulls at purchase cost (Scout
30,000 … Carrier 1,500,000); gated assets at 2× gate-bypass premium (genesis Basic 50,000 /
Enhanced 150,000 / Advanced 500,000; quantum crystals ~10,000 each at 2×); drones ~500, mines
~1,000 (admin-seeded, tunable).
Integrity and anti-abuse summary¶
Settlement locks every involved player and ship row in one global order, re-validates the full offer (ownership, condition, per-asset capacity) and the confirmed version, so credits and items cannot be duplicated and no confirm-time switch is possible. The value-based transfer tax sinks credits on the appraised value moved (including one-way gifts); the progressive surcharge plus value-windowed caps and the new-account receiving cap — applied to every account regardless of subscription tier — blunt alt-account funnelling; every settled trade is written to an immutable audit log feeding chain-aware multi-account throttling. P2P trades do not change reputation.
Player-facing affordances¶
- ✅ PlayerTradeDesk mount — portal overlay from the tactical-target TRADE action (
services/player-client/src/components/trade/PlayerTradeDesk.tsx, mounted byTacticalTargetPage.tsx; origin/feat46bce720). - ✅ Credits-only P2P lifecycle — initiate / accept / decline / offer / confirm / cancel via
tradeAPI(getOpen,get,initiate,accept,decline,offer,confirm,cancel); 2s poll refresh while open; server reason codes mapped to player copy (PlayerTradeDesk.tsx). Desk stages and displays credit amounts only. - 🚧 Commodity / ship offer UI — tip-absent on
46bce720.tradeAPI.offeracceptscommodities,ship_id, andshipsinapi.ts, butPlayerTradeDeskposts{ credits }only and renders offer summaries as credit totals — no commodity picker, ship selector, or cargo staging surface on tip. Tip-pending land path: OPEN #790 / LEG-1478 (commodity + ship offer staging) — do not read #790 as tip-shipped until tip ancestor of its land SHA. - 📐 Genesis-device / quantum-crystal gated-asset trading — prescriptive only per "What can be traded" above; no player UI for gated assets on tip. Do not read GS ✅ credits/commodities/ships kernel claims as PC coverage for gated assets.
Source map¶
| Concern | Path |
|---|---|
| Trade service | services/gameserver/src/services/player_trade_service.py |
| Trade routes | services/gameserver/src/api/routes/player_trade.py (POST /trade/initiate\|{id}/offer\|{id}/confirm\|{id}/cancel, GET /trade/{id}) |
| Trade models | services/gameserver/src/models/player_trade.py (PlayerTradeSession, PlayerTradeLog) |
| Ship registry (ADR-0008, built as part of this work) | services/gameserver/src/models/ship_registry.py — Ship.registered_owner_id / Ship.current_pilot_id split, Ship.stolen_status, append-only ShipRegistry |
| Migration | b4e8c2a91f70 |
| PlayerTradeDesk UI | services/player-client/src/components/trade/PlayerTradeDesk.tsx |
| TacticalTargetPage TRADE mount | services/player-client/src/components/tactical/pages/TacticalTargetPage.tsx |
| Trade API client | services/player-client/src/services/api.ts (tradeAPI) |
| PlayerTradeDesk tests | services/player-client/src/components/trade/__tests__/PlayerTradeDesk.test.tsx |
| TacticalTargetPage TRADE tests | services/player-client/src/components/tactical/pages/__tests__/TacticalTargetPage.test.tsx |
Related¶
- ADR-0089 — full design, ratified parameters, canon reconciliation.
- ADR-0008 — the ship registry this work implements and extends.
ship-registry.md— registry cross-link.fleet-tactics.md— fleet-loot transfer is the only involuntary/combat-driven cross-team credit transfer; this is the first consensual one.