This example shows how to implement omnibus account patterns in Formance using a declarative ledger schema. An omnibus account is a pooled account held at a financial institution that aggregates assets belonging to multiple end-users — the assets represent liabilities to your clients.
Common use cases:
- Banking services holding pooled client funds in a single settlement account
- Financial markets managing custodial accounts on behalf of investors
- Crypto platforms with fiat reserve management across banking partners
This is an illustrative example. Adapt the schema to your specific business requirements, regulatory obligations, and financial practices.
The Complete Schema#
This is the full ledger schema for an omnibus account system. The sections below explain each part.
Chart of Accounts#
The chart section defines three account groups using standard correspondent banking terminology. Nostro ("ours") accounts represent assets you hold at partner institutions. Vostro ("yours") accounts represent liabilities — funds clients hold with you.
Banks (Nostro Accounts)#
These are normal debit accounts — they represent your assets held at partner banks. The $bank_id is typically an IBAN (FR7630004028379876543210943) or routing:account format (021000089:123456789).
The payout sub-accounts isolate each withdrawal as a separate staging area, so you can track the lifecycle of each payout independently.
Clients (Vostro Accounts)#
Normal credit accounts — they represent your liabilities to clients. The credit balance shows funds you owe them.
Platform#
Mixed nature accounts — operational accounts for suspense handling, revenue, and costs. Suspense accounts are normal debit (assets awaiting attribution); revenue and cost accounts follow standard income statement conventions.
Transaction Patterns#
Client Deposit#
The CLIENT_DEPOSIT transaction records an identified deposit. The allowing unbounded overdraft clause on the bank account permits it to go negative — this accommodates the common case where ledger entries are recorded before bank statement reconciliation.
vars {
asset $asset
number $amount
account $bank_id
account $client_id
string $reference
}
send [$asset $amount] (
source = @banks:$bank_id:main allowing unbounded overdraft
destination = @clients:$client_id:main
)
set_tx_meta("reference", $reference)This pattern works for any currency. Pass EUR/2, USD/2, or any asset in Universal Monetary Notation.
Unidentified Deposit#
When funds arrive but you can't identify the client (missing reference, intermediary payment, etc.), UNIDENTIFIED_DEPOSIT parks the funds in a suspense account. You cannot refuse incoming funds to an omnibus account — always book immediately.
vars {
asset $asset
number $amount
account $bank_id
account $platform_name
string $reference
}
send [$asset $amount] (
source = @banks:$bank_id:main allowing unbounded overdraft
destination = @platform:$platform_name:suspense:payin
)
set_tx_meta("reference", $reference)
set_tx_meta("status", "pending_identification")Once the client is identified, SUSPENSE_RESOLUTION moves the funds from suspense to the correct client account.
vars {
asset $asset
number $amount
account $platform_name
account $client_id
string $original_reference
}
send [$asset $amount] (
source = @platform:$platform_name:suspense:payin
destination = @clients:$client_id:main
)
set_tx_meta("original_reference", $original_reference)
set_tx_meta("resolution_type", "client_identified")Monitor your suspense accounts closely. Funds should not remain unresolved for extended periods — most regulatory frameworks require timely resolution.
Client Withdrawal (Payout)#
Payouts are a two-step process:
Reserve
PAYOUT_RESERVE moves funds from the client account to a payout staging account tied to a specific reference. This ensures the client can't spend funds that are being withdrawn.
vars {
asset $asset
number $amount
account $client_id
account $bank_id
string $payout_ref
}
send [$asset $amount] (
source = @clients:$client_id:main
destination = @banks:$bank_id:payout:$payout_ref
)
set_tx_meta("payout_ref", $payout_ref)
set_tx_meta("status", "reserved")Settle
Once the bank confirms the transfer, PAYOUT_SETTLEMENT moves funds from the staging account to the bank's main account, completing the cycle.
vars {
asset $asset
number $amount
account $bank_id
string $payout_ref
string $bank_reference
}
send [$asset $amount] (
source = @banks:$bank_id:payout:$payout_ref
destination = @banks:$bank_id:main
)
set_tx_meta("bank_reference", $bank_reference)
set_tx_meta("status", "settled")If a payout fails, you reverse the reservation by sending from the staging account back to the client.
Why Not Use @world?#
The @world account is Formance's infinite source/sink. While it simplifies examples, omnibus accounting requires explicit tracking of where funds actually are:
- Bank accounts go negative (overdraft) to represent "we received funds but haven't reconciled yet" — this is intentional and meaningful
- Client accounts are liabilities — their balance represents your obligation
- Suspense accounts enable the "book now, attribute later" pattern that omnibus operations require
Using @world would obscure these distinctions and make reconciliation impossible.
Queries#
The queries section defines reusable lookups:
CLIENT_BALANCE— get a specific client's positionPENDING_SUSPENSE— find all unresolved deposits (operational monitoring)INFLIGHT_PAYOUTS— track reserved but unsettled withdrawals (risk management)
These leverage the hierarchical account structure — filtering on :suspense:payin matches across all platforms, and :payout: matches all staging accounts.