14 KiB
Economy — Currency, Marketplace & Creator Payouts — Plan
Status: Planned Owner: Platform team Feature ID:
economyGodot 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
transactionstable with a signed amount, abalance_aftersnapshot, 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_escrowfor 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/CurrentUserdependency inapp/dependencies.py. All economy endpoints reuse this. - DB: async SQLAlchemy + Alembic; migration
0001_initialis 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 existingassetsrows and/or avatar items. - Content moderation:
docs/architecture/content_moderation.mddefines 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.
GETendpoints 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_itemowned 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 UPDATEwithin 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 candelist/removeand trigger audited reversals. - Audit: the ledger plus
payout_requestsreview 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).