How CCTP transfers work
Every CCTP transfer produces two onchain events on two different chains, correlated by a nonce (and a message hash):- Source:
DepositForBurnon theTokenMessengercontract - carries thenonce, burnamount,destinationDomain, andmintRecipient. - Destination:
MintAndWithdrawon theMessageTransmitter/TokenMinter- carries the matchingnonceand mintamount. - In-flight means the burn exists but the mint has not landed yet. Fast Transfers resolve in seconds; Standard Transfers wait for source-chain hard finality (~13-19 minutes on Ethereum).
CCTP identifies each chain by a numeric domain ID, not a chain ID: Ethereum
0, Avalanche 1, Optimism 2, Arbitrum 3, Noble 4, Solana 5, Base 6, Polygon PoS 7. Confirm the current list and contract addresses in Circle’s CCTP docs.Prerequisites
- The Turbo CLI extension installed and logged in.
- A Postgres database and a Goldsky secret.
- The
TokenMessengerandMessageTransmittercontract addresses for each chain you settle on (from Circle’s docs). CCTP V2 is live on Ethereum, Base, Arbitrum, Optimism, Polygon, and Avalanche - all supported by Goldsky across every product.
Step 1: Index burns across every source chain
StreamDepositForBurn from the TokenMessenger on each chain and combine them with UNION ALL. This is the multi-chain monitoring pattern with a decode step.
cctp-burns.yaml
Step 2: Index mints on every destination chain
Mirror Step 1 for the destination side. Exactly as each source chain tagged its burns with a constantsource_domain, each destination chain tags its mints with its own destination_domain - the column Step 3 joins on, so it must be persisted. The nonce comes from the MessageReceived event on each chain’s MessageTransmitter (the MintAndWithdraw event does not carry it).
cctp-mints.yaml
Confirm the
MessageReceived event name and event_params positions against the ABI you fetch - they differ between CCTP V1 and V2. CCTP nonces are scoped per source domain, so the join key is (source_domain, nonce) - both come from MessageReceived, while destination_domain records the chain the mint landed on. Amount and recipient already live on the burn row, so the mint row only needs those keys plus timing. A chain is both a source and a destination, so you can fold burns and mints into one pipeline with more sinks, or keep them split for clarity.Step 3: Reconcile into one ledger
With both tables filling in real time, the cross-chain ledger is a single join on the message identity,(source_domain, nonce):
- Where every dollar is - settled on the destination, or in-flight between chains.
- Your true cross-chain cash position - sum settled balances per domain, plus in-flight amounts by destination.
- Corridor analytics - volume and settlement latency by
(source_domain → destination_domain). - Stuck-transfer detection - anything
delayedpast your SLA.
Step 4: Alert on stuck transfers with Compose
Turndelayed from a dashboard row into an action. A scheduled Compose task checks pending transfers against Circle’s Iris attestation service and escalates anything unresolved.
src/tasks/cctp-watchdog.ts
Business outcomes
- One cross-chain cash position instead of a spreadsheet per chain.
- In-flight visibility - you know the moment a transfer is late, with the exact nonce to investigate.
- Corridor intelligence - real settlement times and volumes per route, to tune where you hold liquidity.
- Works with Circle Gateway too - the same burn/mint ledger underpins reporting on unified USDC balances.