Double-Entry Ledger
An immutable, balanced double-entry ledger that records every financial movement with full audit trails.
Overview
Wallgent uses a double-entry ledger as the source of truth for all financial data. Every transaction produces balanced debit and credit entries. Entries are immutable -- enforced by PostgreSQL triggers that prevent updates and deletes.
Core Principles
- Every transaction balances. The sum of debits must equal the sum of credits. A deferred constraint trigger rejects unbalanced inserts.
- Entries are immutable. Once written, entries cannot be modified or deleted. Corrections are made by posting reversal transactions.
- Optimistic balance caching. Account balances are stored on the account record and updated atomically alongside entry creation. A reconciliation script verifies consistency.
- Deadlock prevention. Accounts are locked in ascending ID order during transactions (SELECT FOR UPDATE with consistent ordering).
Account Types
| Type | Normal Balance | Purpose |
|---|---|---|
ASSET | Debit | System Treasury -- holds platform funds |
LIABILITY | Credit | Wallet accounts -- funds owed to agents |
REVENUE | Credit | Fee collection account |
EXPENSE | Debit | Platform operating costs |
EQUITY | Credit | Owner equity (reserved) |
System Accounts
The seed script creates four system accounts:
- Treasury (ASSET) -- Platform's main fund pool
- Fees (REVENUE) -- Collects transaction fees
- Expenses (EXPENSE) -- Tracks platform costs
- Suspense (LIABILITY) -- Holds funds during dispute resolution
Transaction Lifecycle
PENDING --> POSTED --> (immutable)
|
v
FAILED
|
v
REVERSED (new corrective transaction)A transaction starts as PENDING, moves to POSTED when the ledger service commits it, or FAILED if validation rejects it. Posted transactions cannot be modified -- only reversed by a new transaction.
Entry Structure
Each entry records:
| Field | Description |
|---|---|
transactionId | Parent transaction |
accountId | Target account |
amount | Positive decimal value |
direction | DEBIT or CREDIT |
previousBalance | Account balance before this entry |
currentBalance | Account balance after this entry |
accountVersion | Optimistic concurrency version |
Fee Model
Every payment incurs a fee of 0.5% (minimum $0.25). A transfer between two wallets produces three entries:
- Debit source wallet (LIABILITY) -- reduces sender balance
- Credit destination wallet (LIABILITY) -- increases receiver balance
- Credit Fees account (REVENUE) -- collects the fee
// Example: $100 transfer
// Fee: max(100 * 0.005, 0.25) = $0.50
//
// Entries:
// Debit source_wallet $100.50
// Credit dest_wallet $100.00
// Credit fees_account $0.50Concurrency Control
The ledger service uses pessimistic locking with consistent ordering:
- Sort all affected account IDs in ascending order
- Begin a PostgreSQL transaction with
READ COMMITTEDisolation SELECT FOR UPDATEon each account in sorted order (prevents deadlocks)- Validate sufficient funds for debits
- Insert the transaction and entries with balance snapshots
- Update account balances and versions atomically
- Commit