Feature 09: Economy & Marketplace
Overview
The player economy: gold as primary currency, the auction house marketplace for player-to-player trading, NPC vendors, the mail system for item/gold transfers, and gold sinks to prevent inflation. All marketplace transactions are serialized through the economy worker to prevent race conditions.
Dependencies
- Feature 03 (Items & Inventory) — items to trade
- Feature 01 (Authentication) — user accounts for mail
Technical Tasks
1. Database Migrations
- Create migration
0008_create_economy.sql:marketplace_listings: id, seller_id, item_id, price, listing_fee, status (active/sold/expired/cancelled), listed_at, expires_at (48h), sold_to, sold_at- Indexes: status WHERE active, expires_at WHERE active, seller_id
mail: id, sender_id (nullable — system mail), recipient_id, subject, body, gold_amount, item_ids (UUID[]), is_read, deliverable_at, sent_at, expires_at- Index: recipient_id
2. Economy Rules (crates/game)
- Create
src/economy.rs:- Constants:
LISTING_FEE_RATE: f64 = 0.05(5% of list price, deducted upfront)SALE_TAX_RATE: f64 = 0.10(10% of sale price, deducted from proceeds)MAIL_GOLD_FEE_RATE: f64 = 0.05(5% fee on gold transfers via mail)LISTING_DURATION: Duration = 48 hoursMAIL_DELIVERY_DELAY: Duration = 1 hourMAIL_EXPIRY: Duration = 30 days
calculate_listing_fee(price: u64) -> u64calculate_sale_proceeds(price: u64) -> u64: price - sale taxcalculate_mail_gold_fee(amount: u64) -> u64- NPC vendor pricing:
npc_sell_price(item: &Item) -> u64: base value by rarity tiernpc_buy_price(item: &Item, background: Background) -> u64: 50-70% of sell value (some backgrounds get better rates)
- Constants:
3. Marketplace Queries (crates/db)
- Create
src/queries/marketplace.rs:create_listing(pool, seller_id, item_id, price, listing_fee) -> Listingfind_active_listings(pool, filters: MarketplaceFilters, limit, offset) -> Vec<Listing>:- Filters: item_type, rarity, level_range, price_range, name search
find_listing_by_id(pool, id) -> Option<Listing>find_listings_by_seller(pool, seller_id) -> Vec<Listing>complete_sale(pool, listing_id, buyer_id)— used by economy workercancel_listing(pool, listing_id)— return item to sellerexpire_listings(pool) -> Vec<Listing>— find all WHERE expires_at < now AND status = active
4. Mail Queries (crates/db)
- Create
src/queries/mail.rs:send_mail(pool, sender_id, recipient_id, subject, body, gold, item_ids) -> Mailfind_mail_for_character(pool, character_id) -> Vec<Mail>: WHERE deliverable_at <= nowfind_mail_by_id(pool, id) -> Option<Mail>mark_read(pool, mail_id)collect_mail_attachments(pool, mail_id)— move items/gold to characterdelete_expired_mail(pool) -> u64— delete WHERE expires_at < now AND is_read
5. Economy Worker (crates/workers)
- Create
src/economy_worker.rs:- Single-consumer queue — only one instance processes the
marketplace-buyqueue to prevent race conditions handle_marketplace_buy(job):- BEGIN TRANSACTION
- Load listing (SELECT FOR UPDATE) — verify still active
- Load buyer character (SELECT FOR UPDATE) — verify gold >= price
- Deduct gold from buyer
- Add sale proceeds (price - 10% tax) to seller’s gold
- Transfer item ownership to buyer (set location to ‘mail’)
- Create mail to buyer with purchased item
- Create mail to seller with gold notification
- Update listing status to ‘sold’
- COMMIT
handle_auction_expiry():- Run every 5 minutes via scheduler
- Find all expired active listings
- Return items to sellers via mail
- Update listing status to ‘expired’
- Single-consumer queue — only one instance processes the
6. Marketplace Routes (crates/api)
- Create
src/routes/marketplace.rs:GET /api/marketplace:- Query params: type, rarity, level_min, level_max, price_min, price_max, search, page
- Return paginated listings with item details
POST /api/marketplace/list:- Body:
{ character_id, item_id, price } - Validate: item owned, in backpack/bank, not equipped, price > 0
- Check seller has available listing slots (default 10, up to 30 with purchases)
- Deduct listing fee (5% of price) from seller’s gold
- Move item to ‘listed’ location
- Create listing record
- Body:
POST /api/marketplace/buy/:listing_id:- Body:
{ character_id } - Validate: buyer has enough gold, listing is active, buyer != seller
- Do NOT directly modify gold — enqueue
marketplace-buyjob - Return
{ status: "processing" }(buyer sees result on next poll)
- Body:
DELETE /api/marketplace/:listing_id:- Cancel own listing, return item to backpack
- Listing fee is NOT refunded
7. Mail Routes (crates/api)
- Create
src/routes/mail.rs:GET /api/characters/:id/mail:- Return delivered mail (deliverable_at <= now)
- Include: sender name, subject, body, gold amount, item summaries, is_read, sent_at
POST /api/mail/send:- Body:
{ sender_id, recipient_name, subject, body, gold_amount?, item_ids? } - Validate: sender owns items, has enough gold + transfer fee
- Deduct gold + fee from sender, remove items from inventory
- Create mail with deliverable_at = now + 1 hour
- Body:
POST /api/mail/:id/collect:- Move attached items to character’s backpack
- Add gold to character’s balance
- Mark as collected (prevent double collection)
8. NPC Vendor Routes (crates/api)
- Add to
src/routes/inventory.rs(or new file):POST /api/characters/:id/sell-to-vendor:- Body:
{ item_ids: [uuid] } - Calculate NPC price per item, add gold to character, delete items
- Body:
- NPC buy is handled via faction vendors (Feature 13)
9. Client — Marketplace & Mail
- Create
routes/(game)/marketplace/+page.svelte:- Search/filter bar: item type, rarity, level, price range, text search
- Listing grid: item card with name, rarity color, stats preview, price, time remaining
- Buy flow: click listing → confirmation modal → POST buy → show “Processing…” → poll for result
- Sell flow: select item from inventory → set price → list
- My Listings tab: active listings with cancel button
- Create
routes/(game)/mail/+page.svelte(or sidebar panel):- Inbox list: sender, subject, gold/item indicators, read status
- Mail detail: body, attachments, “Collect” button
- Compose: recipient name autocomplete, subject, body, attach gold/items
Tests
Unit Tests
calculate_listing_fee: 5% of 1000 = 50calculate_sale_proceeds: 1000 - 10% = 900calculate_mail_gold_fee: 5% of 500 = 25npc_sell_price: scales by rarity tiernpc_buy_price: returns 50-70% of sell price
Integration Tests
POST /api/marketplace/listcreates listing, deducts fee, moves itemPOST /api/marketplace/listrejects equipped itemPOST /api/marketplace/listrejects when insufficient gold for feePOST /api/marketplace/listrejects when at listing slot limitGET /api/marketplacereturns active listings with filtersPOST /api/marketplace/buy/:idenqueues job, economy worker transfers gold/item- Economy worker: buyer gold decreases, seller gold increases by correct amounts
- Economy worker: item transferred to buyer via mail
POST /api/marketplace/buy/:idon already-sold listing → 409DELETE /api/marketplace/:idreturns item to seller, no fee refund- Auction expiry: expired listings return items to seller via mail
POST /api/mail/sendwith gold deducts gold + fee, creates deliverable mailPOST /api/mail/sendwith items removes items from sender inventoryGET /api/characters/:id/mailonly returns mail where deliverable_at <= nowPOST /api/mail/:id/collectmoves items/gold to characterPOST /api/mail/:id/collecttwice → 409- Concurrent buy attempts on same listing: only one succeeds (economy worker serialization)
POST /api/characters/:id/sell-to-vendorgrants gold and deletes items