Files
modbison/docs/planning/economy.md

14 KiB
Raw Permalink Blame History

Economy — Currency, Marketplace & Creator Payouts — Plan

Status: Planned Owner: Platform team Feature ID: economy Godot version: 4.6.2 stable Backend: FastAPI + SQLAlchemy (async) + PostgreSQL + Redis

1. Overview

This document breaks the Economy feature into user stories and right-sized implementation tasks. The feature delivers:

  • Virtual currency ("Credits") — a single, server-authoritative currency with an immutable, append-only transaction ledger.
  • Premium purchase — buying Credits with real money through a pluggable payment-provider hook.
  • Marketplace — user-generated content (UGC) listings where creators sell items (assets, avatar items, experience passes) to players for Credits.
  • Creator payouts — accumulating creator earnings and paying them out to external accounts through a pluggable payout-provider hook.
  • Godot client — wallet/transaction history, marketplace browse/buy/sell, and creator payout screens.

The economy is the monetization backbone of the platform. Because it moves virtual value between real people, correctness (no double-spend, no lost funds, no balance drift) and safety (fraud prevention, moderation, audit) are treated as blocking requirements, not add-ons.

2. Why correctness is the differentiator

The platform is a creator economy (players buy UGC from other players). The economy is the highest-risk surface after chat: a single arithmetic or concurrency bug can mint or destroy value and irreversibly erode trust. Correctness is therefore designed in from the start:

  • One currency, integer units. Credits are whole-number integers. There are no floats, no rounding, and no client-computed totals.
  • Append-only ledger. Every balance change is a row in a transactions table with a signed amount, a balance_after snapshot, a reference to the event that caused it, and an idempotency key so retries cannot double credit or double debit.
  • Atomic, locked mutations. Balance changes run in a single database transaction with row locking (SELECT ... FOR UPDATE) so concurrent purchases/payouts cannot race.
  • Escrow hold. Marketplace proceeds are held in pending_escrow for a configurable window before becoming available to the seller, so fraudulent or moderated sales can be reversed without clawing back paid-out funds.
  • Server-authoritative fees. The platform commission is computed server-side from a configured rate; the client never supplies the fee or the seller's net.

3. Existing constraints (do not re-build)

  • Auth: JWT bearer tokens; get_current_user / CurrentUser dependency in app/dependencies.py. All economy endpoints reuse this.
  • DB: async SQLAlchemy + Alembic; migration 0001_initial is the head. Existing tables: users, sessions, worlds, assets.
  • Redis: configured (REDIS_URL) but unused so far; adopted here for idempotency short-caching and rate limiting.
  • Models: User (owner_id), Asset (owner_id, is_public, asset_type). Marketplace items reference existing assets rows and/or avatar items.
  • Content moderation: docs/architecture/content_moderation.md defines flag vocabulary, age gate, and moderation actions. Marketplace items and listings must inherit and enforce these rules (see §7).
  • Client: Godot 4.6.2, minimal main_menu.gd; no economy UI yet.

4. Key design decisions

Concern Decision
Currency model Single virtual currency "Credits" (CR), integer units. One wallet row per user; no fractional amounts.
Ledger Append-only transactions table: signed amount, balance_after, reference_type/reference_id, unique idempotency_key.
Concurrency SELECT ... FOR UPDATE on the wallet row inside the mutation transaction; never read-modify-write from stale data.
Payments Pluggable PaymentProvider interface with a no-op default that simulates a confirmed payment for dev/testing (clearly flagged). Stripe/PayPal later.
Marketplace items catalog_items (what is sold) + marketplace_listings (a seller's offer with a price/status). One item can have multiple listings over time.
Purchase flow Atomic transaction: debit buyer → hold funds in escrow (pending_escrow) → grant item. Platform fee computed server-side.
Escrow Configurable hold window (default 72h) before pending_escrow becomes available seller earnings. Refunds during the window reverse the sale.
Fees Single configurable MARKETPLACE_FEE_RATE (default 0.30). seller_net = floor(price * (1 - fee_rate)).
Creator balance creator_balances table: available (earned, payable), pending (inside an open payout request), lifetime_earned. Updated in the same transaction as the sale completion.
Payouts Pluggable PayoutProvider.pay() with a no-op default (simulated external ref). Minimum payout threshold, admin review for large payouts.
Idempotency Every money-moving request carries a client-generated Idempotency-Key; duplicate keys return the original result.
Rate limiting Redis token-bucket dependency reused from the social feature for purchase/listing/payout endpoints.
Moderation Listings enforce content flags + age gate; moderators can delist/remove; fraud/chargeback reversal is an audited admin action.

5. User stories

US-1 Currency & wallet

As a player, I can see my Credit balance and transaction history, and the system credits/debits my wallet only through server-authoritative events.

Acceptance criteria

  • Exactly one wallet per user, created lazily on first use.
  • Balance is always a non-negative integer; no operation can make it negative.
  • Every mutation appends an immutable ledger row with amount, resulting balance, reference, and idempotency key.
  • Retrying a request with the same idempotency key does not double-apply.
  • GET endpoints expose current balance and paged transaction history.

US-2 Premium purchase (buy Credits)

As a player, I can purchase Credits with real money and have them credited only after the payment is confirmed.

Acceptance criteria

  • A purchase session is created through the payment provider (or the no-op default) and is idempotent.
  • Credits are granted exactly once, only after a verified provider confirmation (webhook/confirm endpoint), never on client claim.
  • Purchase amounts are bounded and server-defined; clients cannot pick arbitrary prices.
  • Confirmation is idempotent and audited (provider reference recorded).

US-3 Marketplace listing (sell)

As a creator, I can list a catalog item for sale at a Credit price and manage/remove my listings.

Acceptance criteria

  • A listing references a valid catalog_item owned by the seller (or an authorized seller).
  • Price is bounded (min/max) and set server-side; status transitions are explicit (active, sold, delisted, removed_by_moderation).
  • Only the seller (or a moderator) can update/remove a listing.
  • Listings are searchable/filterable (item type, price range, owner).

US-4 Marketplace purchase (buy)

As a player, I can buy a listed item. The purchase is atomic, holds funds in escrow, grants the item, and splits the payment between platform and seller.

Acceptance criteria

  • A single transaction debits the buyer, holds funds in escrow, and grants the item — no partial states on failure.
  • Platform fee and seller net are computed server-side from the configured rate.
  • After the escrow hold window, funds move from escrow to the seller's available balance.
  • Refunds during the hold window reverse the sale and return funds to the buyer.
  • Concurrent purchases of a one-of-a-kind listing cannot both succeed.

US-5 Creator payouts

As a creator, I can view my earned balance, request a payout, and track its status; admins can review and approve/reject payouts.

Acceptance criteria

  • Available balance is derived only from completed (escrow-released) sales and completed payouts; it never goes negative.
  • Payout requests are bounded (min threshold, max per period) and rate-limited.
  • Large payouts require admin review; all payout requests carry an audit trail (requested by, reviewed by, provider reference, status).
  • The payout provider is called exactly once; idempotent on retry.

US-6 Godot client

As a player, I can manage Credits, browse/buy/sell marketplace items, and manage payouts from the game UI.

Acceptance criteria

  • An API client handles JWT auth, HTTP, and idempotency keys for all economy calls.
  • Dedicated screens: wallet + transaction history, marketplace browse/buy, sell-listing, and creator payout.
  • The client never computes balances, fees, or seller nets — it only displays server responses.

6. Task breakdown

Right-sized to 1–3 files / one coherent piece of functionality each. Task IDs are sequential for dependency chaining.

# Task Story Depends on
1 Wallet & ledger models + Alembic migration (wallets, transactions) US-1 —
2 Wallet service (atomic credit/debit, idempotency, locking) + wallet/transactions API US-1 1
3 Premium purchase: PaymentProvider interface + purchase/confirm endpoints US-2 1
4 Catalog item & listing models + migration (catalog_items, marketplace_listings, creator_balances) US-3 1
5 Marketplace listing API (create/list/search/update/remove) US-3 4
6 Marketplace purchase service + API (atomic escrow, fee split, item grant) US-4 2, 4, 5
7 Creator payout request models + API (payout_requests) US-5 2, 6
8 Payout admin review + PayoutProvider + audit US-5 7
9 Marketplace moderation + content-flag/age-gate enforcement cross-cutting 5, 6
10 Godot economy API client (auth + HTTP + idempotency) US-6 2
11 Godot wallet & transaction history UI US-6 10
12 Godot marketplace browse/buy UI US-6 6, 10
13 Godot selling & payout UI US-6 5, 7, 10

7. Phasing

  • Phase 1 (currency + payments): tasks 1–3. The foundation — ledger and money movement must be rock-solid before anything spends Credits.
  • Phase 2 (marketplace): tasks 4–6. Listing and atomic purchase with escrow.
  • Phase 3 (payouts): tasks 7–8. Turn released escrow into creator payouts.
  • Phase 4 (moderation): task 9. Lock down the marketplace against mature content and abuse.
  • Phase 5 (client): tasks 10–13, each landing after its backend dependency.

8. Data model sketch

wallets
  id                bigint PK
  user_id           int UNIQUE FK -> users.id
  balance           bigint NOT NULL DEFAULT 0  CHECK (balance >= 0)
  created_at        timestamptz
  updated_at        timestamptz

transactions        (append-only ledger)
  id                bigint PK
  wallet_id         bigint FK -> wallets.id
  type              enum(credit_purchase, marketplace_purchase,
                        marketplace_sale, escrow_release, payout,
                        refund, admin_adjustment)
  amount            bigint NOT NULL            (signed)
  balance_after     bigint NOT NULL
  reference_type    text                        (e.g. "purchase", "sale", "payout_request")
  reference_id      text
  idempotency_key   text UNIQUE
  metadata_json     jsonb
  created_at        timestamptz

catalog_items
  id                bigint PK
  owner_id          int FK -> users.id
  asset_id          int FK -> assets.id (nullable; null for non-asset virtual items)
  item_type         enum(asset, avatar_item, experience_pass)
  name              text
  description       text
  metadata_json     jsonb
  created_at        timestamptz

marketplace_listings
  id                bigint PK
  catalog_item_id   bigint FK -> catalog_items.id
  seller_id         int FK -> users.id
  price             bigint NOT NULL CHECK (price > 0)
  status            enum(active, sold, delisted, removed_by_moderation)
  created_at        timestamptz
  updated_at        timestamptz

purchases
  id                bigint PK
  listing_id        bigint FK -> marketplace_listings.id
  buyer_id          int FK -> users.id
  seller_id         int FK -> users.id
  price             bigint NOT NULL
  platform_fee      bigint NOT NULL
  seller_net        bigint NOT NULL
  status            enum(pending_escrow, completed, refunded, reversed)
  escrow_hold_until timestamptz
  created_at        timestamptz

creator_balances
  id                bigint PK
  user_id           int UNIQUE FK -> users.id
  available         bigint NOT NULL DEFAULT 0
  pending           bigint NOT NULL DEFAULT 0
  lifetime_earned   bigint NOT NULL DEFAULT 0
  updated_at        timestamptz

payout_requests
  id                bigint PK
  creator_id        int FK -> users.id
  amount            bigint NOT NULL CHECK (amount > 0)
  status            enum(pending, approved, paid, rejected)
  external_ref      text
  provider_ref      text
  requested_by      int FK -> users.id
  reviewed_by       int FK -> users.id (nullable)
  reviewed_at       timestamptz
  created_at        timestamptz

9. Security, anti-fraud & audit

  • Server-authoritative balances/fees: clients never compute totals; amounts are validated server-side against bounds and the current listing.
  • Row locking: wallet and creator-balance mutations use SELECT ... FOR UPDATE within one transaction.
  • Idempotency: every money-moving endpoint requires an Idempotency-Key; duplicate keys return the stored original outcome.
  • Rate limiting: purchase, listing, and payout endpoints are token-bucket rate-limited per user.
  • Escrow & reversibility: funds are not payable until the hold window elapses; chargebacks/refunds reverse before release.
  • Moderation: listings carry the item's effective content flags; mature items require age verification (per content_moderation.md); moderators can delist/remove and trigger audited reversals.
  • Audit: the ledger plus payout_requests review fields form an append-only audit trail for every movement of value.

10. Out of scope for this feature

  • Real Stripe/PayPal/KYC integrations (provider interfaces leave the hooks; the defaults are deterministic no-ops).
  • Subscriptions, recurring billing, or premium memberships.
  • Gift cards, promotional codes, or user-to-user Credit transfers.
  • Partial refunds, chargeback disputes, and seller tax/VAT handling.
  • In-world vending machines / scripted economy triggers (future, built on this ledger via the scripting sandbox).