---
title: "Book every money movement as balanced · Agent Wranglers"
url: https://agentwranglers.se/playbook/pricing/ledger-and-accounting/double-entry-ledger
id: PRC-18
level: Recommended
applies: payments
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1321
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Book every money movement as balanced double-entry postings

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Ledger and accounting](https://agentwranglers.se/playbook/pricing/ledger-and-accounting.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: PRC-18
- Level: Recommended
- Chapter: [Ledger and accounting](https://agentwranglers.se/playbook/pricing/ledger-and-accounting.md)
- Rule: Record each money event as one journal entry of two or more postings to named accounts, whose debits and credits sum to zero per currency. Reject any entry that does not balance, and never update or delete a posting.
- Check: A database constraint or a test proves that every journal entry sums to zero per currency, and an unbalanced insert fails. As the app role, an UPDATE and a DELETE on the postings table both fail. The sum of all postings in every currency is zero.
- Applies when: The project takes payments.
- How: A single-entry list of charges says what came in, but not what you owe. Double entry shows both sides. A prepaid annual plan debits cash and credits deferred revenue. Each month of service then moves one twelfth from deferred revenue to revenue. Bought credits credit a credits liability, and their use moves it to revenue. Keep a short chart of accounts, such as cash, provider balance, receivables, deferred revenue, credits liability, revenue, fees, refunds and tax payable. Store amounts as signed integers in minor units, with debits positive and credits negative. A balance is the sum of an account's postings. Each row in [Record every money movement in an append-only ledger](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/payment-ledger.md) becomes one journal entry here. So does each row in [Keep credits in a ledger instead of a balance you overwrite](https://agentwranglers.se/playbook/usage-billing/credits-and-prepaid-balances/credit-ledger.md). Corrections are new entries that reverse old ones.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### SQL example

```sql
CREATE TABLE journal_entries (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  occurred_at timestamptz NOT NULL,
  source_id   text NOT NULL UNIQUE,
  memo        text NOT NULL
);
CREATE TABLE postings (
  entry_id     bigint NOT NULL REFERENCES journal_entries (id),
  account      text NOT NULL,
  currency     char(3) NOT NULL,
  amount_minor bigint NOT NULL
);
GRANT SELECT, INSERT ON journal_entries, postings TO app_role;
```

### TypeScript example

```ts
type Posting = { account: string; currency: string; amountMinor: bigint }

export async function postEntry(db, sourceId: string, memo: string, postings: Posting[]) {
  const sums = new Map<string, bigint>()
  for (const p of postings) sums.set(p.currency, (sums.get(p.currency) ?? 0n) + p.amountMinor)
  if ([...sums.values()].some(sum => sum !== 0n)) throw new Error(`Unbalanced entry ${sourceId}`)
  await db.transaction(async tx => {
    const entryId = await tx.insertEntry(sourceId, memo)
    for (const p of postings) await tx.insertPosting(entryId, p)
  })
}
```

### Python example

```python
from collections import defaultdict

def post_entry(db, source_id: str, memo: str, postings: list[dict]) -> None:
    sums = defaultdict(int)
    for p in postings:
        sums[p["currency"]] += p["amount_minor"]
    if any(total != 0 for total in sums.values()):
        raise ValueError(f"Unbalanced entry {source_id}")
    with db.transaction() as tx:
        entry_id = tx.insert_entry(source_id, memo)
        for p in postings:
            tx.insert_posting(entry_id, p)
```

## Related rules

- PAY-21 [Record every money movement in an append-only ledger](https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/payment-ledger.md) in Payments and billing
- USG-6 [Keep credits in a ledger instead of a balance you overwrite](https://agentwranglers.se/playbook/usage-billing/credits-and-prepaid-balances/credit-ledger.md) in Usage-based billing
- PRC-19 [Recognise revenue as it is earned](https://agentwranglers.se/playbook/pricing/ledger-and-accounting/revenue-recognition.md) in Pricing, plans and entitlements
- PRC-22 [Export balanced journals to the accounting system each period](https://agentwranglers.se/playbook/pricing/ledger-and-accounting/general-ledger-export.md) in Pricing, plans and entitlements

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/pricing/ledger-and-accounting/double-entry-ledger">Agent Wranglers pricing, plans and entitlements playbook</a>`. In code you write, add a comment next to it with the same link.

- Next: [Recognise revenue as it is earned](https://agentwranglers.se/playbook/pricing/ledger-and-accounting/revenue-recognition.md)
- [Ledger and accounting](https://agentwranglers.se/playbook/pricing/ledger-and-accounting.md)
