Skip to content

Trading

The economic backbone. Players move commodities between stations whose buy/sell patterns and prices respond to local supply.

Stations (ports)

Codebase calls them stations (models/station.py); user-facing docs call them ports. Same thing. Each station has a class (0–11) that fixes its trading pattern.

Station classes

Source: services/trading_service.py, models/station.py:get_trading_pattern, Resources.aispec.

Class Buys Sells
0 (Sol) special goods + colonists special goods + colonists
1 ore organics, equipment
2 organics ore, equipment
3 equipment ore, organics
4 (Distribution) ore, organics, equipment, fuel
5 (Collection) ore, organics, equipment, fuel
6 ore, organics equipment, fuel
7 equipment, fuel ore, organics
8 (Black Hole) ore, organics, equipment, fuel — (premium prices)
9 (Nova) ore, organics, equipment, fuel (premium)
10 gourmet_food luxury_goods, exotic_technology
11 (Premium Tech Specialist) exotic_technology, luxury_goods exotic_technology, luxury_goods (premium prices both directions)

The Capital Sector of every region holds a Class-0 station that also issues pioneer migration contracts (one cargo unit = one pioneer in cryosleep transit) — see planets/colonization.md. Per-pioneer fee clamp is 30–80 cr (COMMODITY_PRICE_RANGES["colonists"]). The Capital is fixed at sector 1 in Terran Space, randomized within the Federation Zone in player regions, and anchored in the Gateway Plaza cluster of the Central Nexus.

Class 8 / Class 9 premium pricing

Class 8 (Black Hole) buys all commodities at +20% over the standard formula. Class 9 (Nova) sells all commodities at +25% over the standard formula. These multipliers are applied after the base supply/demand calculation but before reputation/rank modifiers.

Status: ✅ Shipped — is_premium_buyer / is_premium_seller flags on Station (models/station.py) drive the multiplier logic in services/trading_service.py:apply_modifiers. The numbers above are the design target.

Class 11 (Premium Tech Specialist)

Class 11 stations are premium tech-specialist ports — they buy and sell only exotic_technology and luxury_goods, with premium pricing in both directions (+25% buy / +25% sell over the standard formula). Functionally they're the high-end equivalent of Class 8/9 generalists, restricted to the most valuable commodities. The legacy advanced_components commodity is not in COMMODITY_PRICE_RANGES and is dropped from the catalog.

Status: ✅ Shipped — services/trading_service.py:apply_modifiers now handles Class 11 via a non-exclusive gate (the Class 8/9 exclusive checks would suppress it). Both directions use get_class_premium(StationClass.CLASS_11, "buy"/"sell") from core/station_class_map.py, applying the +25% / +25% target for exotic_technology and luxury_goods. (confirmed in code 2026-06-16)

Destruction & recovery

A destroyed station enters a 24-hour automated rebuild cycle. During recovery:

  • The station is non-functional (no docking, no trading, no services).
  • Defenses are inactive.
  • Cargo, drones, and credits in storage are preserved.

After 24 hours the station auto-rebuilds at reduced capacity (50% of pre-destruction commodity inventory; defenses must be re-purchased).

Status: 📐 Design-only — auto-recovery logic does not exist in code.

Bang generator commodity gap

🐛 Bug — Procedural-port commodity coverage. The world generator sw2102-bang emits stations with only three of the seven core commodities: fuel_ore (mapped to ore), organics, and equipment. The other four core commodities (gourmet_food, fuel, exotic_technology, luxury_goods) are referenced throughout the spec but do not appear at any procedurally-generated station.

The design target is for all 7 core commodities to be tradable at procedural ports through the binary class-encoding system. Two resolution paths:

  • Extend Bang's commodity enum and port-class generator to emit all 7 commodities.
  • Or post-process Bang output via a translator (see ../../OPERATIONS/bang-integration.md) to inject the missing commodities at gameserver bootstrap.

Until resolved, gourmet_food / fuel / exotic_technology / luxury_goods are only available via crafting, special locations (Stardock near the Capital Sector — see ../../SYSTEMS/bang-import-pipeline.md#stardock — and TradeDocks), or rare events. This is not the intended Launch state.

Pricing model

services/trading_service.py:calculate_dynamic_price runs a supply/demand formula plus a fixed spread:

supply_ratio = current_quantity / capacity   ∈ [0,1]
midpoint     = base_price × (1.5 - supply_ratio)
sell_price   = midpoint × 1.15        # station charges player
buy_price    = midpoint × 0.85        # station pays player

Result is clamped to commodity-specific ranges:

Commodity min max
ore 15 45
organics 8 25
gourmet_food 30 70
fuel 20 60
equipment 50 120
precious_metals 80 180
exotic_technology 150 300
luxury_goods 75 200
colonists 30 80

The 15% spread guarantees sell_price > buy_price (with a final buy_price = sell_price - 1 fallback if rounding ever inverts). This is the structural source of all arbitrage.

Stock regen

tick_production runs periodically — each commodity advances quantity = min(capacity, quantity + production_rate). Production rates are per-station JSONB on Station.commodities.

Market history

Per-(station, commodity) MarketPrice rows track: - buy_price, sell_price - previous_buy_price, previous_sell_price - price_trend — relative change since last tick - supply_level, demand_level — for analytics - volatility — derived from price_variance

Drives realtime feeds in services/realtime_market_service.py and ARIA market intelligence.

Trade transaction flow

  1. Player navigates ship to station's sector.
  2. Dock (1 turn). Sets Player.is_docked = True, current_port_id.
  3. Trade as many commodities as desired — each transaction is 0 turns.
  4. Buy: credits debited, cargo credited, station stock decremented.
  5. Sell: cargo debited, credits credited, station stock incremented.
  6. Undock (1 turn). Free to move.

TradingService.can_player_trade enforces docking + same-sector before any transaction.

API: services/gameserver/src/api/routes/trading.py.

Faction price modifiers

Player's faction reputation modifies the station's effective price (see factions-and-teams.md) — ✅ Shipped (trading_service.compute_player_price_multiplier, wired into both buy and sell paths in api/routes/trading.py):

Reputation value Trade multiplier
≥ +700 (EXALTED) ×0.85
+500 to +699 ×0.90
+300 to +499 ×0.95
+100 to +299 ×0.97
−99 to +99 (NEUTRAL band) ×1.00
−299 to −100 ×1.05
−499 to −300 ×1.15
−699 to −500 ×1.30
≤ −700 (PUBLIC ENEMY) ×1.50

Personal reputation also adjusts pricing: ≤ −500 = +20% markup, ≥ +500 = −10% discount (personal_reputation_service.get_reputation_info). ✅ Shipped — trading_service.compute_player_price_multiplier maps Player.reputation_tier through the per-tier band (Villain ×1.20 .. Legendary ×0.90).

Military rank stacks on top with the rank-defined trading bonus (0% to +50% from Recruit to Fleet Admiral).

Price-stacking order

Modifiers apply in a fixed order, and the commodity hard bands are the final clamp (canon, blessed by Max 2026-06-14). ✅ Shipped:

  1. supply/demand midpoint + the 15% spread (sell_price / buy_price);
  2. multiplicative modifiers — faction reputation × personal reputation × military rank × Class-8/9/11 premium — stacked on the spread price;
  3. then clamp to the commodity-specific [min, max] ranges above.

The clamp runs after the premium multiplier, not before it: premium pricing can push a price toward its band edge, but no stack of modifiers may carry a price outside the commodity's hard band. This keeps the bands an absolute floor/ceiling regardless of how modifiers combine.

Haggling

Two negotiation paths exist: numerical haggling (back-and-forth offers, max 4 rounds) and narrative haggling (AI-evaluated persuasive statements with anti-exploitation safeguards). See haggling.md for the full design, evaluation rubric, and anti-gaming controls.

Black market

Off-the-books trades available at certain stations and via specific NPCs — illicit commodities, stolen goods, and reputation-gated routes. See black-market.md.

Trade contracts

Cargo-delivery contracts, escort contracts, and standing orders that pay out on completion. See contracts.md.

TradeDock & shipyard

TradeDocks are rare premium stations combining a high-end trading hub with a 12-slip player shipyard (rentable per-day construction of all ship classes including the Warp Jumper). See tradedock-shipyard.md.

Port ownership

Players meeting trade-volume and faction-standing thresholds can buy a port and earn tariff income, supply priority, and configure pricing/defenses. See port-ownership.md.

Player-facing affordances

  • Trading UI: two-column "Port Selling" vs "Your Cargo" with current prices, trend arrows, and quantity controls.
  • Profit/loss preview before confirming.
  • Per-trade confirmation modal.
  • Market price history charts (via realtime market WebSocket).
  • ARIA recommendations overlay (which commodities, which ports).
  • Cargo hold visualization with drag-and-drop arrangement.

Source map

Topic Path
Trading service (pricing, ticks) services/gameserver/src/services/trading_service.py
Realtime market services/gameserver/src/services/realtime_market_service.py
Market prediction services/gameserver/src/services/market_prediction_engine.py
Station model + class patterns services/gameserver/src/models/station.py
Market price/transaction models services/gameserver/src/models/market_transaction.py (canonical), resource.py (catalog metadata)
Trade API services/gameserver/src/api/routes/trading.py
Admin economy tools services/gameserver/src/api/routes/admin_economy.py, admin_comprehensive.py
Economy analytics services/gameserver/src/services/economy_analytics_service.py
Faction trade modifier services/gameserver/src/services/faction_service.py
Personal-rep modifier services/gameserver/src/services/personal_reputation_service.py

Status: 🚧 Partial — the canonical transaction table (enhanced_market_transactions, defined in market_transaction.py) is the target schema. A legacy resource-enum-keyed market_transactions table (defined in resource.py) still exists in the schema and will be removed once all reads/writes migrate off it.