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, carrying thenonce, burnamount,destinationDomain, andmintRecipient. - Destination:
MintAndWithdrawon theMessageTransmitter/TokenMinter, carrying 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, persisted so the ledger records where each mint landed. The join key itself is (source_domain, nonce), and both come from the MessageReceived event on each chain’s MessageTransmitter (the MintAndWithdraw event does not carry the nonce).
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.