Wallgent

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

  1. Every transaction balances. The sum of debits must equal the sum of credits. A deferred constraint trigger rejects unbalanced inserts.
  2. Entries are immutable. Once written, entries cannot be modified or deleted. Corrections are made by posting reversal transactions.
  3. Optimistic balance caching. Account balances are stored on the account record and updated atomically alongside entry creation. A reconciliation script verifies consistency.
  4. Deadlock prevention. Accounts are locked in ascending ID order during transactions (SELECT FOR UPDATE with consistent ordering).

Account Types

TypeNormal BalancePurpose
ASSETDebitSystem Treasury -- holds platform funds
LIABILITYCreditWallet accounts -- funds owed to agents
REVENUECreditFee collection account
EXPENSEDebitPlatform operating costs
EQUITYCreditOwner 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:

FieldDescription
transactionIdParent transaction
accountIdTarget account
amountPositive decimal value
directionDEBIT or CREDIT
previousBalanceAccount balance before this entry
currentBalanceAccount balance after this entry
accountVersionOptimistic concurrency version

Fee Model

Every payment incurs a fee of 0.5% (minimum $0.25). A transfer between two wallets produces three entries:

  1. Debit source wallet (LIABILITY) -- reduces sender balance
  2. Credit destination wallet (LIABILITY) -- increases receiver balance
  3. 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.50

Concurrency Control

The ledger service uses pessimistic locking with consistent ordering:

  1. Sort all affected account IDs in ascending order
  2. Begin a PostgreSQL transaction with READ COMMITTED isolation
  3. SELECT FOR UPDATE on each account in sorted order (prevents deadlocks)
  4. Validate sufficient funds for debits
  5. Insert the transaction and entries with balance snapshots
  6. Update account balances and versions atomically
  7. Commit

On this page