Our previous articles covered how Canton works without a global ledger and eight production use cases in financial markets. This walkthrough puts those ideas into practice, following a single repo trade from asset creation through agreement, settlement, and repurchase. Each step connects the Daml contract model to the Java code and shows what the institutions can see.
One bank has cash it does not need for a few days. Another needs the cash and has government bonds sitting in its account. The second raises money by selling the bonds and agreeing to buy them back later. The cash and the bonds have to change hands together, even though different institutions keep their records.
The demonstration models this arrangement with one Java program per institution. Two banks compete to provide the cash. Once a proposal is accepted, the borrower withdraws the other proposal and the lender reserves the cash. The programs react automatically to these events, but each exchange of cash and securities requires an operator command.
The code is available on GitHub. The repository's README.md contains setup instructions and the manual command sequence. The setup instructions and the limits of the demonstration appear after the walkthrough.
Prerequisites
Docker with Docker Compose: runs the Canton participant nodes, synchronizer services, and Postgres database. The Docker engine must be running before the demo starts.
Daml SDK 3.4.11: compiles the contract model and runs the Daml tests. The current build invokes the Daml Assistant from this SDK.
Java 21: runs the Java desk application and CLI. Use a JDK so the Java code can also be compiled.
Maven: resolves Java dependencies and builds and tests the Java application.
Python 3: serves the local trace page used to inspect participant events with
make watch-trace.
The launchers support Bash on macOS and Linux. They select JDK 21 through JAVA_HOME or PATH, with an additional macOS lookup. JDK21 can select an installation explicitly. The repository's README.md covers installation and environment checks.
The settlement model
A repo is a sale of securities with an agreement to buy them back later. Here, the repurchase price includes interest. The trade spans two sets of records: securities maintained by a depository and cash maintained by a central bank. At both the initial sale and the repurchase, cash and securities must change hands together. Both transfers succeed or neither takes effect.
The demonstration represents both assets as Daml contracts on Canton 3.x. Daml defines the records, the parties allowed to see them, and the operations allowed on them. Each institution runs a participant node with its own database. A shared synchronizer coordinates transactions between these nodes. The demonstration covers assets already represented on Canton. No real depository or central bank payment system is connected, and the balances have no backing outside the demo.
The repository pins Canton 3.5.15 for the nodes and Daml SDK 3.4.11 for compiling the contract model.
Five institutions, five parties
Each institution has one role in the scenario:
| Institution | Role |
|---|---|
| ICSD | Registers the bond and maintains records of securities holdings |
| CentralBank | Creates cash balances and publishes the business date used for settlement |
| AlphaBank | Provides the securities, receives cash and later repurchases the securities |
| BravoBank | Offers to lend cash against the collateral |
| CharlieBank | Competes with BravoBank for the same collateral |
AlphaBank is the borrower. BravoBank and CharlieBank are competing lenders, and either can win.
Each institution is represented by a Daml party, an identity used in contracts and transactions. A contract names its signatories, whose authority is needed to create it, and any observers who can also see it. Operations on a contract are called choices. Each choice names the parties allowed to invoke it, called controllers. A participant node hosts a party and processes its transactions.
This setup hosts one party per participant, so each node's view corresponds to one institution. Canton also supports several parties on one participant or a party hosted across several participants. These relationships are established through signed topology mappings, which record hosting permissions. Knowing a party's identifier is not enough to act on its behalf. Canton's topology documentation describes these mappings.
The bootstrap script sets up the five parties, uploads the contract model to every participant and waits for their identity mappings to become available across the network. It then writes the party identifiers and connection addresses to ledger.properties for the Java application.
What is running
The local setup runs eight containers: Postgres, a sequencer, a mediator and five participant nodes. The seven Canton nodes each have a separate database within the same Postgres instance.

Logical roles and connections in the local setup. Each participant exchanges messages with the synchronizer through its sequencer. D2 source (docs/blog/diagrams/topology.d2).
The synchronizer consists of the sequencer and mediator. The sequencer orders and distributes messages, while transaction contents remain encrypted for their intended recipients. The mediator collects the participants' signed confirmations and determines the transaction outcome without reading those contents. Both services still handle protocol metadata, so encryption does not hide all information about network traffic.
Five Java processes run alongside the containers, one for each institution. These are called desks. Each desk connects to its institution's participant, reads its stream of contract events, and reacts to changes. The command-line interface (CLI) sends instructions to a desk through a local socket. A separate trace utility reads all five participant APIs to show the demonstration from each institution's perspective.
The desks also exchange selected contract data through local folders. This lets a lender use collateral information it cannot see through its own participant, as explained in the trade-agreement step below. Shared folders and a shared Postgres instance simplify the local setup.
Creating the assets
The first Daml template, Instrument, defines a record containing the bond's identity and quoted price. ICSD is its signatory, and the three banks are observers. ICSD supplies the price used when a lender accepts a proposal, so the borrower cannot unilaterally choose that valuation. This relies on ICSD as a trusted price source. The model does not check whether the price is current or appropriate.
In canton/model/Custody.daml, the template declares its fields, who can see the record, its validation rules, and the two choices available to ICSD:
The ensure clause requires a non-empty ISIN and issuer, and a positive price. The ISIN identifies the security. Both choices name ICSD as controller: Reprice replaces the quoted price, while Issue creates a securities holding.
The following commands assume the nodes have been set up and all five desks are running. Prerequisites are listed above. Startup commands appear in "Running and inspecting the scenario".
The command-line tool sends RegisterInstrument to the ICSD desk. Its decision-making component, an actor, checks whether the instrument is already known. If not, it returns a decision containing the command to create it on the ledger. The handler in IcsdActor is shown below:
Instrument.create is generated from the Daml model. The desk submits it to ICSD's Ledger API, waits for the transaction result, and updates its local view with the new contract. Each bank receives the creation event through its own participant's event stream. CentralBank does not receive it because it is not an observer.
After registration, the ICSD desk logs registered DE0001102580. Repeating the command returns that the instrument is already registered and sends no new ledger command. This duplicate check belongs to the Java application. The Daml template itself does not enforce ISIN uniqueness.
The Instrument contract describes the bond but does not give anyone a holding. The next command issues a holding to AlphaBank:
Only ICSD can invoke Issue. It creates a SecurityPosition signed by ICSD and visible to its owner, AlphaBank. Once AlphaBank's desk processes the event, the positions show a holding with a nominal amount of one billion. The other banks can see the bond's reference data, but not AlphaBank's holding.
Existing contract data cannot be edited. A consuming choice archives the contract it acts on and can create replacements. For example, Reprice replaces the old Instrument with one containing the new price. A nonconsuming choice leaves the original contract active. Issue uses this form so that the same bond record can be used to create further holdings.
The cash side follows the same pattern. CentralBank creates CashBalance contracts, which it signs and makes visible to each account owner:
The first three commands fund the banks. AlphaBank needs an initial cash balance to cover the interest due at repurchase, because the cash received at opening covers only the principal. The final command creates the initial BusinessDate on a fresh ledger. Later calls advance this shared business date.
Two lenders, one collateral holding
With the assets in place, the trade can proceed through agreement, opening settlement and repurchase. The diagram separates operator commands from the desks' automatic reactions.

After agreement, the losing proposal is withdrawn and the winner reserves cash. These actions happen independently. Opening settlement and repurchase each require a separate operator command.
Each lender first publishes a CollateralPolicy describing what it will accept: eligible securities, a minimum haircut, quantity and cash limits, currency, the depository and central bank identities, and an expiry date. The haircut reduces the amount lent against the collateral's quoted value. The lender signs the policy and shares it with AlphaBank.
The policy's interest rate is indicative. Acceptance checks the collateral and funding limits, but does not require the proposed rate to meet that indicative rate.
AlphaBank first sets aside the securities:
AllocateSecurity replaces the original SecurityPosition with two contracts. An AllocatedSecurity reserves 365 million for the trade, while a free position holds the remaining 635 million. This reserved holding, or hold, is signed by ICSD and AlphaBank. No lender is assigned to it yet. AlphaBank then sends both lenders a proposal referring to that same hold:
AlphaBank's desk creates two RepoProposal contracts in a single transaction, one for each lender. AlphaBank signs both, and each lender can see its own proposal. Both contracts are created together, although their events may reach the desks at different times.
Giving the lenders access to the collateral
At this point, neither lender is a signatory or observer of the reserved holding. Including its contract ID in a proposal does not give the lender access to the contract's contents.
Before submitting the proposals, AlphaBank's desk retrieves the hold's disclosure data and writes a copy to each lender's folder under .repo-courier/. Each lender includes that data with its acceptance command, allowing Canton to validate use of the contract. This is an explicit disclosure. It provides access to the contract without changing who may invoke its choices or allowing an archived contract to be used again. The explicit disclosure documentation describes the mechanism.
In this local setup, folders replace the authenticated communication channel that would be needed between institutions. Each desk therefore reads two inputs: its participant's event stream and its disclosure folder.
Why only one lender can accept
A lender's desk attempts AcceptProposal once the proposal, policy, instrument, and disclosure data are available. The choice checks the collateral, quoted price, and policy limits. It then invokes AssignSecurity on the shared hold, replacing it with a hold assigned to the successful lender. The same transaction creates a RepoTrade signed by AlphaBank and that lender.
Both proposals refer to one hold, and that hold can be consumed only once. If both lenders attempt acceptance, only one can succeed. The application reports the other attempt as:
The losing lender may never reach that attempt. As soon as AlphaBank's desk sees the agreed trade, it withdraws the remaining proposal. Meanwhile, the winning lender's desk sees the assigned collateral and automatically reserves cash in an AllocatedCash contract. CentralBank and the lender are signatories, and AlphaBank is an observer.
Applying the quoted price and haircut gives a purchase price of 365,000,000 × 98.75 / 100 × (1 − 0.03) = 349,624,375.00 EUR. The securities and cash are now reserved for settlement, but ownership has not changed.
Opening and repurchase
Assume BravoBank wins. Once its cash is reserved, the operator starts opening settlement with the following command. If CharlieBank wins, its name is used instead:
RepoTrade.SettleOpening checks the parties, assets, quantities, currency, amount, reservation references, and business date. The final operations transfer the assets and record the obligation to repurchase:
These three operations form one transaction. It consumes the securities hold, cash hold, and RepoTrade, then creates the new holdings and an OpenRepo recording the outstanding repo. If either transfer fails, none of these changes takes effect.

Both banks have already authorized the trade by signing RepoTrade. The reserved-asset contracts also carry the authority of the depository or central bank and the asset owner. Their transfer choices use that authority to create the replacement holdings. The Java desk submits one command through its own participant, without coordinating separate writes to the securities and cash records.
The relevant participants validate the parts of the transaction they receive and check its input contracts. The sequencer orders the messages, and the mediator collects confirmations and determines the outcome. Together, these steps make the transfers atomic: both take effect or neither does. The Canton protocol paper describes validation and conflict checks in detail.
What each institution sees
The resulting events differ by institution. For a trade won by BravoBank, the table below summarises the contracts and holdings each institution can see:
| Institution | Opening settlement view |
|---|---|
| ICSD | The securities hold is consumed and a holding is created for BravoBank |
| CentralBank | The cash hold is consumed and a balance is created for AlphaBank |
| AlphaBank | Securities are delivered, cash is received and the trade becomes an open repo |
| BravoBank | Cash is paid, securities are received and the trade becomes an open repo |
| CharlieBank | No events from this settlement. Its proposal is withdrawn in a separate transaction |
Both banks see the agreed trade terms. ICSD and CentralBank do not receive the RepoTrade or OpenRepo contracts containing those terms. However, restricted contract visibility does not prevent every inference. CentralBank can link the opening and repayment payments by their references. If it also knows how the contract calculates interest over time, it can estimate the annualised rate from the amounts, allowing for rounding. The payment dates alone do not establish the period during which settlement is late.
The shared business date and repurchase
Repurchase depends on a shared business date represented by BusinessDate. CentralBank signs this contract, and ICSD and the three banks are observers. Settlement choices read it to check whether the required date has been reached. The operator advances it through CentralBank:
The initial date comes from the Java desk's local clock when no calendar exists. After that, explicit commands advance BusinessDate, and the Daml settlement rules use this published value. Changing a bank's local clock cannot make a settlement due earlier.
Each proposal includes trade, purchase and repurchase dates. The desk adjusts the repurchase date to a business day using the modified-following convention, so --days 1 does not always produce one day of interest. For purchase on 14 September 2026 and repurchase on 15 September, the ACT/360 calculation uses one day divided by 360. At 3.5%, interest is 33,991.26 EUR and the repurchase price is 349,658,366.26 EUR. An adjusted date can change both amounts.
When the repo becomes due, AlphaBank's desk reserves the repayment cash and the lender's desk reserves the securities for return. These preparations are automatic. The operator then starts the closing transfer:
OpenRepo.SettleClosing checks and consumes both reserved holdings, returns the securities and pays the repurchase price in one transaction. It also consumes OpenRepo, completing the trade. In the one-day example, AlphaBank again holds one billion nominal of securities and has 49,966,008.74 EUR in cash. The winning lender has earned 33,991.26 EUR. The other lender's cash balance is unchanged.
The Java desks
The automatic steps above use the same structure in all five desks. Each desk maintains a local view of its contracts and passes commands and contract events to its actor. The actor decides what action is needed and returns a Decision containing ledger commands. The desk submits those commands to its participant. Keeping decision logic separate from submission allows it to be tested without running Canton.
For example, an agreed trade and assigned collateral tell the lender's actor that cash must be reserved. It first checks whether that reservation already exists. If not, it selects a balance or merges smaller balances to fund it. Later contract events update the local view before the next decision. The desk also checks its disclosure folder and retries decisions that are waiting for another contract.
| Trigger | Automatic response |
|---|---|
| Register, issue and fund | Record the resulting reference data and holdings |
| Publish a policy and propose a trade | Lenders attempt acceptance once the required contracts are available |
| Acceptance commits | AlphaBank withdraws the other proposal. The winner reserves cash |
| Settle the opening | Counterparties record the open repo and new holdings |
| Advance the business date | AlphaBank reserves repayment cash. The lender reserves the securities for return |
| Settle the closing | Counterparties record repayment and returned securities |
After submission, the desk updates its view from the returned transaction events. Other institutions receive events through their own participant streams. Generated Java classes provide the constructors and choice arguments used to build commands. Daml validates the business rules when those commands reach the ledger.
Decisions are handled independently, so a rejected proposal does not block other work. A recognised rejection by the contract rules stops further attempts for that proposal within the running desk. A temporary connection error leaves it eligible for retry. Recovery after a lost event stream or an uncertain transaction result is incomplete, as described in "Limits of the local setup".
The command-line tool exposes the operations used in this walkthrough. The Daml model also defines WithdrawPolicy and MergeSecurity, but the CLI does not expose policy withdrawal and the desks do not merge securities automatically. A collateral reservation therefore needs one securities position large enough to cover it. Cash funding can combine smaller balances.
Running and inspecting the scenario
On a fresh demo ledger, run:
This command starts the containers, sets up Canton, builds the CLI, and runs the five desks through the trade. The script waits until settlement is ready, checks that repurchase removes the active trade from AlphaBank's view, prints the holdings, and stops the desks. The containers and database contents remain available. To start again from an empty state, make clean removes that state and the generated artifacts.
The manual command sequence is in docs/presentation.md. The desks must remain running because they react to events between commands. The positions, cash and trades commands read a desk's local view, so the relevant desk must be restarted before using them after the automatic script has finished.
To inspect the participant histories together:
This command serves the trace page and refreshes its data. The utility reads all five participant APIs and groups events by transaction ID. Each column shows one institution's view of the same trade. Access to all five APIs is specific to this inspection tool. Each desk still connects only to its own participant.

The trace screenshot comes from an earlier run with smaller balances and a different collateral-allocation model. It illustrates the layout by institution. Its amounts and collateral visibility differ from the current walkthrough.
The trace is intended for inspecting short runs. Committed rows use the ledger’s effective transaction time, while rejected attempts use the desk’s local clock. The display sorts times without their dates, so it cannot reliably order events across days. It also does not establish latency or throughput, neither of which has been measured.
make test checks the Daml contract rules. make unit checks Java decisions, failure handling and local transport. The trace complements these tests by showing the events received by each participant during a running scenario.
Limits of the model
The demonstration creates securities holdings and cash balances directly. It does not connect to external issuance, custody or payment systems, or reconcile its records with theirs. The results describe the demo's contracts and transactions, not the legal or operational finality of transfers involving real assets.
ICSD is the only valuation source. Acceptance checks its published price, but the model does not check price freshness, compare sources, revalue collateral, request additional collateral or support substitutions. The policy's interest rate is indicative, and its cash limit applies to each proposal rather than to the lender's total exposure.
The business calendar is incomplete. Java excludes weekends and four fixed holidays, while Daml checks weekdays only. The additional Java checks are not enforced by the ledger rules. Near month-end, the date adjustment can also move repurchase back to the purchase date for a short trade. Daml rejects those terms, so the walkthrough then needs a longer duration.
Each settlement is atomic, but accepting a proposal does not guarantee that the trade will be funded or repaid. Owners can release reserved assets before settlement. CancelTrade requires both banks. DeclareDefault consumes an overdue OpenRepo without transferring assets, calculating a close-out amount or implementing a broader default process.
The model also omits negotiation, early termination, rollover, third-party collateral management, central clearing and regulatory reporting. As the payment example showed, an institution may still infer facts from shared references and amounts even when it cannot see the full trade contract. These limits also apply if the institutions run on separate, secured infrastructure.
Limits of the local setup
The contract model uses Canton's proposal and acceptance pattern, choice authorization and explicit disclosure. The Java backend, however, has limited recovery support. Digital Asset's application architecture guidance recommends reliable retries, writes that can be repeated without duplicating their effect, and the Participant Query Store (PQS) for reading ledger data. These practices are only partly implemented here.
Each desk rebuilds its in-memory view by reading contract events from the beginning of the ledger history. It therefore depends on that history remaining available. A longer-lived deployment would need a durable local view or a snapshot of active contracts followed by new events. The trace would still need historical events for its past transaction views. PQS is not integrated.
If an event stream fails or ends, the desk stops and must be restarted manually after the participant recovers. There is no automatic reconnection, persistent record of submitted commands, checks to resolve uncertain results or increasing delays between retries. A request can time out even though its transaction succeeds. Retrying a funding command with a new request ID could then credit the account twice. Reusing the ID lets the ledger recognise a duplicate request, but only within its configured deduplication window. Digital Asset's command deduplication guidance describes these cases.
All institutions run on one machine. Their seven Canton databases share one Postgres instance, and contract disclosures pass through local folders without authenticated transport. The host operator can access all of this data. TLS and API authentication are not configured, development database credentials are included, and the Compose port mappings do not explicitly limit access to localhost.
The synchronizer is administered locally, with bootstrap ownership assigned to the sequencer node's identity. Institutional onboarding, traffic charging, and an agreed upgrade procedure are absent. Each node has one instance, so there is no redundancy or demonstrated recovery from a node or storage failure. Key rotation, key recovery, and party migration have not been tested.
The health checks verify only that a Canton API port accepts a connection, not that a transaction can complete. Postgres is configured for 300 connections without capacity-test validation. Application metrics and performance measurements are also missing.
The build uses the legacy Daml Assistant from SDK 3.4.11. Digital Asset recommends DPM, but migration and validation of this build remain outstanding.



