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.
Playerholds aggregate counters (credits,attack_drones,defense_drones,mines,genesis_devices,quantum_shards,quantum_crystals, commodities); aShipseparately holds its owncargo,mines,genesis_devices,quantum_charges,equipment_slots,upgrades, andinsurance. A trade must name which row owns each traded quantity, or goods move twice. - The ship registry is not built yet.
models/ship.pyhas a singleowner_idand no per-ship stolen flag (the onlyis_wantedflag is player-level). ADR-0008's registered-owner/current-pilot split, per-ship stolen flag, andShipRegistryappend 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_valueis 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_idcolumn (nullable, partial-unique), set under the Player row lock at/initiateand cleared on terminal status under the same lock. Concurrent/initiatecalls 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¶
- 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") 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.
- 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.
- Settle — the second confirmation triggers atomic settlement (below).
- 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 bystr(id)(ids are UUIDs, matchingcombat_service'sstr(sid)key;combat_service/constructionadopt the samestr(id)key as part of this work to prevent cross-feature deadlock).populate_existingis mandatory so already-loaded instances refresh under lock. - JSONB writes use
flag_modified. Everycargo/equipment_slots/upgrades/insurancemutation callsflag_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_versionsetsSETTLED(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'](neverShipSpecification.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'smax_*(onboard leg); co-location/docking still holds; a lockedFOR UPDATErecount of the seller's non-destroyed ships + a re-readcurrent_ship_idconfirm 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
ShipRegistryappend), then loads/credits, then thePlayerTradeLogaudit 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
MultiAccountClusterid (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
insurancestate 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_idsplit,Ship.stolen_status, append-onlyShipRegistry(models/ship_registry.py). - Trade:
models/player_trade.py—PlayerTradeSession(parties, staged offers, per-party confirm version, session version, expiry, status) andPlayerTradeLog(durable audit); additivePlayer.open_trade_session_id; thePLAYER_TRADEABLE_PRICEStable. services/player_trade_service.py: open / stage / confirm / cancel and the lockedsettle()(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_PRICESreference table and thestr(id)lock-key retrofit incombat_service/constructionbefore 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.mdcalls 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.mdis 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.mdcross-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.