Codemojex · C6.1 · a dive of The Revenue Ledger and the KeyShop
The house’s own signed book.
Where C5 moves the players’ money, this dive is the house’s. The revenue_ledger (RVL-branded) is the deliberate inverse of players: a signed delta with no non-negative CHECK, no exactly-once index, uniqueness the RVL primary key alone. Every platform movement funnels through one primitive — Wallet.house_post/5 — and the chapter law is conservation over three observable columns.
Grounding — echo/…/codemojex/wallet.ex (house_post/5 · book_house/5 · house_balance/1 · revenue_breakdown/1 · the buy_in/2 booking site) · schemas/revenue_ledger.ex · rooms.ex (SEAM-1 · SEAM-2) · the revenue-ledger story.
Rooms books the seed through book_house (SEAM-1): one deposit_seed row of −100 keys, in the SAME Repo.transaction as the games-row write that opens the pool at 1000 diamonds. The house swings negative — no non-negative CHECK stands in the way (D-1).The inverse of players
schemas/revenue_ledger.exThe players row guards a balance: a non-negative CHECK (players_non_negative), a FOR UPDATE row lock on every mutation, and the buy-in marker’s (player, ref) exactly-once index. The revenue_ledger inverts each choice on purpose — each house post is a distinct accrual, so nothing deduplicates it; the negative seed row is legitimate, so nothing forbids it.
@primary_key {:id, :string, autogenerate: false}
schema "revenue_ledger" do
field :account, :string
field :currency, :string
field :delta, :integer # signed — no non-negative CHECK (D-1)
field :reason, :string
field :ref, :string
timestamps(type: :utc_datetime_usec, updated_at: false) # append-only — no row is ever updated
end
The balance is the sum of rows, never an in-place mutation. The table holds only platform movements — account = "platform", the @house module attribute behind Wallet.house_account/0, the sole account this rung — so the platform balance is a clean aggregate with no player rows to exclude (cm.6 D-1). And because house_post/5 touches no balance column, it needs no player lock and can never breach players_non_negative.
The revenue-ledger story pins the shape: account=platform + currency=keys + a signed delta — a negative seed row exists on the real book, and no non-negative CHECK stands in its way (D-1).
One primitive, five movements
wallet.ex · house_post/5house_post/5 mints EchoData.BrandedId.generate!("RVL"), inserts one signed %RevenueLedger{} row — account, currency, delta, reason, ref — and returns the RVL id. Rooms reaches it through the public boundary alias book_house/5 for the two seam movements (the seed debit, SEAM-1; the void reclaim, SEAM-2); buy_in/2 calls it directly for the per-member credit, inside the buy-in’s own transaction under the games-row lock — atomic double-entry, no new transaction, no new lock.
pool_keys =
Economy.entry_fee_split_keys(
ordinal,
g.start_threshold || 0,
g.first_movers || 0,
g.entry_fee_revenue_percentage || 0,
fee
)
reason = if ordinal <= (g.start_threshold || 0), do: "deposit_recovery", else: "revenue"
house_post(@house, "keys", fee - pool_keys, reason, game) # the exact keys complement
The cut fee − pool_keys is computed from the keys floor directly, never re-divided from the diamonds, so house_keys + pool_keys == fee exactly — the revenue-ledger story proves the partition across the fee × revenue-percentage grid, including a div(pool, 9) ≠ div(pool, 10) divergence cell. cm.7 adds a sixth booking site: the KeyShop settlement books its "purchase" row through the same primitive (C6.2).
The conservation check
wallet.ex · revenue_breakdown/1The chapter law, stated exactly: Σ(player key debits) == Σ(house key credits, excl. the deposit legs) + Σ(pool key portions) — conservation over three observable columns: the players.keys deltas, the revenue_ledger rows, and games.prize_pool ÷ 10. Explicitly not Σ all-ledger-rows = 0: the player debit is a bare keys-column move and the pool is a games column, so a naive single-table row-sum does not balance — it is the house balance, not the system total (the D-3 honesty statement).
revenue-ledger story).def house_balance(account \\ @house) do
Repo.all(
from r in RevenueLedger,
where: r.account == ^account,
group_by: r.currency,
select: {r.currency, type(sum(r.delta), :integer)}
)
|> Map.new()
end
def revenue_breakdown(game) do
Repo.all(
from r in RevenueLedger,
where: r.ref == ^game,
group_by: r.reason,
select: {r.reason, type(sum(r.delta), :integer)}
)
|> Map.new()
end
house_balance/1 returns %{"keys" => Σ} for the golden game, and the KeyShop’s "stars" purchase rows sum into their own bucket with no read change (the multi-currency seam, D-5). revenue_breakdown/1 reads the four-member game as %{"deposit_seed" => -100, "deposit_recovery" => 16, "revenue" => 12}; a voided room adds its "deposit_reclaim" leg. The :bigint SUM is cast to :integer — keys is integer-exact (D-2), so finance reads a plain integer, never a Decimal.
References
sources · relatedSources
- Ecto — Repo.transactionthe transactional toolkit behind paired bookings — one transaction, all or nothing.
- Helland — Life beyond Distributed Transactionsentities behind boundaries; the dedicated-table ledger.
Related
- C5.3 — The bank, the pool, and the rakethe pool the buy-in funds
- C6.2 — The KeyShopthe pay-in that books here
- BCS — the architecture lawthe ledger law behind the books