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
i128minor units (no floating point). - Negative balances are never permitted for player or creator wallets.
- Platform fee is configurable via the
AETHER_ECONOMY_PLATFORM_FEE_BPSenvironment 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:
- Idempotency check -- If this
tx_idwas already processed, return the originaljournal_id. - Fraud evaluation -- Velocity and anomaly checks against the sender.
- Balance check -- Verify sufficient funds (overdraft prevention).
- Debit sender -- Subtract amount from the sender's wallet.
- Credit receiver -- Add amount to the receiver's wallet.
- Post ledger entries -- Record the double-entry in the append-only journal.
- Record idempotency key -- Store the
tx_idtojournal_idmapping.
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:
| Variable | Default | Description |
|---|---|---|
AETHER_ECONOMY_MAX_TX_PER_MINUTE | 60 | Velocity check threshold |
AETHER_ECONOMY_ANOMALY_AMOUNT_THRESHOLD | 1,000,000 | Anomaly flag amount |
AETHER_ECONOMY_FRAUD_SCORE_BLOCK_THRESHOLD | 0.8 | Score 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:
| State | Description |
|---|---|
Queued | Payout is waiting to be processed |
InFlight | Payout is being processed by the payment provider |
Completed | Payout successfully delivered |
Failed | Payout 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.