Economy Module — EmojiPay
Location: apps/api/src/modules/economy/
Aggregate roots: EmojiWallet, EmojiPayment (append-only), PostBoost
Feature flag: emoji-pay — must be enabled via admin settings.
Core Concepts
EmojiPay is a mana-based micro-economy that lets users express appreciation for content through emotional resonance. Mana is not real money in Alpha — no fiat conversion until Wave 3.
Wallet
Every user receives an EmojiWallet on registration via the identity.user_registered event.
| Property | Value |
|---|---|
| Starter balance | 50 mana |
| Daily regeneration | +5 mana |
| Regeneration cap | 50 mana (regen does NOT apply if balance ≥ 50) |
| Regeneration interval | 24 hours (last_regen_at guard) |
The nightly cron job processes all wallets that are below cap and have last_regen_at older than 24h.
Wallet Fields
{
balance: number; // Current mana
lifetimeEarned: number; // Total mana received ever
lifetimeSpent: number; // Total mana sent ever
lastRegenAt: Date; // Guard for regen cadence
}Emotions
8 canonical emotions on the emotion wheel:
| Slug | Display |
|---|---|
love | Love |
awe | Awe |
joy | Joy |
curiosity | Curiosity |
gratitude | Gratitude |
insight | Insight |
calm | Calm |
courage | Courage |
Payment Tiers
6 tiers with different mana costs and reputation requirements:
| Tier | Name | Mana | Rep Required* |
|---|---|---|---|
| 0 | Spark | 1 | 0 |
| 1 | Wave | 3 | 0 |
| 2 | Storm | 10 | 10 |
| 3 | Surge | 25 | 20 |
| 4 | Tempest | 50 | 35 |
| 5 | Nova | 100 | 50 |
*Rep required = minimum reputation in the post's primary interest. Only enforced when postId is provided (post context).
Payment Flow
sequenceDiagram
participant Sender
participant API
participant Receiver
Sender->>API: POST /economy/pay {postId, emotion, tier}
Note over API: Validate tier rep threshold<br/>Debit sender wallet<br/>Split across co-authors<br/>Credit receiver(s)
Note over API: Append-only EmojiPayment row
API-->>Sender: {payment, senderBalance}
API->>Receiver: notification (via EventBus)Co-Author Mana Split
When a post has accepted PostCoAuthor rows, the payment is split:
- Primary author receives:
(100 - sum(accepted splitShare)) %of the mana - Each co-author receives their
splitShare % - Server enforces: total accepted splitShare ≤ 90 (primary always keeps ≥ 10 %)
Example: post with one co-author at splitShare = 30, payment of 10 mana:
- Co-author receives: 3 mana
- Primary author receives: 7 mana
Append-Only Ledger
Never UPDATE an emoji_payments row.
Reversals are a new row with reversed: true. The balance re-credit is a separate transaction. This ensures the full payment history is always auditable.
Concurrency Invariant
Every mana-moving operation runs inside a transaction that pessimistically locks the wallet row before reading the balance:
SELECT user_id FROM "emoji_wallets" WHERE "user_id" = $1::uuid FOR UPDATE;This closes a TOCTOU (time-of-check / time-of-use) race where two concurrent debits could both pass the balance check and overdraw the wallet. The same lock pattern guards pay, boost, and the guild-treasury operations below.
Boost System
Authors can spend mana to boost their post's reach in the feed algorithm.
| Tier | Cost | Score Multiplier | Duration |
|---|---|---|---|
| 1 | 10 mana | ×1.5 | 24 hours |
| 2 | 25 mana | ×2.0 | 48 hours |
Post.boostScoreis the materialised multiplier (0 = not boosted)Post.boostExpiresAtis when the current boost expires- A cron job runs to clear expired boosts (set
boostScore = 0,boostExpiresAt = null) post_booststable stores all boost purchases — append-only
Boost API
POST /posts/:id/boost
Body: { tier: 1 | 2 }EmojiBreakdown
All post listing endpoints include an emojiBreakdown object showing aggregated payments per emotion:
{
"emojiBreakdown": {
"love": { "count": 12, "total": 45 },
"curiosity": { "count": 3, "total": 9 },
"awe": { "count": 1, "total": 10 }
}
}API Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /economy/wallet | My current wallet state |
| POST | /economy/pay | Send EmojiPay (body: postId?, replyId?, emotion, tier) |
| GET | /economy/payments?direction=sent|received | Payment history |
| GET | /economy/payments/:postId/breakdown | Emoji breakdown for a post |
Guild Treasury
Guilds hold a shared mana treasury (guild_treasuries) managed under /api/v1/treasury. Members contribute mana; stewards disburse it. See Guilds for the governance model.
| Method | Path | Description |
|---|---|---|
| GET | /treasury/:guildId | Treasury balance + ledger |
| POST | /treasury/:guildId/contribute | Member donates mana into the treasury |
| POST | /treasury/:guildId/disburse | Steward pays a single recipient |
| POST | /treasury/:guildId/distribute-by-contribution | Split a pool across members weighted by their contribution score |
Invariant: disburse and distributeByContribution lock the treasury row (SELECT guild_id … FOR UPDATE) inside the transaction — this closes a TOCTOU overdraw race where two stewards could both disburse the same balance. Contributions debit the contributor's wallet and credit the treasury in one atomic step.
Marketplace
Mana also settles peer offerings in the marketplace (/api/v1/economy/market via MarketplaceService). Listings are part of the unified content stream (offering type) and surface in admin under the Market content kind. See Content Contexts for the listing model and lifecycle.
Admin
The admin Economy page shows:
- Wallet balances sorted by balance / lifetime earned
- Recent payment ledger (all transactions)
- Boost activity log
- Ability to trigger manual regen for testing