# Supply & Demand Economy — Phase 1 Design Status: **Approved direction** (decisions confirmed with product owner, 2026-09-06). Not yet implemented. ## Confirmed decisions | Decision | Choice | |---|---| | Supply model | **Abstract per-item ledger** (`market-state` collection), GM baseline + transaction-driven adjustments. Never derived by scanning physical stockpiles. | | Item scope | **Resources + tradeable assets only.** Vehicles deferred to a later phase. | | Pricing reach | **New NPC listings only** + displayed reference price. Existing listings, player listings, and open negotiations are never repriced. | | Simulation cadence | **New `economy-tick` bin** (external cron, idempotent, SSE notify). `market-tick` consumes the resulting state. | | GM surface | **Minimal per-item** (baseline supply, baseline demand, price band, enable toggle) on Resources/Assets; global knobs on GameRules. | ## Current-state facts this builds on - Price anchors: `Assets.marketTrading.perceivedValue.baseBuyPrice/baseSellPrice` (`src/lib/market/index.ts` — `getBaseBuyPrice`/`getBaseSellPrice`), `Resources.baseValue`, `Structures.sellPrice`. - NPC listing generation: `src/scripts/marketTick.ts` — `autoPrice` (±15%) × `vendor.priceModifier`, `minPrice = price × 0.6`. - Real-demand signal sources that already exist: `bank-transactions` (type `payment`, memo carries asset + qty), `market-listings` (sold/buyer/soldAt), `market:sale` event logs with structured data. - Unwired production engine: `Structures.autoGeneratedResources[]` (min/max amount, frequency, produce chance) — schema exists, no tick consumes it. - Tick bin pattern: `bin[]` registration in `src/payload.config.ts`, `src/scripts/.ts` exporting `script`, `createBinLogger` (`src/scripts/lib/binFileLogger.ts`), SSE notify via `/api/game-tick/notify` with `x-game-tick-secret`. - Negotiation system invariants to protect: NPC stance anchored to asking price, `minPrice` floor, patience meter, strictly-increasing buyer offers (`src/lib/market/negotiations.ts`). ## Architecture ### 1. `market-state` collection (new) — the supply/demand ledger One doc per item (`itemType: "resource" | "asset"`, `itemId`). Fields: - `gmBaselineSupply`, `gmBaselineDemand` — numbers, GM-set (copied from per-item fields on first creation). - `currentSupply` — adjusted by: unit purchases from NPC listings (decrement), unit sales to NPC buyers (increment, later phase), production events (increment). - `artificialDemand` — slow-drifting NPC pressure, updated per tick (random walk bounded around `gmBaselineDemand`). - `realDemand` — decaying rolling aggregate of completed unit purchases (quantity-weighted), decayed each tick by a GameRules decay factor. - `priceModifier` — computed, bounded: derived from demand-vs-supply ratio, clamped to the item's band (`±maxModifier`). - `lastComputedAt`. ### 2. Per-item GM fields (Resources + Assets) - `economy.enabled` (default true for tradeable items) - `economy.baselineSupply`, `economy.baselineDemand` (numbers; defaults derived from rarity) - `economy.maxModifier` (price band, e.g. 0.5 → price can move ±50% of base) ### 3. GameRules global knobs (new group `economy`) - `economyTickIntervalHint` (docs for cron), `realDemandWindowTicks`, `realDemandDecay`, `artificialDemandDrift`, `coldStartBehavior` (use baseline until N observations), global price band default, `economyEnabled` master gate (pattern: `staffHiringEnabled`). ### 4. `economy-tick` bin (new) `src/scripts/economyTick.ts` + logic in `src/lib/economy/`: 1. For each enabled market-state doc (creating missing ones from GM baselines): decay `realDemand`, drift `artificialDemand`, recompute `priceModifier` from `demand / supply` ratio, clamp to band. 2. Cold start: items with no purchase observations keep `priceModifier = 1` (neutral). 3. Emits a batch summary `economy:tick` event + per-item `economy:price-change` events only when the modifier crosses a visible threshold (avoid event spam). 4. SSE notify (same path as other bins). Idempotent: a re-run recomputes the same values from stored state; drift steps are seeded deterministically per period. ### 5. Consumption by market-tick When generating a new NPC auto listing, price becomes: ``` price = round(baseBuyPrice × priceModifier × vendor.priceModifier) ``` `autoPrice` ±15% randomness is dropped (the modifier replaces it) — `minPrice = price × 0.6`, `desiredPrice = price` unchanged, so negotiation invariants are untouched. A "reference price" (modifier-applied base) is displayed on item views for player pricing guidance. Shortage behavior (phase 1): modifier raises prices and NPC restock quantity scales down when `supply < demand` (bounded, never zero without a GM flag). ### 6. Real-demand ingestion Hook points (no new collection needed in phase 1 — read from existing records): - `buyListing` (`src/lib/market/index.ts`) — on successful purchase of an NPC/vendor listing, increment `realDemand` on that item's market-state (quantity-weighted). - Player-to-player sales do **not** move prices in phase 1 (no double counting). ## Out of scope (explicit deferrals) NPC faction economies, autonomous NPC buyers with wallets, production/crafting chains, vehicle market, physical-stock reconciliation, multi-currency, repricing existing listings/negotiations, real-time price streaming (refresh-on-tick only — SSE bus is single-instance). ## Migration & rollout - New collection + fields = **named migration** (`bun run payload migrate:create add_market_economy_state`), registered in `migrations/index.ts`; dev applies via `bun run payload migrate` (`push: false`). Never `db push`. - Existing listings stay at their prices; modifier applies only to newly generated listings. `economyEnabled` master flag allows instant disable. - Regenerate `src/payload-types.ts` after schema changes. ## Top risks (from design review) 1. **Non-atomic mutations** — `buyListing` already credits locker before clearing payment; new flows must not compound this. Define failure/retry behavior before adding ledger writes. 2. **Negotiation breakage** — never reprice under open negotiations (phase 1 avoids entirely by only touching new listings). 3. **Ledger/physical divergence** — abstract supply will not track destroyed/admin-edited stock; GM can reset per-item state from the admin panel (rebuild from baselines). 4. **Tick duplication** — external cron may retry; idempotency + deterministic period boundaries required. 5. **Scope explosion** — every extension (factions, vehicles, production) is a separate approved phase. ## Implementation phases 1. **P1a — schema**: `market-state` collection, per-item economy fields, GameRules knobs, named migration, generated types. 2. **P1b — engine**: `src/lib/economy/` (modifier math, decay, drift, cold start) + unit-testable pure functions. 3. **P1c — tick**: `economy-tick` bin + event types + SSE wiring. 4. **P1d — consumption**: market-tick uses modifier for new NPC listings; restock scaling; reference price display; `buyListing` real-demand ingestion; GM reset action.