_Docs/
Get StartedModulesPlatformDeployCookbookChangelogReference
_Stack
_Modules
  • Ledger
    • Quick Start
    • Core Concepts
      • Accounts
      • Transactions
      • Constraints
      • Source/destination
      • Designing a Chart of Accounts
    • Working with the Ledger
      • Assets & Currency conversion
      • Bi-temporality
      • Bulk processing
      • Filtering queries
      • Idempotency
      • Data isolation with buckets
      • From credit/debit to source/destination
      • Streaming to analytics systems
      • Ledger Schema
    • Advanced Topics
      • Architecting for scale
      • Events Publishers
      • Performance model
  • Numscript
  • Connectivity
  • WalletsEE
  • FlowsEE
  • ReconciliationEE
  1. Modules
  2. Ledger
  3. Core Concepts
  4. Designing a Chart of Accounts
Ledger

Designing a Chart of Accounts

A chart of accounts is the set of accounts you model in your ledger and the conventions you use to name them. Get this right early and the rest of your integration falls into place — Numscripts read naturally, queries filter cleanly, and an auditor can read your books without a tour guide.

Structuring Account Hierarchies#

Use colons (:) to compose address segments into a hierarchy. The segments are just strings — Ledger doesn't interpret them — but consistent conventions let you query, group, and reason about accounts at scale.

users:{user_id}:wallet:main
users:{user_id}:wallet:pending
users:{user_id}:payment:{payment_id}

merchants:{merchant_id}:earnings
merchants:{merchant_id}:settlements

platform:fees
platform:revenue
platform:taxes:{tax_type}

orders:{order_id}:authorization
orders:{order_id}:capture
orders:{order_id}:refund

Why this works#

  1. Filtering: query every account under a prefix (e.g. users:123:*).
  2. Organisation: related accounts live next to each other in any listing.
  3. Scalability: adding new account types is just another segment — no restructure.

How much of your treasury do you model in the ledger?#

The world account is a built-in "infinite source" — it can always send funds, regardless of balance. It's the natural placeholder for "money came from outside" without committing to where outside.

Whether to lean on world or to model your asset side explicitly is the central design question for a product ledger. There's a spectrum, and where you sit depends on what role you want the ledger to play.

Use world as the asset side (default for fintech product ledgers)#

For most fintech use cases — third-party funds ledgering, embedded finance, neobank-style products — the product ledger's job is to track liabilities: what the platform owes to each user, merchant, partner, or internal pool. The asset side (the actual cash sitting in safeguarding accounts, FBO arrangements, custody providers) is a treasury concern, often spread across many providers, and changing that map shouldn't ripple through your product code.

A user deposit might land across six different safeguarding accounts depending on currency, region, partner bank, and risk policy. Folding all of that into the same ledger means every product feature has to know about treasury topology. Instead, the product ledger treats funds as coming from world and lets a separate system reconcile against the actual asset-side accounts.

Numscript
// Product ledger only cares that Alice now has a claim worth $100.
send [USD/2 10000] (
  source = @world
  destination = @users:alice:wallet
)

This keeps the product ledger small, focused on the liability side, and decoupled from the asset-side complexity.

Model the asset side explicitly (when treasury belongs in the ledger)#

If you do want the ledger to double as your treasury view — to know exactly which bank account funds sit in, or to enforce that liabilities match asset coverage — replace world with named asset accounts:

treasury:bank:main
treasury:bank:secondary
treasury:bank:eu
treasury:bank:us:main
treasury:custody:circle

A deposit then names its actual landing place:

Numscript
send [USD/2 10000] (
  source = @treasury:bank:us:main allowing unbounded overdraft
  destination = @users:alice:wallet
)

This trades simplicity for tighter coupling: every deposit / payout / settlement flow has to know which asset account moved, and a treasury reshuffle (new safeguarding partner, account migration) is a ledger change. It's the right call when the ledger is your treasury system, less so when treasury is a distinct concern handled upstream.

Pool the asset side (paired with Payments cash pools)#

The two approaches above pull in opposite directions — one keeps the ledger blissfully unaware of treasury topology, the other pulls every bank account into the chart. A pairing with the Payments module's cash pools lets you have both at once.

Model a small, stable set of pool accounts in the ledger — each one standing for "value held somewhere across this group of underlying accounts":

treasury:pool:eu
treasury:pool:us
treasury:pool:cards
treasury:pool:wires

A deposit names the pool, not the specific bank:

Numscript
send [USD/2 10000] (
  source = @treasury:pool:us
  destination = @users:alice:wallet
)

Each ledger pool has a matching Payments cash pool on the operational side, aggregating the real-world PSP / safeguarding / FBO accounts. A reconciliation policy compares the ledger pool's balance against the Payments cash pool's aggregate balance and flags any drift. The product ledger stays small and stable; treasury reshuffles happen entirely on the Payments side (new partner bank? add it to the cash pool query — the ledger doesn't change).

This is usually the right answer for fintechs that have grown past "one account at one partner" but don't want every flow in the product to depend on the precise asset layout.

Other legitimate world uses#

Regardless of the broader choice, world is the natural source for:

  • Initial system bootstrapping.
  • Minting new assets you control end-to-end (loyalty points, tokens, in-app credits).
  • Non-funded currency conversions (see Currency Conversion).

Specific account addresses on the liability side#

Whichever side of the asset-modeling spectrum you pick, name your liability-side accounts with as much context as you can:

@users:alice:wallet:main
@users:alice:payment:order-123
@merchants:acme:earnings
@platform:fees

The address is your audit trail and your query surface — users:alice:* should list every account belonging to Alice, payment:order-123 should let you trace a single business event end-to-end. Generic names like revenue or pending lose that context the moment you have two of them.

Modeling Common Financial Scenarios#

E-commerce platform#

├─ customers
│ └─ $customer_id^[a-zA-Z0-9_-]+$
│ └─ walletaccounttype=customer_wallet
├─ merchants
│ └─ $merchant_id^[a-zA-Z0-9_-]+$
│ ├─ earningsaccount
│ └─ settlementsaccount
├─ orders
│ └─ $order_id^[a-zA-Z0-9_-]+$
│ ├─ pendingaccount
│ └─ capturedaccount
└─ platform
├─ feesaccount
├─ taxes
│ └─ vataccount
└─ refunds
└─ poolaccount

Lending platform#

├─ borrowers
│ └─ $borrower_id^[a-zA-Z0-9_-]+$
│ ├─ principalaccounttype=borrower_principal
│ └─ interestaccount
├─ lenders
│ └─ $lender_id^[a-zA-Z0-9_-]+$
│ ├─ availableaccount
│ └─ deployedaccount
└─ loans
└─ $loan_id^[a-zA-Z0-9_-]+$
├─ outstandingaccount
└─ paymentsaccount

Multi-currency wallet#

├─ users
│ └─ $user_id^[a-zA-Z0-9_-]+$
│ └─ wallet
│ ├─ USDaccount
│ ├─ EURaccount
│ └─ BTCaccount
└─ exchange
├─ liquidity
│ ├─ USDaccount
│ └─ EURaccount
└─ feesaccount

Modeling Liabilities with Negative Balances#

Accounts in Formance can carry negative balances, which is the natural way to represent obligations: a debt, a drawn credit line, a pending settlement, an unsettled invoice. The negative balance reflects the deficit until it's cleared.

By default, accounts can't go below zero. To allow it, add the allowing overdraft clause on the source in your Numscript:

Numscript
send [USD/2 1000] (
  source = @users:alice allowing unbounded overdraft
  destination = @users:bob
)

Or with a cap:

Numscript
send [USD/2 1000] (
  source = @users:alice allowing overdraft up to [USD/2 500]
  destination = @users:bob
)

See Overdraft for the full syntax.

Example: debt settlement#

When a debtor pays down a balance, the same transaction primitive — send — reduces the negative side and increases the creditor's:

  1. Bob lends 100toAlice→Alice′sbalanceis‘−100 to Alice → Alice's balance is `-100toAlice→Alice′sbalanceis‘−100`.
  2. Alice pays back 50→Alice′sbalancebecomes‘−50 → Alice's balance becomes `-50→Alice′sbalancebecomes‘−50`.
Numscript
// Initial debt creation (Bob lends to Alice)
send [USD/2 10000] (
  source = @users:alice allowing unbounded overdraft
  destination = @users:bob
)

// Debt repayment (Alice pays back Bob)
send [USD/2 5000] (
  source = @users:alice
  destination = @users:bob
)

This mirrors standard double-entry: one account's reduction offsets another's increase, no special "liability" type needed.

Summary#

ConceptRecommendation
HierarchyCompose addresses with : to enable filtering and grouping
Asset sideworld (minimal) → named bank accounts (ledger is treasury) → pool accounts paired with cash pools (the usual fintech sweet spot)
Liability sideSpecific, contextual addresses (users:alice:payment:order-123) for traceability
LiabilitiesModel with negative balances + allowing overdraft
Debt settlementStandard send reducing the negative side

For how Formance's source/destination model differs from traditional double-entry, see Source-Destination Accounting Model.

Source/destinationAssets & Currency conversion
On This Page
  • Structuring Account Hierarchies
  • Why this works
  • How much of your treasury do you model in the ledger?
  • Use world as the asset side (default for fintech product ledgers)
  • Model the asset side explicitly (when treasury belongs in the ledger)
  • Pool the asset side (paired with Payments cash pools)
  • Other legitimate world uses
  • Specific account addresses on the liability side
  • Modeling Common Financial Scenarios
  • E-commerce platform
  • Lending platform
  • Multi-currency wallet
  • Modeling Liabilities with Negative Balances
  • Example: debt settlement
  • Summary