Skip to main content

On-chain Settlement

Status: Implemented. Activated per venue by Rocky operations; before activation every fill reports settlementStatus: "internal".

Rocky matches spot orders off-chain and settles them on the Canton Network. For every fill between two customer accounts, the two assets change hands in a single atomic Canton transaction — a delivery-versus-payment (DvP) — directly between the parties' own wallets. Each settled fill yields a Canton updateId, exposed on GET /api/v3/myTrades as cantonUpdateId, that anyone can verify against the ledger.

This page explains the model, the timing and its risks, the status machine, how to verify a settlement yourself, and the rules that follow from it (fees, minimum notional, self-trade prevention).

Participants and segments​

SegmentWhoFundingSettlement
Wallet-fundedInstitutional and retail accounts bound to a Canton party that has completed MemberAuthAssets stay in the participant's own Canton wallet; the exchange holds a mirror balanceEvery fill settles on-chain
Internally-fundedRocky's own market-making accountsExchange custodyBookkeeping in the DvP layer; a fill between two such accounts is recorded as skipped and is committed on-chain as data in the next epoch commitment

A fill between a wallet-funded account and a Rocky house account settles on-chain between the participant's wallet and Rocky's custody party. A fill between two wallet-funded accounts settles wallet-to-wallet. Which segment an account is in is fixed at onboarding (see Institutional Onboarding).

Mirror balances​

For a wallet-funded account, GET /api/v3/account reports a mirror of the on-chain holdings, maintained by a reconciler that runs on a fixed interval (SPOT_MIRROR_SYNC_INTERVAL_MS, default 30 s). It is timer-driven only — it is not triggered by settlement events — and it acts on a non-zero drift only after observing it in two consecutive rounds, so an external change takes about two cycles to land. The reconciler computes, per asset:

drift = on-chain holdings − ( mirror free + mirror locked + unsettled outgoing − unsettled incoming )
  • Deposits: transfer assets into your wallet; once the reconciler has seen the surplus in two consecutive rounds it credits free. There is no deposit endpoint and no confirmation step (/v1/spot/transfer is rejected for wallet-funded accounts with HTTP 422 and an {"error": …} body, see Spot Transfer).
  • Withdrawals: none needed — settled assets are already in your wallet. /api/v3/account reports canWithdraw: false (the field is false for every spot account).
  • Removing assets from the wallet while you have exposure (open orders or unsettled sells) is detected as negative drift: free is reduced first; if that is insufficient, open orders on that asset are cancelled to release locked; if the remaining shortfall exceeds what those cancellations can release (and is more than dust), the account is frozen; a smaller shortfall is re-derived on the next round once the refunds land.

Frozen accounts​

A frozen account is rejected at order entry with -2010 Account is frozen pending settlement review. Existing orders remain cancellable, reconciliation continues, and settlement of in-flight fills continues. Unfreezing is a manual operations action after the on-chain shortfall is covered and the mirror reconciles to zero drift. Contact support via your Integration Pack channel.

Optimistic settlement: timing and risk disclosure​

Settlement is asynchronous and optimistic:

  1. The fill executes and the internal mirror balances move immediately. Your next order can use the proceeds at once.
  2. The settler picks the fill up (poll interval SPOT_CHAIN_POLL_INTERVAL_MS, default 1 s; production runs 2 s), builds the two-leg DvP with the fewest input holdings that cover each leg — as two direct Token Standard transfers in one atomic transaction when both receivers hold a direct preapproval, otherwise through the allocate-and-execute path — and submits it to Canton. A small pool of workers (SPOT_CHAIN_WORKERS, default 2) settles in fill order; two in-flight settlements never spend the same holding, so a chain of trades by one party still delivers in sequence and "sell what you have not yet received" resolves naturally on-chain. Throughput is on the order of a fill every couple of seconds per worker; under sustained load the queue grows and pending lasts longer.
  3. On confirmation the updateId is recorded and exposed.

Net settlement. Fills between the same two parties are not delivered one by one. They are collected into a batch per counterparty pair and settled as one atomic multi-leg DvP with the per-asset net amounts (BIS DvP Model 3: a buy and a later sell of the same size net to nothing on the base asset and to the price difference on the quote asset). A batch opens with the first fill and normally settles 60 s later; while its net exposure in stable quote assets stays small it may wait, up to 15 minutes after opening, for more fills to net against; it settles at once after 500 fills or when its exposure is large. Every fill keeps its own settlementStatus; when the batch confirms, all its fills report the batch's cantonUpdateId, and a batch that nets to zero everywhere is confirmed without a ledger transaction (lastError: "netted-to-zero"). The fill count therefore no longer drives Canton traffic; the number of counterparties you trade against per window does.

What this means for you:

  • Credit exposure between fill and delivery. Until confirmed, your counterparty's delivery exists only as an internal balance movement. Rocky bounds this with the mirror reconciler (which clamps and freezes accounts whose wallets no longer cover their exposure) and requires institutional participants to sign credit terms at onboarding. Treat pending/processing/retry balances as unsettled in your own risk accounting.
  • failed is possible and is a manual case. If a settlement cannot be delivered (for example the counterparty revoked MemberAuth between fill and delivery), the internal books already reflect the fill. Rocky operations are alerted immediately and resolve the position manually with the affected parties.
  • Your own wallet must remain able to deliver. Moving assets out of your wallet while you have unsettled sells triggers the clamp/freeze path above. Keep settlement inventory in the trading wallet.

Settlement status machine​

┌──────────┐ claim ┌────────────┐ confirmed ┌───────────┐
fill ──────► │ pending │ ──────────► │ processing │ ───────────► │ confirmed │
└──────────┘ └────────────┘ └───────────┘
▲ │ transient error
backoff │ ▼
elapsed, │ ┌─────────┐
re-claimed └─ │ retry │
└─────────┘
│ terminal error, or attempt cap
│ (SPOT_CHAIN_MAX_ATTEMPTS, default 20) reached
▼
┌─────────┐
│ failed │
└─────────┘

fill between two house accounts ───────► skipped
no settlement row (pre-activation, or internally-funded segment) ───► internal
StatusTerminalcantonUpdateId
pendingnonull
processingnonull
retrynonull
confirmedyesset
failedyesnull
skippedyesnull
internalyesnull

The full per-value semantics are in Account Trade List. A retry row goes back to processing (never to pending) when its backoff elapses. Backoff is 1 + attempt seconds (attempt capped at 30) for transport and contention errors and 2 + 2 × attempt seconds for sequencer backpressure; errors classified as terminal (missing MemberAuth, Daml authorization failure, unsupported instrument, missing party) go straight to failed without retrying. A row left in processing for 5 minutes (for example after a settler restart) is re-claimed and re-submitted with the same deterministic command id, so a fill can never settle twice. The attempt cap is configuration (SPOT_CHAIN_MAX_ATTEMPTS, default 20). Rocky operations monitor for settlements that stay in processing or retry for more than 10 minutes.

Historical fills are never back-filled: fills executed before on-chain settlement was activated for the venue keep internal permanently.

Every fill is committed on-chain​

Independently of the DvP above, every fill — including skipped house fills and internal fills executed after 2026-09-18 — and every order event is written to an append-only journal in the same database transaction as the event and committed to Canton once per epoch as a Merkle root plus the epoch's fill records. Use GET /v1/chain/proof to obtain an inclusion proof for any tradeId or orderId and verify it against the on-chain root. See Epoch Commitments.

Verifying a settlement on Canton​

Every confirmed fill is one Canton transaction whose root is an exercise of Rocky's MemberAuth Auth_Open choice and which contains exactly two Allocation_ExecuteTransfer exercises nested inside it — one per leg — that share a settlementRef whose id equals the fill's trade id. You can check this with your own Canton participant or with a Canton Scan explorer; Rocky is not in the loop.

  1. Get the ids. Call GET /api/v3/myTrades?symbol=CBTC-USDCB. For the fill, note id (the trade UUID) and cantonUpdateId (a hex string, typically beginning 1220).
  2. Open the update. In Canton Scan, search for the update id, or query the ledger API of a participant that hosts your party (GET /v2/updates/update-by-id or the equivalent for your participant version) with the updateId. Your party is a stakeholder on its own leg, so the transaction is visible to you.
  3. Check the shape. The transaction must contain exactly two exercised choices named Allocation_ExecuteTransfer (the surrounding MemberAuth Auth_Open / Auth_RunWith exercises are the wrapper). Anything else — one leg, three legs, a different transfer choice name — is not a Rocky spot settlement.
  4. Match the reference. In each exercise's argument, read settlementRef.id. Both legs must carry the same value, and it must equal the trade id from step 1. This is what binds the on-chain transaction to the fill you were shown.
  5. Match the amounts and parties. The leg where you are the sender transfers exactly qty of the base asset (if you sold) or quoteQty of the quote asset (if you bought); the other leg transfers the opposite, less your commission when the counterparty is Rocky's custody party (see Fees). The counterparty is either the other participant's party or Rocky's custody party for a fill against a house account. Instrument ids correspond to the baseAsset / quoteAsset names in exchangeInfo.
  6. Reconcile. The sum of confirmed legs for an asset over a period should equal the change in your on-chain holdings for that asset over the same period, net of your own transfers in and out. A mismatch is a support case; quote the cantonUpdateId.

Rocky also produces a daily settlement report (count and list of updateIds) for institutional participants; ask for it through your Integration Pack channel.

Fees​

The wallet-funded side of a fill pays the standard spot fee: currently 4 bps maker, 10 bps taker, charged in the asset you receive and reported as commission / commissionAsset in myTrades. The fee is withheld on chain: when the counterparty is Rocky's custody party, the leg it sends you carries the fill amount less your commission, so your wallet receives exactly what your mirror balance is credited. The leg you send always carries the full fill amount.

Exception: a fill in which both sides are wallet-funded is not charged, because the settlement authorization only allows transfers between the two counterparties and leaves no leg to carry a fee to Rocky. commission is 0 on such fills. Fee schedules are not advertised in exchangeInfo.

Minimum notional​

Every order from a wallet-funded account must satisfy price × quantity ≥ minNotional for its symbol, checked at order entry (-1013 Filter failure: NOTIONAL). For MARKET orders the protective band price is used. The current values are published per symbol in exchangeInfo (NOTIONAL.minNotional); for USD-quoted markets (CBTC-USDCB, CETH-USDCB, CBTC-USX, CBTC-USDCX) it is 10 quote units, and for CETH-CBTC it is 0.00015 CBTC. Always read the live value rather than hard-coding it.

Self-trade prevention​

Self-trade prevention is always on and not configurable. When one of your orders would match one of your own resting orders, the matcher skips your resting order and continues to the next price level or counterparty; your resting order stays in the book untouched, and your incoming order may rest behind it (LIMIT) or cancel its remainder (MARKET). No fill, no cancel, no error is generated for the skipped order. There is no selfTradePreventionMode parameter and no EXPIRED_IN_MATCH status.