agentskills.codes
LE

ledger-invariants

Ledger design — single-entry vs double-entry choice, balance invariants, suspense accounts, reconciliation cadence, close discipline, out-of-balance break alerts. Outputs to `docs/design/ledger-<project>.md`. Reads `/project-classify`; skip XS. Use when user says "ledger", "double-entry", "general l

Install

mkdir -p .claude/skills/ledger-invariants && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14541" && unzip -o skill.zip -d .claude/skills/ledger-invariants && rm skill.zip

Installs to .claude/skills/ledger-invariants

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Ledger design — single-entry vs double-entry choice, balance invariants, suspense accounts, reconciliation cadence, close discipline, out-of-balance break alerts. Outputs to `docs/design/ledger-<project>.md`. Reads `/project-classify`; skip XS. Use when user says "ledger", "double-entry", "general ledger", "GL", "journal entry", "reconciliation", "out of balance", "trial balance", "suspense account", "/ledger-invariants", or before any product moves money / records balances.
479 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

/ledger-invariants — Money-Movement Ledger Design

Invoke as /ledger-invariants. Defines the algebra of how your product records value movement: the invariants that must always hold, the cadence you check them, and the suspense-account workflow when they break.

Why you'd care

Every system that records money will eventually go out of balance. Without explicit invariants + automated break detection, you find out at audit — by which time you've shipped wrong numbers to customers + investors for months and the auditor can't sign your statements. Real cost: a 10-day delayed close at a fintech costs ~$200k-$2M in audit + remediation + counsel fees, and tanks Series-B due diligence.

Pre-flight

  1. Read docs/classify/<project>.md. XS → SKIP.
  2. Read docs/design/data-model.md for entity model.
  3. Read docs/design/idempotency-<service>.md (ledger writes MUST be idempotent).
  4. If regulated: read docs/compliance/regulator-relations-<project>.md.

Inputs

  • Money-movement surfaces: payments-in, payments-out, transfers, fees, refunds, chargebacks, settlements, FX, interest accruals, write-offs.
  • Counterparty types: customers, merchants, vendors, internal accounts, partner banks, processors.
  • Asset classes held: fiat (which currencies), crypto (which assets), securities, gift cards, points/credits.
  • Regulatory cadence: GAAP/IFRS, Reg DD for deposit accounts, Reg E error resolution windows, settlement timing.

Process

  1. Choose ledger paradigm — single-entry, double-entry, or triple-entry:

    ParadigmWhen to useWhen to avoid
    Single-entry (one row per movement)Internal counters, points/credit, prepaid usage without real moneyAnytime real money flows, regulated activity, multi-party movement
    Double-entry (debit/credit pair per entry, sum = 0)Default for any product handling money, GL, settlement, payoutsAlmost never wrong — overhead worth it
    Triple-entry (cryptographic third-party witness)Crypto custody, on-chain settlement, high-trust marketplace escrowOperational overhead; only when counterparties distrust each other

    Default = double-entry. Single-entry is technical-debt-at-birth for any product that eventually adds payouts/refunds/FX.

  2. The canonical invariants — these must hold at every commit boundary:

    • I1 — Conservation: sum(debits) == sum(credits) per journal entry. Always. No partial entries.
    • I2 — Account equation: assets = liabilities + equity at every point-in-time across the chart of accounts.
    • I3 — Trial balance: Sum of all account balances across the chart sums to zero (after closing entries).
    • I4 — Period close immutability: Once a period is closed, no journal entry may post to it. Adjustments must use a reversal + reposting in current period.
    • I5 — Ledger continuity: account.balance = sum(entries.amount where account = X up to date). Recomputable from history (event-sourced).
    • I6 — Idempotency: Same external event id → at most one journal entry posted. Replay safe.
    • I7 — Causality: Every journal entry traces to an external business event (payment, transfer, refund, etc.) and is reproducible from it.
    • I8 — Currency homogeneity: A single journal entry posts in one currency; cross-currency requires explicit FX entries with rates pinned to a date.
    • I9 — Cash on hand ≥ user liabilities: For products that hold customer funds (FBO accounts), the segregated balance at the bank ≥ sum of user-facing balances at all times. (Real fines: PayPal $25M, Robinhood $30M, FTX terminal failure.)
    • I10 — Settlement-aware available balance: User's available ≠ user's total. Funds in transit / pending hold / freeze must be carved out. Show both balances.
  3. Chart of accounts (COA) skeleton:

    1xxx Assets
      1100 Cash — operating (per bank per currency)
      1200 Cash — customer FBO (per bank per currency)
      1300 Receivables
      1400 Prepaid expenses
    2xxx Liabilities
      2100 Customer balances (per user)
      2200 Payables
      2300 Deferred revenue
      2400 Tax payable
    3xxx Equity
      3100 Retained earnings
      3200 Contributed capital
    4xxx Revenue
      4100 Transaction fees
      4200 Subscription
      4300 Interchange
      4900 Other revenue
    5xxx Cost of revenue
      5100 Processor fees
      5200 Bank fees
      5900 Reversal losses
    6xxx Operating expenses
    7xxx Other (interest, FX)
    9xxx Suspense / clearing
      9100 Cash-in suspense
      9200 Cash-out suspense
      9300 FX clearing
      9900 Unreconciled
    

    Every user has a sub-ledger account under 2100. Internal-control rule: a user-facing account can only credit/debit via posted journal entries — never direct UPDATE.

  4. Journal-entry schema (minimum viable):

    CREATE TABLE journal_entry (
      id              uuid PRIMARY KEY,
      external_id     text UNIQUE NOT NULL,  -- idempotency
      description     text NOT NULL,
      business_event  text NOT NULL,         -- 'payment.captured' etc.
      posted_at       timestamptz NOT NULL,
      effective_date  date NOT NULL,         -- accounting period
      period_id       text NOT NULL,         -- YYYY-MM, FROZEN once closed
      created_by      text NOT NULL,
      reversal_of     uuid REFERENCES journal_entry(id)  -- corrections
    );
    CREATE TABLE journal_line (
      id              uuid PRIMARY KEY,
      entry_id        uuid REFERENCES journal_entry(id),
      account_id      text NOT NULL,          -- 1100, 2100-<user_id>, ...
      direction       text NOT NULL CHECK (direction IN ('debit','credit')),
      amount          numeric(20,4) NOT NULL CHECK (amount > 0),
      currency        char(3) NOT NULL,
      metadata        jsonb
    );
    -- I1 enforced by trigger or app:
    -- sum(debit) - sum(credit) == 0 per entry per currency
    

    Use integer minor units (cents, satoshis) or numeric(20,4) — never floats.

  5. Event → entry mapping table — every business event names its journal recipe:

    EventDebitCreditNotes
    payment.captured (card → user wallet)1100 cash-operating2100-<user> customer balanceNet of fees in separate entry
    processor.fee5100 processor fees1100 cash-operatingFee posted concurrently
    refund.issued2100-<user>1100 cash-operatingReverses original
    payout.requested2100-<user>9200 cash-out suspensePending bank confirmation
    payout.confirmed9200 cash-out suspense1100 cash-operatingSuspense cleared
    payout.returned1100 cash-operating2100-<user>Reverses if NSF/bounce
    fx.conversion1100 src9300 FX clearingTwo-leg if rate differs
    interest.accrued5x interest expense2400 interest payableDaily accrual
    chargeback.received2100-<user> + 5900 loss1100 cash-operatingLoss recognition

    This table IS the design — every new feature must add rows before code ships.

  6. Suspense / clearing accounts — the "I don't know yet" pattern:

    • Use a suspense (9xxx) account when one leg is known but the other isn't yet (e.g., money received but customer not identified)
    • Hard rule: every entry to suspense gets a workflow task with SLA (T+3 days resolve, escalate at T+5)
    • Daily suspense report: balance > 0 = work to do; aged buckets (0–3d / 3–7d / 7d+)
    • Suspense should trend to zero — non-zero steady state means a broken upstream pipeline
  7. Reconciliation cadence — how you prove the books match reality:

    ReconCadenceSource ASource BTolerance
    Bank vs GL (cash)DailyBank statement1100 ending balance$0 (any break = break)
    Processor vs GLDailyStripe / Adyen settlement file1100 + 5100 movements$0
    User sub-ledger sum vs 2100Dailysum(2100-<user>)2100 control account$0 (I2)
    FBO bank vs user liabilitiesDailyFBO bank balancesum(2100-<user>)$0 (I9)
    FX revaluationDailyFX rates9300 + open positions$0
    Trial balanceDailysum of all account balances0$0 (I3)
    Subledger tie-outs (invoice, payout, refund)DailyModule DBGL$0
    Bank feesMonthlyBank statement5200$0
    Tax payableMonthlyTax engine2400$0

    Every break = open ticket. Owner = Controller / Treasury. Aging buckets reported to CFO weekly.

  8. Period close discipline — monthly + annual:

    • T+1 to T+5: subledger close (AR, AP, payroll, depreciation)
    • T+5 to T+8: bank recs + sweeps + accrual entries
    • T+8 to T+10: management close — preliminary trial balance
    • T+10: period locked in software; flag set per I4
    • T+10 to T+15: financial statements + management report
    • Adjustments after lock = reversal in current period (never edit prior)
    • Annual close: external audit T+45 to T+90 for SOC1 / GAAP
  9. Out-of-balance break alerts — what fires + when:

    • Real-time: I1 violation (entry doesn't balance) → reject + page (this should be impossible — trigger guards against bugs)
    • Hourly: I9 violation (FBO < user liabilities) → page Treasury + freeze new payouts
    • Daily 06:00: I2/I3 (trial balance not zero, control account doesn't match sublegder) → page Controller + Finance Eng
    • Daily 09:00: bank recon break > threshold → ticket
    • T+3 / T+5 / T+7: suspense aging → escalating ticket / page
  10. Anti-patterns (real finance disasters):

    • Allowing UPDATE on balance fields → makes I5 unverifiable. Use journal entries only.
    • Single-entry with periodic "fix-up" jobs → tech debt eats the company; never reach SOC1
    • Float arithmetic on money — banker's rounding silently shifts pennies; auditors catch it after months
    • Suspense balance "tolerated" — every dollar in 9900 is a break someone deferred
    • No period_id lock — late-a

Content truncated.

Search skills

Search the agent skills registry