Skip to content

0089 — Player-to-player direct trading

Status

Accepted (Max, 2026-06-28). Design-only — no code yet. This work delivers the ADR-0008 ship registry foundation and the player-to-player trade window on top of it (single-delivery scope). All balance numbers are ratified (see Ratified parameters and the resolved Open decisions below); they are launch starting values, tunable once the live economy provides flow.

Context

The economy supports station trading (player ↔ market), a team treasury (intra-team transfer), and a peer-to-peer ship sale designed in ADR-0008 (still design-only). There is no general way for two arbitrary players to exchange credits, cargo, ships, and equipment directly. Cross-team gifting, cooperative resupply, and any player-driven market depend on this missing primitive.

Three properties of the current model shape the design:

  • Several assets live in two places. Player holds aggregate counters (credits, attack_drones, defense_drones, mines, genesis_devices, quantum_shards, quantum_crystals, commodities); a Ship separately holds its own cargo, mines, genesis_devices, quantum_charges, equipment_slots, upgrades, and insurance. A trade must name which row owns each traded quantity, or goods move twice.
  • The ship registry is not built yet. models/ship.py has a single owner_id and no per-ship stolen flag (the only is_wanted flag is player-level). ADR-0008's registered-owner/current-pilot split, per-ship stolen flag, and ShipRegistry append log are all design-only. Safe ship ownership transfer requires them, so this work builds the ADR-0008 registry first and layers trade on it.
  • There is no trustworthy server price for an asset. Live station prices are player-manipulable by design (the structural source of arbitrage), and Ship.current_value is a mutable column. A value-based anti-RMT model therefore needs a dedicated reference table.

Direct player trading is the economy's highest-risk surface: credit/item duplication (settlement races), RMT / alt-account funnelling, and confirm-time scams. The design makes duplication impossible under lock, makes confirm-time switches impossible, and aims the anti-RMT levers at the actual funnel play — barter and one-way gifts into fresh accounts — not just credit transfers.

Decision

Deliver, as one feature: (1) the ADR-0008 ship registry (registered-owner / current-pilot split, per-ship stolen_status, append-only ShipRegistry), then (2) a bilateral, co-located, atomically-settled trade window between any two players. Asynchronous offers / a marketplace board are out of scope for v1.

Parties, locality, and single-session enforcement

  • A player initiates a trade with a player in the same sector; the target accepts. A trade including a ship additionally requires both docked at the same port.
  • Cross-team and cross-faction trading is allowed (a galaxy-wide primitive, distinct from the intra-team treasury).
  • One open session per player, enforced by 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. Concurrent /initiate calls cannot both succeed.

Tradable assets — whitelist, canonical owning row, ship-as-bundle

A trade moves only assets on an explicit PLAYER_TRADEABLE whitelist; everything else is non-movable even if held. Each tradeable quantity has a single canonical owning row:

Asset Owning row Notes
Credits Player.credits the credit leg
Commodities Ship.cargo of a named ship "incoming cargo" always targets a specific receiving ship, which is locked
Quantum shards / crystals Player wallet value-dense; gated (below)
Genesis devices, attack/defense drones, mines Player aggregate counters a player pool — distinct from a ship's onboard scalars
Ships Ship row (registry) transfers as a bundle — below
Uninstalled equipment / upgrades standalone item must be uninstalled to trade independently

A ship transfers as an atomic bundle: the hull and its onboard cargo, installed equipment_slots, upgrades, quantum_charges, and onboard mines/genesis_devices move as one unit. An item installed in / loaded on a staged ship cannot be separately staged — settlement asserts no staged equipment/cargo id and no staged onboard scalar intersects a staged ship's contents (kills the trade-ship-and-its-contents double-count). The Player-pool counters and a ship's onboard scalars are distinct assets with separate capacity checks. A Carrier with ships in its hangar may not be traded unless every hangared ship is validated (none is the seller's piloted/last ship; the receiver can hold them) and the hangared rows are in the settlement lock set.

Gated assets. Genesis devices and quantum crystals are reputation/acquisition-gated (genesis: Heroic rep ≥ 250 + 3/week; quantum: warp-gate gating). A receiver below the asset's acquisition threshold cannot receive it (no progression-skip), and gated assets appraise at a gate-bypass premium so they trip value caps early.

Never tradable: turns (the turn pool is ADR-0004/0023; the monotonic cumulative_turn_count of ADR-0042 is audit-critical — trading turns corrupts anti-cheat accounting), reputation and medals (account-bound), a player's currently-piloted or last remaining ship, and any stolen-flagged or under-active-report ship. The last-ship rule is a UX soft-lock (because the retained ship may be a free Escape Pod) — it is not an anti-funnel control; the real ones are the appraisal-based tax and caps.

The trade-window protocol

  1. Open — initiator opens a session (single-session gate) targeting a co-located player; target accepts. Short inactivity expiry.
  2. Stage — each party adds/removes offered items. Every change bumps the session version and resets both confirmations ("lock on change") and captures the staged assets' material attributes (hull, cargo, stolen_status, insurance, equipment_slots, upgrades, quantum_charges, onboard mines/genesis, commodity quantities, appraised value, tax) into the confirmed snapshot. Any install / uninstall / load / combat-damage on a ship in an open session also bumps the version.
  3. Confirm — each party confirms a specific session version, which is recorded. A confirmation is refused server-side if the offer does not currently fit (capacity / ownership) — so doomed deals fail at stage, before either party travels.
  4. Settle — the second confirmation triggers atomic settlement (below).
  5. Cancel / expire — either party may cancel; the target may fast-decline (frees both sessions); inactivity, leaving the sector, or undocking (ship trades) cancels. A short initiator cooldown follows a cancelled/expired session (anti lock-and-abandon grief).

Atomic settlement — the anti-duplication contract

Settlement is a single database transaction with these named invariants:

  • Lock everything, one order. populate_existing().with_for_update() on every row read or mutated — both players, every named ship (source/receiving/hangared) — ordered Player rows then Ship rows, each by str(id) (ids are UUIDs, matching combat_service's str(sid) key; combat_service/construction adopt the same str(id) key as part of this work to prevent cross-feature deadlock). populate_existing is mandatory so already-loaded instances refresh under lock.
  • JSONB writes use flag_modified. Every cargo / equipment_slots / upgrades / insurance mutation calls flag_modified (and counters are reassigned, not mutated in-place) — otherwise SQLAlchemy silently drops the in-place edit, a partial-settle/dupe vector.
  • Versioned terminal transition in the same tx. UPDATE ... WHERE status='OPEN' AND version=:confirmed_version sets SETTLED (unique terminal status → idempotent). Redis is a cache only; the DB row is the single source of truth for status and version, so a stage edit racing a confirm cannot settle.
  • Re-validate the whole offer under lock, per class: each side still owns every staged asset at the agreed quantity and condition (the full snapshot field set, not just ownership); the receiver has capacity for every incoming class — cargo against ship.cargo['capacity'] − ship.cargo['used'] (never ShipSpecification.max_cargo), evaluated net of that hull's own outgoing so symmetric swaps validate; drones/mines/genesis against the receiver's player caps (pool leg) and the target ship's max_* (onboard leg); co-location/docking still holds; a locked FOR UPDATE recount of the seller's non-destroyed ships + a re-read current_ship_id confirm no staged ship is the piloted or only ship (auto-spawned escape pods don't count as "another ship").
  • Apply, then log. 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, never truncates.

Value appraisal

A static, admin-maintained PLAYER_TRADEABLE_PRICES reference table gives every tradeable asset a server reference value, independent of live station prices (which are player-manipulable) and never Ship.current_value. A periodic job sanity-reconciles the table against galaxy-median market price (not any single station). Ship appraisal = hull reference + appraised onboard cargo + installed equipment/upgrade reference values. The table is versioned and audited; this table is a build prerequisite — the tax and all caps are undefined without it.

Anti-RMT model

  • Flat tax (inflation sink). ~5% of the gross appraised value moved is sunk in credits on every trade, including one-way gifts (rounded up, with a minimum fee; if the giver can't pay the sink, the trade is blocked). This is the inflation lever, not the RMT deterrent.
  • Progressive surcharge (the RMT deterrent). A steeply progressive surcharge applies to value above a legitimate-play band and to flows into new/low-rep accounts.
  • Value-windowed caps, not counts. A per-account ceiling on net value sent and received per rolling window, summed across all counterparties (so A→C→B fan-out hits A's send ceiling regardless of path), plus a stricter slowly-decaying per-counterparty sub-limit. A receiving value cap for new/low-rep accounts is ON at launch.
  • Caps and tax apply to every account, all subscription tiers. They are economic friction, not a legitimacy block, so ADR-0056's paid-tier "no block" amnesty (which governs team/vote/beacon legitimacy) does not exempt them. A cluster-level aggregate cap binds at settlement for accounts sharing a detected MultiAccountCluster id (read under the same lock), and 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) — the substrate for the chain detector and economy analytics.
  • No reputation effect. P2P trades do not change faction/personal reputation (prevents rep-farming via alt trades).

Ratified parameters

All anti-RMT and throttle numbers are ratified as launch starting values; the live economy will tune them. They anchor to the credit scale — a normal player nets ~1,000 cr per trade and ~15,000–30,000 cr/day mid-game; hulls span 30,000 (Scout) to 1,500,000 (Carrier); endgame assets reach 1–2M. The progressive surcharge and the value caps key off net value sent (out − in) per account over a rolling 7-day window, summed across all counterparties; the flat 5% sink applies to the gross value each party sends. A balanced trade therefore pays only the sink, while a one-way value flow escalates.

Progressive surcharge — marginal bands on net value sent per rolling 7-day window:

Net value sent / 7 days Surcharge
0 – 50,000 cr 0% (the 5% 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.

Value-window 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

New/low-rep = account age under 14 days or personal-reputation tier below Neutral.

PLAYER_TRADEABLE_PRICES seed and cadence. Credits 1:1; commodities at their 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 a 2× gate-bypass premium so they trip the caps early (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). A weekly job reconciles the table toward the galaxy-median market price; any deviation beyond 2× is flagged for manual review rather than applied automatically.

Throttles — 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 or expired session.

Insurance, equipment, turn cost

  • A traded ship's insurance voids on transfer (ADR-0008); the snapshot captures insurance state and the trade-window UI surfaces the void to the buyer.
  • Independent equipment/upgrade trades require the item uninstalled first; installed equipment moves only as part of a ship bundle.
  • Settle costs 0 turns; a small initiate cooldown / per-day free-initiate budget throttles grief spam (it is not an anti-RMT control — RMT is low-frequency/high-value, governed by the caps).

Data model and surface (target paths)

  • Registry (this work): Ship.registered_owner_id / Ship.current_pilot_id split, Ship.stolen_status, append-only ShipRegistry (models/ship_registry.py).
  • Trade: models/player_trade.pyPlayerTradeSession (parties, staged offers, per-party confirm version, session version, expiry, status) and PlayerTradeLog (durable audit); additive Player.open_trade_session_id; the PLAYER_TRADEABLE_PRICES table.
  • services/player_trade_service.py: open / stage / confirm / cancel and the locked settle() (appraisal + sink + per-class capacity checks).
  • api/routes/player_trade.py: POST /trade/initiate|{id}/offer|{id}/confirm|{id}/cancel, GET /trade/{id}; live staging + the will/won't-fit indicator pushed over the realtime bus.

Consequences

  • Unlocks cooperative play, cross-team gifting, and the ship registry ADR-0008 specified — without held-escrow/dispute machinery (mutual versioned confirm + atomic settle replaces held-escrow for co-located bilateral trades; the contract-escrow model of ADR-0049 (SK13) / ADR-0062 remains for asynchronous contracts).
  • Adds a value-based credit sink that bites barter and gifts, plus a progressive surcharge + value caps that target the funnel.
  • Larger delivery than a trade window alone (it includes the registry foundation), but ships full scope in one coherent feature.
  • Requires the PLAYER_TRADEABLE_PRICES reference table and the str(id) lock-key retrofit in combat_service/construction before it can ship safely.

Canon reconciliation

  • ADR-0008. This work implements ADR-0008's registry design (owner/pilot split, stolen flag, ShipRegistry) and extends its consensual ship sale into the trade window, preserving the instant / no-dispute principle. The value-based sink is a new cost layered on the previously fee-free consensual sale (justified by anti-RMT); it does not reinstate ADR-0008's 30% salvage-transfer fee, which is a different path. ADR-0008 stays immutable; the relationship is recorded as implementation + partial extension on acceptance.
  • Cross-team credit. fleet-tactics.md calls the fleet-loot transfer the only cross-team credit transfer; this adds the first consensual one, leaving fleet-loot the only involuntary / combat-driven one. fleet-tactics.md is scoped accordingly at merge.
  • Escrow / turns. Held-escrow canon is ADR-0049 (SK13) / ADR-0062; the turn model is ADR-0004 / ADR-0023.
  • Treasury whitelist. The team treasury deliberately restricts player-movable resources to credits + quantum_crystals; P2P trade widens that set, governed by the appraisal + sink + caps above.
  • The GLOSSARY "Trading (ship sale)" entry and SYSTEMS/ship-registry.md cross-link this protocol at merge.

Open decisions

Resolved (Max, 2026-06-19): scope is full, building the ADR-0008 registry as part of this work; the flat transfer tax is ~5% of the gross appraised value, sunk in credits; the new/low-rep receiving value cap is ON at launch; P2P trades carry no reputation trigger.

Resolved (Max, 2026-06-28): the progressive surcharge bands, the value-window caps (send/receive ceilings, the per-counterparty sub-limit, the cluster aggregate cap, and the new/low-rep age/rep threshold), the PLAYER_TRADEABLE_PRICES seed values and reconcile cadence, and the initiate-cooldown / free-initiate budget are all ratified — the numbers are in Ratified parameters above. They are launch starting values, tunable once the live economy provides flow data.