Skip to content

Haggling

Price negotiation between a player and a station's trader. Haggling is optional — every transaction can complete at posted prices — but successful negotiation shifts the price toward the player by single-digit to low double-digit percentages. Two parallel modes are supported: numerical (offer/counteroffer) and narrative (free-form persuasive prose evaluated by an LLM).

Haggling triggers from the trade UI after a player selects a commodity and quantity but before they confirm. The player chooses Accept, Numerical haggle, or Narrative haggle. Choosing either haggle mode starts a session bound to that single (station, commodity, direction, quantity) tuple — sessions don't carry across commodities or visits.

The haggling outcome multiplies the station's posted price; the underlying trading.md pricing model, faction reputation modifiers, and rank trade bonus all stack on top of whatever discount or premium haggling produces.

Modes at a glance

Mode Inputs Evaluator Status
Numerical Offer credits, up to 4 rounds Deterministic vs. station's perceived fair price 🚧 Partial
Narrative Free-form prose, single submission per round LLM scored against context, originality, personality fit 📐 Design-only

Both modes share the same outcome surface: a final multiplier in [0.80, 1.20] applied to the posted price, plus optional reputation deltas with the station's controlling faction.

Numerical haggling

Status: 🚧 Partial — service hooks exist on TradingService, the round loop is not wired to the trade UI yet.

Round structure

A numerical session runs up to 4 rounds. Each round:

  1. Player submits an offer (credits per unit).
  2. Station compares the offer to its perceived fair price — the posted price adjusted for the station's personality, the player's standing, and any prior offers in the session.
  3. Station replies with one of:
  4. Accept — transaction executes at the agreed price; session ends.
  5. Counteroffer — station proposes a price between the player's offer and its fair price; player may accept, counter, or walk.
  6. Reject — offer is too far from acceptable range; player loses one round but session continues.
  7. After 4 rounds without acceptance the session closes and the player must transact at the posted price or walk away. Closing on a reject locks that commodity for the remainder of the docking session.

Offer reasonableness

The station evaluates each offer as a delta from its fair price:

Direction Delta band Station response
Buy (player buying) offer ≥ fair × 0.97 Accept
Buy fair × 0.90 ≤ offer < fair × 0.97 Counter halfway
Buy fair × 0.80 ≤ offer < fair × 0.90 Counter near fair
Buy offer < fair × 0.80 Reject
Sell (player selling) offer ≤ fair × 1.03 Accept
Sell fair × 1.03 < offer ≤ fair × 1.10 Counter halfway
Sell fair × 1.10 < offer ≤ fair × 1.20 Counter near fair
Sell offer > fair × 1.20 Reject

The bands narrow each round (the station gets more impatient) and shift in the player's favour with positive standing.

Modifiers

Stack multiplicatively on the acceptance bands:

Source Range Direction
Faction reputation tier ×0.97 to ×1.05 Better standing → wider acceptance band
Personal reputation ×0.95 to ×1.05 High personal rep → station leans toward acceptance
Military rank trade bonus 0% to +5% bonus to acceptance threshold Higher rank → station expects shrewder offers (cuts both ways)
Station personality difficulty ×0.85 to ×1.20 See port personalities

Reputation and rank modifiers come from the same sources used by trading.md; no double-application — they affect the acceptance band, not the posted price (which already has them baked in).

Narrative haggling

Status: 📐 Design-only — no implementation today. The provider plumbing for ARIA exists (OPERATIONS/aria.md) but the haggling-specific service does not.

Flow

  1. Player submits a single free-form persuasive statement (max 280 chars) per round, up to 2 rounds.
  2. The narrative-haggling service builds a prompt containing:
  3. The station's personality (type, faction, current trader mood, memory of this player).
  4. Recent player ship state (hull integrity, last sectors visited, cargo manifest, faction standing).
  5. The commodity, quantity, posted price, and the implied target price the player is asking for.
  6. A summary of prior haggling lines this player has used at this station.
  7. The LLM returns a structured response: an acceptance verdict, a counter price (or null), an in-character trader reply (1–3 sentences), and an evaluation breakdown with scores for creativity, originality, context fit, and personality match.
  8. The verdict is enforced server-side: the LLM cannot grant a discount outside the [0.80, 1.20] clamp, and any in-character reply is post-filtered for prompt-injection echoes before reaching the player.

Evaluation axes

Axis What it scores Weight
Creativity Linguistic originality and specificity to scenario 0.25
Originality Distance from prior submissions (see anti-exploitation) 0.30
Context fit Internal consistency with verifiable game state 0.30
Personality match Alignment with the station's personality preferences 0.15

Scores are 0–1; the weighted sum drives the multiplier between the player's target and the posted price.

Context verification

Before scoring, the service deterministically checks any factual claim the player makes against game state. Failed checks zero out the context-fit axis and feed back into the trader's reply.

Player claim Validated against
Ship damage / hull integrity Ship.hull_current / Ship.hull_max
Recent dangerous sector Player.travel_log last 24h
Cargo loss to pirates combat_log entries in last 24h
Faction allegiance Player.faction_reputation ≥ +300 with claimed faction
Specific past trade enhanced_market_transactions filtered by player + station

Verifiably-false claims also incur a small reputation penalty (-5 personal rep, -1 standing with the station's faction).

Port personality types

Every station has a trader_personality JSONB document (see trader personality JSONB) whose type is one of five archetypes. The archetype controls which kinds of narrative appeals land, the base difficulty modifier on numerical haggling, and how long the station remembers a player's prior lines.

Status: 🚧 Partial — type field is present in the trader_personality schema; default values per station class are seeded but the runtime selection of preferred-vs-rejected appeal types is design-only.

Personality Found at Difficulty mod Memory days Preferred appeals Rejected appeals
Federation High-faction-standing core sectors, Class 0–4 ×0.85 (easier) 14 Procedural / paperwork ("I have priority routing credentials"), faction-credential ("Commander Yates vouched for this run"), formal compliance Personal sob stories, contraband solidarity, vague threats
Border Mixed-faction frontier-edge, Class 1–7 ×1.00 (baseline) 30 Economic / mutual-benefit ("we both move 100 units this week, both win"), supply-chain logic, direct survival accounts Aristocratic posturing, paperwork-heavy appeals, pure flattery
Frontier Outer rim, Class 1–7 with low faction control ×1.10 (harder) 60 Personal ("I've run this corridor for six years"), shared-hardship, frontier solidarity, blunt risk acknowledgement Federation-style procedural, luxury / status, anything condescending
Luxury Class 10 hubs and high-prestige Class 6–7 ×1.15 (very hard) 21 Cultural / aesthetic ("this vintage pairs with the Vega cycle"), exclusivity, prestige, tasteful name-drops Survival sob stories, frontier grit, anything that implies the trader is short on credits
Black Market Class 8 and select hidden-network ports ×1.25 (extreme) 90 Risk / discretion ("we both walk away with nothing in writing"), contraband solidarity, demonstrated mutual exposure Faction credentials, paperwork, anything that smells of Federation oversight

Difficulty mod multiplies the acceptance band size in numerical haggling — a Federation port at ×0.85 makes the acceptable range 15% narrower for the player; Black Market at ×1.25 widens it. (Yes — Black Market is easier to numerically haggle in raw band terms; the trade-off is its much longer memory and harsher anti-exploitation thresholds in narrative mode.)

Memory days is how long a station remembers each player's haggling lines and outcomes. After memory expires, embedding-similarity scoring against that player's prior lines at that station resets.

Scaling difficulty

Status: 📐 Design-only.

Haggling difficulty scales with player experience so new players get gentle introductions and veterans face genuine resistance. Two inputs:

Input Source Effect
Player.military_rank models/player.py +1% to required offer reasonableness per rank tier (Recruit → Fleet Admiral spans +0% to +12%)
30-day trade volume sum of enhanced_market_transactions.value for last 30 days Brackets: <100k → no penalty; 100k–500k → +3%; 500k–2M → +6%; ≥2M → +10%

The scalar tightens the numerical acceptance bands and raises the LLM's expected creativity floor. New players face a Federation port that accepts offers within 5% of fair; a Fleet Admiral with 5M monthly volume has to come within 1.5%.

The same scalar floors the originality axis in narrative mode — a veteran cannot recycle a phrase that worked at rank 3.

Anti-exploitation

Status: 📐 Design-only — none of these are implemented; all rely on the narrative-haggling service that doesn't exist yet.

The narrative mode is the high-risk surface. Without controls a player could craft one excellent persuasive line and replay it at every station. Four mechanisms defend against this:

1. Embedding-similarity database

Every submitted narrative line is converted to a vector embedding and stored with its outcome. On each new submission the service computes cosine similarity against:

  • The player's prior lines at this station (window: that station's memory_days).
  • The player's prior lines at any station of the same personality type (window: half of memory_days).
  • A global pool of high-scoring lines from all players (rolling 30-day window).
Similarity Effect on originality score
≥ 0.95 Score = 0; trader explicitly calls out the recycle
0.85–0.95 Score halved
0.70–0.85 Score reduced by 25%
< 0.70 No penalty

2. Port memory

Each station carries a per-player record (StationPlayerMemory):

  • Number of haggling sessions and their outcomes.
  • Average originality score over the window.
  • Last-seen timestamp.

Repeated visits with declining originality trigger a fatigue modifier on the trader's mood — the trader is cordial on first meet, skeptical on the third, dismissive by the sixth low-effort attempt. Fatigue ages out at 1 point per real-time day.

3. Context validation

Already detailed in context verification above. Falsifiable claims are validated; failures both zero the context score and feed an in-character rebuttal back to the player ("My logs say your hull is at 94% — try again.").

4. Cooldowns

Limit Window
Narrative haggling attempts per station 3 per real-time hour
Narrative haggling attempts per player 30 per real-time hour
Same-target-price retries 1 per session

Cooldown counters are server-enforced and visible in the trade UI as a shrinking timer.

Trader personality JSONB

The per-station trader configuration lives in Station.trader_personality (JSONB). The schema — including haggling_difficulty, trust_level, memory_duration, and the personality type enum — is documented in DATA_MODELS/jsonb-schema.md. This document does not duplicate that schema; the constraints there (haggling_difficulty 1–10, trust_level 0–100, memory_duration in days) are the source of truth.

Source map

Concern Path Status
Numerical haggling round loop services/gameserver/src/services/trading_service.py:negotiate_price (target) 🚧 Partial
Narrative haggling service services/gameserver/src/services/ai_haggling_service.py (target, doesn't exist) 📐 Design-only
Embedding-similarity DB services/gameserver/src/services/haggling_memory_service.py (target, doesn't exist) 📐 Design-only
Port-personality archetypes services/gameserver/src/core/trader_personalities.py (target) 📐 Design-only
Trader personality JSONB on station services/gameserver/src/models/station.py (trader_personality column) 🚧 Partial
Context-validation hooks (ship, travel, combat logs) services/gameserver/src/services/player_state_service.py (target) 📐 Design-only
LLM provider plumbing shared with ARIA — see OPERATIONS/aria.md 🚧 Partial
Haggling API surface services/gameserver/src/api/routes/trading.py (extension target) 📐 Design-only

Overall: 🚧 Partial (numerical haggling has service hooks and JSONB schema; round loop and UI are not wired) + 📐 Design-only (narrative haggling, embedding similarity, port memory, context validation, cooldowns, scaling difficulty, archetype-driven appeal preferences).

Numerical haggling is the launch-priority path. Narrative haggling is post-launch and gated behind the ARIA security model.