Builder guide
This is Step 9, the final step in the canonical newcomer learning path. Complete Make your first AMM code change first. This guide helps you plan a behavior-changing extension without crossing the DEX, Token Standard, registry, backend, or wallet boundaries by accident.
Three layers, one boundary
Section titled “Three layers, one boundary”Every extension lives in one of three layers, and most extensions succeed or fail on whether they respect the boundary between them.
flowchart TB
subgraph DEX["DEX contracts — market structure"]
P["Pool / PoolRules"]
O["Order / OrderMatchExecution"]
RQ["Rfq / MatchedTrade"]
end
subgraph TS["Token Standard V2 — reservation and settlement"]
SB["AllocationFactory · SettlementFactory_SettleBatch"]
end
subgraph REG["Registry — asset semantics"]
H["Holding · Instrument · choice context"]
end
DEX -->|"builds legs, drives"| TS
TS -->|"moves value through"| REG
- DEX contracts own market structure: orders, pools, LP issuance, RFQ, trades.
- Token Standard contracts own reservation and settlement: a trade is a set of
holder-authored allocations settled by one
SettlementFactory_SettleBatch. - Registry contracts own asset semantics: what a holding is, who may hold it, and the choice context a settlement needs.
A DEX choice never moves a holding itself. It builds the transfer legs and asks the settlement factory to move them, under authority the holder already signed. Any change that blurs these layers shows up later as duplicated state or authority confusion; keep them separate and most extensions stay local.
What this reference is
Section titled “What this reference is”A runnable Canton DEX that:
- represents every asset (base, quote, and LP) as a Token Standard V2 (CIP-0112)
V2.Holding; - uses iterated allocations, so pool reserves and resting orders adjust in place without a re-funding round trip; commitment is selected separately according to each workflow’s exit requirements;
- records an operator
PolicyReceipton every RFQ accept, so dealer ranking is replayable after the fact; - deploys to a Canton testnet participant with the included tooling
(
scripts/deploy-testnet.sh; see Run on a testnet).
It deliberately leaves out a production limit-order-book matcher, order routing, oracle integration, custody, and a compliance/KYC layer. Those belong in forks or deployment-specific services, not the shared templates. See Non-goals.
Before extending the AMM
Section titled “Before extending the AMM”Do not start a second learning route here. Follow the canonical path through the tested first-change tutorial, then use the Daml proof map — AMM pool to locate the exact source choice and smallest proof for the behavior you plan to alter. The allocation surface is the lookup page for the Token Standard contracts beneath those choices.
The four workflow families
Section titled “The four workflow families”The Daml test suite exercises four families. Treat the sections below as a builder’s lookup map; the newcomer curriculum remains the canonical path in the documentation index.
A. Pair and instrument listing
Section titled “A. Pair and instrument listing”Register a tradable pair, and for pool mode its instruments.
Dex/DexPair.daml— the listing: base + quote instrument ids, fee model, trading mode (OrderBook/Pool/Both), and anactiveflag. The mode and flag guide off-ledger discovery/routing; they are not fetched byPoolRulesorOrderMatchExecutionand therefore are not on-ledger settlement gates.Registry/V2.daml— the reference registry’s V2 interfaces plus its registry-specificInstrumentConfig(precision, supply bookkeeping, placeholder requirement records, optional ISIN/CUSIP).- Source and focused checks: Daml proof map — Pair listing metadata.
B. OTC and RFQ settlement
Section titled “B. OTC and RFQ settlement”A bilateral block trade settles as one atomic batch.
Dex/MatchedTrade.daml—MatchedTrade_RequestAllocations(one request per authorizer),MatchedTrade_Settle(groups legs by registry admin, callsSettlementFactory_SettleBatch),MatchedTrade_Cancel.Dex/Rfq.daml+PolicyReceipt.daml— trader RFQ, dealer quotes, then a jointRfq_Acceptthat emits aMatchedTradecarrying an operator-signedPolicyReceiptinSettlementInfo.meta.- Source and focused checks: Daml proof map — RFQ and OTC.
C. Resting orders backed by a V2 allocation
Section titled “C. Resting orders backed by a V2 allocation”A limit order rests in the book, funded by the trader’s own locked allocation.
Dex/OrderFundingRequest.daml— the trader-signed intent.Dex/Order.daml— the operator-boundOrderand itsOrderAllocationRequest. The trader authors the allocation withAllocationFactory_Allocate, so their own authority locks the holding; the operator cannot move it.- Expiring orders commit funding until their deadline. GTC funding remains uncommitted, allowing the trader to withdraw through the standard allocation interface if the venue is unavailable; a later match then fails safely.
Dex/OrderMatchExecution.daml— the atomic match (see the matcher section below).- Source and focused checks: Daml proof map — Resting orders.
D. Constant-product pool
Section titled “D. Constant-product pool”An AMM whose reserves are committed allocations.
Dex/Pool.daml+PoolState.daml+PoolSlice.daml— immutable config, the hot reserves/supply/status, and one committed allocation per slice (each slice is its own contract, passed by cid).Dex/PoolRules.daml—PoolRules_RequestSwap,PoolRules_Swap,PoolRules_Pause,PoolRules_Resume.Dex/PoolLiquidityRules.daml+LiquidityAllocationRequest.daml— the DvP add/remove path (_RequestAddLiquidity/_SettleAddLiquidityand the remove pair), co-signed byoperatorandlpRegistrar.Lp/Policy.daml+Lp/Instrument.daml— the LP token, owned bylpRegistrar, keyed by aV2.InstrumentId, and unaware of pools or orders.- Source and focused checks: Daml proof map — AMM pool.
The off-ledger matcher: where a fork does most of its work
Section titled “The off-ledger matcher: where a fork does most of its work”The on-ledger OrderMatchExecution template settles two opposing allocations
atomically. Everything above it — finding opposing orders and choosing the fill
quantity and price — is operator code, so a fork can rewrite matching without
touching a Daml template.
The operator scans active Orders (/v1/orders), pairs compatible ones (same pair,
opposite side, bid.limitPrice >= ask.limitPrice), sets the fill quantity to
min(remaining) and a policy fill price, then creates and exercises the match in one
submission:
choice OrderMatchExecution_Execute : OrderMatch_ExecuteResult with factoryCid : ContractId V2.SettlementFactory extraArgs : ExtraArgs -- registry choice context for the batch controller operator do ... -- finalize both allocations with the concrete match legs, -- SettleBatch, roll each order onto its next-iteration -- allocation, and write a SettledTradeUsing one createAndExercise keeps funds and orders moving together: the settle
archives both allocations, so an order left pointing at a spent one could neither be
filled nor cancelled. The split is deliberate — matchers change often, settlement
primitives do not.
Wallet integration
Section titled “Wallet integration”The dApp never signs as the trader. Trader-authority writes (placing an order,
authoring add/remove-liquidity or swap allocations with AllocationFactory_Allocate)
go through the connected wallet over the CIP-0103 dApp standard
(prepare → sign → execute): the backend supplies registry context and allocation
specifications, the dApp composes the command, and the wallet signs and submits.
Rfq_Accept is jointly controlled by trader and operator; deployments must
provide both authorities through wallet/delegation or an explicitly enabled
co-submission path. The included RFQ page demonstrates the last option with
configured parties; it is not part of the wallet-intent surface or a public
relay service.
Read endpoints (/v1/pools, /v1/trades, …) are operator-observed and served from
the backend’s indexer cache. Keep self-custodial allocation writes on the wallet
path; any relay path must name and enforce the parties the backend may act for.
CIP-0103 does not impose a one-command limit, but supported wallet gateways may.
The DEX therefore composes one top-level command per wallet approval. Where a
flow needs several allocations at once, it uses the token standard’s batching
utility (Splice.Util.Token.Wallet.BatchingUtilityV2, vendored
under vendor/splice/daml/splice-util-token-standard-wallet/): the wallet
createAndExercises ExecuteBatch, which accepts the request and authors every
named allocation in one transaction. Deploy that DAR alongside the DEX DAR.
Extending the reference
Section titled “Extending the reference”| Goal | How |
|---|---|
| Add a trading pair (BTC/EUR, ETH/USDT, …) | Create a DexPair; add a Pool for pool mode. See Add a trading pair. |
| Issue a new LP token or lifecycle-rich instrument (vested, dividend-bearing) | See Add an LP or instrument. |
| Use a different registry | Keep the DEX services behind registry-client, then configure discovery, choice context, disclosures, and metadata for the target registry. CantonDex.Testing.MockRegistry appears only in Daml test fixtures and is not the deployed backend. See Registry integration. |
| Add a pricing curve (StableSwap, weighted) | Add curve-specific configuration and rules, then reuse the V2 allocation and settlement pattern. No generic curve interface is defined by this package. |
| Change the executable pool fee | Update Pool.feeBps and the constantProductOut quote math. Mirror the value into DexPair.feeModel only where off-ledger listing consumers need it; that record does not gate or price PoolRules_Swap. |
| Add an RFQ policy (oracle-weighted, multi-tier) | Rfq.policyCmp defines the ordering used by applyPolicyPairs; bump policyVersion/policyHash and mirror it in app/web/src/services/rfq-policy.ts. |
| Point at a different participant | Set CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, CANTON_SYNCHRONIZER. See Run on a testnet. |
Whatever you change, keep the layer boundary above intact: DEX contracts own market structure, Token Standard contracts own reservation and settlement, registry contracts own asset semantics.
Your first change
Section titled “Your first change”Use Make your first AMM code change for the complete red/green loop: exact edits, focused test, layer-impact check, full local suite, and live sandbox proof.
For later changes, distinguish a new choice from a new template field. A new choice can leave existing contract construction sites intact. A new field changes the serialized template shape and every construction site must supply it; follow Upgrade discipline before making that edit.
Upgrade discipline
Section titled “Upgrade discipline”Keep the templates as small as possible; do not carry compatibility choices “just in
case”. If an adopter needs to preserve Daml smart-upgrade lineage, follow the
participant’s upload-check rules: new fields Optional and at the end of the record,
choices kept rather than removed, input/result field types stable, no field
reordering. To break compatibility on purpose, rename the package and treat it as a
fresh lineage.
Testing
Section titled “Testing”cd trading-testsdpm test # every in-script Daml suitedpm test -p testDexPairLifecycleUpdates # one named design proofdpm test --files CantonDex/Tests/LifecycleChoiceTests.damlUse -p <test-name> while reading one workflow, then run the whole suite before
handoff. Exact source/test links and focused commands are in the
Daml proof map; broader commands and expected
outcomes are in Getting Started.
Testnet smoke test:
npm --prefix services/operator-backend run live:matched-trade # real V2-standard tradeKeep deployment-specific responsibilities outside the reference core — custody, KYC/compliance, oracle selection, production routing, market surveillance — so the shared templates stay small.
Reference: contract surface
Section titled “Reference: contract surface”DexPair pair listing, fee model, optional public observersPool constant-product pool config, slice-local reservesPoolState / PoolSlice hot reserves+supply+status; one committed allocation per slicePoolRules swap + pause/resume choices over pool statePoolLiquidityRules DvP add/remove-liquidity settle choicesLPTokenPolicy LP instrument supply ledger, record-mint/burn policyLiquidityAllocationRequest operator-issued; carries the add/remove DvP allocation requestOrder resting limit order backed by a V2 allocationOrderAllocationRequest trader-observed allocation request (V2 interface)OrderMatchExecution operator-driven match of two opposing allocationsMatchedTrade bilateral block-trade carrier, optional PolicyReceiptTradeAllocationRequest per-authorizer allocation request for a matched tradeRfq / RfqQuote trader's request for quotes; dealer's quotePolicyReceipt on-ledger record of the operator ranking policy at accept timeRegistry.V2.* reference registry implementing Token Standard V2 interfacesThe Daml package is canton-dex-trading (current version v0.1.4).
Reference: off-ledger layout
Section titled “Reference: off-ledger layout”services/operator-backend/ src/ ledger/ JSON LAPI driver, LedgerSubmitter abstraction indexer/ SQLite indexer, idempotency cache, operator config kv http/ REST endpoints admin/ pair / pool / pricing administrative writes pool/, rfq/, order/, matched-trade/ per-flow modulesapp/web/ src/ services/ HTTP client and wallet handoff boundary pages/ route-level React pages components/ Pool, Trade, Portfolio, Admin, Rfq views wallet/ wallet providers (CIP-0103 SDK, WalletConnect, mock, ...)Where to read next
Section titled “Where to read next”You have completed the canonical newcomer path. Choose the task that matches your extension:
- Reference: HTTP API · Allocation surface · Daml proof map
- Deeper design: Liquidity and custody · Pricing · Non-goals
- Recipes: Add a trading pair · Add an LP or instrument