Economy System

Double-entry ledger, wallet management, fraud detection, transaction processing, and settlement in Aether's virtual economy.

Aether's economy system provides a complete virtual currency infrastructure built on double-entry accounting principles. The aether-economy crate handles wallet management, atomic transactions, idempotency, fraud detection, and settlement processing.

Key Concepts

  • Double-entry ledger -- Every mutation posts a debit and a matching credit entry atomically. The sum across all entries is always zero.
  • Wallets -- User accounts with non-negative balance enforcement and freeze capability.
  • Idempotency -- Duplicate transaction IDs are harmless no-ops returning the original result.
  • Fraud detection -- Velocity checks and large-amount anomaly detection protect the economy.
  • Settlement -- Async payout processing moves records through a lifecycle from queued to completed.

Architecture

The economy system uses a facade pattern with specialized managers for each concern:

EconomyService (facade)
  |
  +-- WalletManager       (create / query / freeze wallets)
  +-- CurrencyLedger      (append-only double-entry journal)
  +-- IdempotencyGuard    (dedup by tx_id)
  +-- FraudDetector       (velocity + anomaly checks)
  +-- SettlementProcessor (payout lifecycle)

All components share data through the EconomyService struct with in-memory storage. A persistence layer wraps this crate for production deployments.

Currency Model

Aether uses AEC (Aether Credits) as its internal currency:

  • Represented as i128 minor units (no floating point).
  • Negative balances are never permitted for player or creator wallets.
  • Platform fee is configurable via the AETHER_ECONOMY_PLATFORM_FEE_BPS environment variable (default: 250 basis points / 2.5%).

Wallet Management

Wallets are created, queried, frozen, and unfrozen through the EconomyService:

use aether_economy::{EconomyService, ServiceError};

let mut service = EconomyService::new();

// Create a wallet for a user
service.create_wallet(user_id, "wallet-alice")?;

// Query balance
let balance = service.get_balance("wallet-alice")?;
assert_eq!(balance, 0);

// Freeze a wallet (blocks all operations)
service.freeze_wallet("wallet-alice")?;

// Operations on frozen wallets fail
let result = service.process_transaction(tx);
assert!(matches!(result, Err(ServiceError::WalletFrozen)));

// Unfreeze restores access
service.unfreeze_wallet("wallet-alice")?;

Double-Entry Ledger

Every transaction creates two entries -- a debit from the sender and a credit to the receiver. The ledger is append-only, ensuring a complete audit trail:

use aether_economy::{LedgerEntry, LedgerRecord};

// After a purchase, the ledger contains:
// - Debit entry: wallet-alice, -500 AEC
// - Credit entry: wallet-bob, +500 AEC
// The sum of all entries is always zero.

The CurrencyLedger enforces the invariant that for every LedgerRecord, the debit amount exactly matches the credit amount. This makes reconciliation straightforward and detectable anomalies obvious.

Transaction Processing

All economy operations (purchases, trades, tips, rewards) flow through a single process_transaction method:

use aether_economy::{EconomyService, TransactionRequest, TransactionKind};

let mut service = EconomyService::new();

// Set up wallets with initial balances
service.create_wallet(alice_id, "wallet-alice")?;
service.create_wallet(bob_id, "wallet-bob")?;

// Process a purchase
let tx = TransactionRequest {
    tx_id: "tx-001".to_string(),
    kind: TransactionKind::Purchase,
    from_wallet: "wallet-alice".to_string(),
    to_wallet: "wallet-bob".to_string(),
    amount: 500,
};

let journal_id = service.process_transaction(tx)?;

The transaction flow enforces multiple safety checks in sequence:

  1. Idempotency check -- If this tx_id was already processed, return the original journal_id.
  2. Fraud evaluation -- Velocity and anomaly checks against the sender.
  3. Balance check -- Verify sufficient funds (overdraft prevention).
  4. Debit sender -- Subtract amount from the sender's wallet.
  5. Credit receiver -- Add amount to the receiver's wallet.
  6. Post ledger entries -- Record the double-entry in the append-only journal.
  7. Record idempotency key -- Store the tx_id to journal_id mapping.

Idempotency

Duplicate transactions are safely handled through idempotency keys:

// First call processes the transaction
let journal_id_1 = service.process_transaction(tx.clone())?;

// Second call with the same tx_id returns the original result
let journal_id_2 = service.process_transaction(tx)?;
assert_eq!(journal_id_1, journal_id_2);

Idempotency keys are retained for a configurable period (default: 30 days via AETHER_ECONOMY_IDEMPOTENCY_TTL_DAYS).

Overdraft Prevention

Balance is checked before any debit. Insufficient funds return an error without mutating state:

let tx = TransactionRequest {
    tx_id: "tx-overdraft".to_string(),
    kind: TransactionKind::Purchase,
    from_wallet: "wallet-empty".to_string(),
    to_wallet: "wallet-bob".to_string(),
    amount: 999999,
};

let result = service.process_transaction(tx);
assert!(matches!(result, Err(ServiceError::InsufficientFunds)));

Fraud Detection

The fraud system combines velocity checks and anomaly detection:

Velocity checks: Configurable maximum transactions per minute per player. Exceeding the threshold blocks the transaction.

Anomaly detection: Transactions above a configurable amount threshold are flagged for review. A fraud score is computed, and transactions above the block threshold are rejected.

use aether_economy::FraudDetector;

let detector = FraudDetector::new(
    60,         // max 60 transactions per minute
    1_000_000,  // anomaly threshold: 1M AEC
    0.8,        // block score threshold
);

// High-frequency transactions trigger velocity block
// Large-amount transactions trigger anomaly scoring

Configuration is driven by environment variables:

VariableDefaultDescription
AETHER_ECONOMY_MAX_TX_PER_MINUTE60Velocity check threshold
AETHER_ECONOMY_ANOMALY_AMOUNT_THRESHOLD1,000,000Anomaly flag amount
AETHER_ECONOMY_FRAUD_SCORE_BLOCK_THRESHOLD0.8Score above which tx is blocked

Settlement and Payouts

The settlement processor manages async payout lifecycles for creator earnings:

use aether_economy::{EconomyService, PayoutRecord};

let mut service = EconomyService::new();

// Enqueue a payout for a creator
let payout = PayoutRecord {
    payout_id: "payout-001".to_string(),
    wallet_id: "wallet-creator".to_string(),
    amount: 10000,
    // ...
};
service.enqueue_payout(payout)?;

// Process the next queued payout
if let Some(completed) = service.process_next_payout() {
    // Payout moved through: Queued -> InFlight -> Completed
}

Payout states:

StateDescription
QueuedPayout is waiting to be processed
InFlightPayout is being processed by the payment provider
CompletedPayout successfully delivered
FailedPayout failed and may be retried

Transaction Types

The economy supports several transaction kinds: Purchase (player buys an item), Trade (player-to-player exchange), Tip (voluntary payment to a creator), Reward (system-issued), and AsyncSettlement (deferred payout). Each kind flows through the same process_transaction pipeline with consistent idempotency, fraud checking, and ledger accounting.

Error Handling

Economy operations return structured ServiceError variants (WalletNotFound, WalletFrozen, InsufficientFunds, FraudBlocked, DuplicateWallet, InvalidAmount). All errors are non-destructive -- failed transactions leave no partial state in the ledger or wallets.