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:
- Player submits an offer (credits per unit).
- 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.
- Station replies with one of:
- Accept — transaction executes at the agreed price; session ends.
- Counteroffer — station proposes a price between the player's offer and its fair price; player may accept, counter, or walk.
- Reject — offer is too far from acceptable range; player loses one round but session continues.
- 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¶
- Player submits a single free-form persuasive statement (max 280 chars) per round, up to 2 rounds.
- The narrative-haggling service builds a prompt containing:
- The station's personality (type, faction, current trader mood, memory of this player).
- Recent player ship state (hull integrity, last sectors visited, cargo manifest, faction standing).
- The commodity, quantity, posted price, and the implied target price the player is asking for.
- A summary of prior haggling lines this player has used at this station.
- 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. - 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 |
Status footer¶
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.
Related¶
- ./trading.md — main trading flow, pricing model, faction modifiers (haggling stacks on top).
- ../gameplay/aria-companion.md — ARIA's market-intelligence layer adjacent to haggling (ARIA can suggest target prices but does not negotiate on the player's behalf).
- ../../DATA_MODELS/jsonb-schema.md —
Station.trader_personalityshape. - ../../OPERATIONS/aria.md — ARIA security model; the narrative-haggling LLM call is gated through it.