CCIP Intents

An intent is basically a request for a cross-chain outcome.

  1. You specify what outcomes you want (desired token, destination chain, recipient on destination), and,
  2. You specify what inputs you are providing for the transfer (e.g., source token and amount), and the protocol arranges fulfillment instead of you sending the message through the standard CCIP Router path yourself.

This page will show you how to use the Chainlink's Intents API to move USDC from Base to Ronin on mainnet.

Intents vs standard CCIP transfers

The same CCIP API serves both surfaces, but they model different things:

Standard CCIP transfersIntent-based transfers
Execution modelLock-and-mint / burn-and-mint message passing through the canonical CCIP routerFulfillment-based: a provider fills your requested outcome
IdentifierCCIP messageIdSource transaction hash, then provider-prefixed intentId
FeesExplicit fee token selection at send timeImplicit in the quote (inputAmount − expected output)
Status trackingOnchain message state, or GET /messages/{messageId}Intents API: GET /intents/tx/{txHash}, GET /intents/id/{intentId}
Arbitrary dataFully programmable messagingLimited customization options

When to use intents

Common use cases for intents instead of standard CCIP transfers:

  • Fast transfers: standard CCIP waits for full source-chain finality before executing on the destination chain, which takes roughly 18 minutes from Base and 12.8 from Ethereum (finality times by chain). With intents, a provider fills your requested outcome instead, so you get the speed without opting into Faster-Than-Finality at every layer of the message path or taking on its reorg risk.
  • Cross-chain swaps: receive a different token on the destination chain than the one you sent, in a single transaction. Standard CCIP transfers deliver the same token you sent.
  • Routes and assets standard CCIP does not serve: token support on standard CCIP requires the token issuer to deploy and register pools on each chain (CCT overview), so coverage grows one token at a time.
  • Simpler fees and operations: the fee is implicit in the quote, so you never select, hold, or approve a fee token. You also skip destination gas estimation and manual execution handling.

If you need arbitrary messaging or programmable token transfers, use the standard CCIP transfer flow instead. Intents also serve mainnet routes only and require an API key; for anything outside the supported routes, use standard CCIP.

Access and prerequisites

Before you begin:

  1. Obtain an Intents API key (see above). This tutorial refers to it as the INTENTS_API_KEY environment variable.
  2. Hold ETH on Base for gas and USDC on Base to transfer. Both are real mainnet funds.
  3. Install cast from Foundry to send the onchain transactions.

How it works

An intent moves through five steps:

Sequence diagram of the CCIP intents flow: request a quote from the CCIP API, approve the token spend, submit the intent transaction on the source chain, look up the intent ID by transaction hash, and poll the intent status until it reaches a terminal state.

The CCIP intents flow: quote, approve, submit, discover the intent ID, then poll status.

  1. Quote: POST /intents/quotes returns the expected output amount and an executable transaction.
  2. Approve: you approve the token spend required by the quote.
  3. Submit: you submit the quote's transaction on the source chain.
  4. Discover: GET /intents/tx/{txHash} returns the intent ID (or IDs, for batched intents) created by that transaction.
  5. Track: you poll GET /intents/id/{intentId} until the intent reaches a terminal status.

Step 1: Get a quote

POST /intents/quotes prices a cross-chain swap or bridge operation and returns executable transaction data. This request quotes a transfer of 1 USDC (USDC has 6 decimals, so 1000000 in smallest units) from Base to Ronin:

Terminal
export INTENTS_API_KEY="<your-intents-api-key>"
export SENDER_ADDRESS="<your-base-address>"
export RECEIVER_ADDRESS="<recipient-on-ronin>"

curl -s -X POST "https://api.ccip.chain.link/v2/intents/quotes" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $INTENTS_API_KEY" \
  --data @- <<EOF
{
  "sourceChainSelector": "15971525489660198786",
  "destChainSelector": "6916147374840168594",
  "inputToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "outputToken": "0x0B7007c13325C48911F73A2daD5FA5dCBf808aDc",
  "inputAmount": "1000000",
  "sender": "$SENDER_ADDRESS",
  "receiver": "$RECEIVER_ADDRESS",
  "calldata": "0x"
}
EOF

The request body fields:

FieldDescription
sourceChainSelectorCCIP chain selector of the source chain. 15971525489660198786 is Base.
destChainSelectorCCIP chain selector of the destination chain. 6916147374840168594 is Ronin.
inputTokenToken you send on the source chain. Shown: USDC on Base.
outputTokenToken you want to receive on the destination chain. Shown: USDC on Ronin.
inputAmountAmount in the token's smallest unit, as a decimal string. 1000000 is 1 USDC.
senderAddress that submits the intent transaction on the source chain.
receiverAddress that receives tokens on the destination chain.
calldataOptional hex-encoded execution data for arbitrary contract calls. Supported on some routes only; use 0x.

A successful response contains the quote and the transaction to submit. Example response (illustrative):

JSON response
{
  "inputAmount": "1000000",
  "expectedOutputAmount": "998500",
  "estimatedExecutionTime": 180,
  "transaction": {
    "chainFamily": "EVM",
    "transactionPurpose": "INTENT",
    "to": "0x15a7cA97D1ed168fB34a4055CEFa2E2f9Bdb6C75",
    "data": "0xabcdef1234567890...",
    "value": "0",
    "chainId": "8453"
  },
  "deadline": 1734192060,
  "refundable": false
}

Three response fields drive the rest of the flow:

  • transaction: the exact transaction to submit on the source chain: target to, data, and value. Do not modify these fields; the quote is only valid for the transaction as returned.
  • deadline: Unix timestamp (seconds) for quote validity. Submit before this time or re-request the quote. Some routes encode a separate, longer onchain deadline inside transaction.data for fulfillment; deadline is the shorter quote-validity window.
  • expectedOutputAmount: the destination-chain amount after all fees, in the output token's smallest unit. May be omitted if the provider does not return an estimate.

If the API returns 404 with ROUTE_NOT_FOUND, no provider serves that chain/token pair. Confirm any route you intend to use with a successful quote call before building on it. This tutorial uses the Base → Ronin USDC route from Supported routes and tokens.

For the full schema and an interactive playground, see Create quote for intent in the CCIP Tools API reference.

Step 2: Approve and submit the intent transaction

The intent transaction transfers tokens from your sender address to the protocol contract, so you must first approve the spend. Approve inputToken to the quote's transaction.to address for at least inputAmount:

Terminal
export INTENT_TO="<transaction.to from your quote>"
export BASE_RPC_URL="<your-base-rpc-url>"
export PRIVATE_KEY="<your-private-key>"

cast send "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" \
  "approve(address,uint256)" "$INTENT_TO" 1000000 \
  --rpc-url "$BASE_RPC_URL" \
  --private-key "$PRIVATE_KEY"

Then submit the quote's transaction as-is, passing transaction.data as raw calldata and transaction.value as the value (typically "0" for intents):

Terminal
cast send "$INTENT_TO" \
  --data "<transaction.data from your quote>" \
  --value 0 \
  --rpc-url "$BASE_RPC_URL" \
  --private-key "$PRIVATE_KEY"

Save the transaction hash from this submission; the next step uses it. If the quote's deadline has passed by the time you submit, discard the transaction and request a fresh quote.

Step 3: Get the intent ID

GET /intents/tx/{txHash} returns the status of every intent created in a transaction, as an array. A single transaction can create multiple batched intents. Use it to obtain the intentId after your submission confirms:

Terminal
export TX_HASH="<your-submission-transaction-hash>"

curl -s "https://api.ccip.chain.link/v2/intents/tx/$TX_HASH" \
  -H "accept: application/json" \
  -H "x-api-key: $INTENTS_API_KEY"

Example response (illustrative), a single-element array:

JSON response
[
  {
    "intentId": "EC-e429343c7b4c75e8c14ae8810b1fc8df20de32f05fa907c4839aa80a2c2db13e",
    "transactionHash": "0xabc1234567890def1234567890abcdef1234567890abcdef1234567890abcdef",
    "status": "PENDING",
    "subStatus": "WaitingToFulfill",
    "sourceChainSelector": "15971525489660198786",
    "destChainSelector": "6916147374840168594",
    "sender": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7",
    "receiver": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7",
    "inputToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "outputToken": "0x0B7007c13325C48911F73A2daD5FA5dCBf808aDc",
    "inputAmount": "1000000",
    "createdAt": 1734192060
  }
]

Intent IDs are provider-prefixed (for example, EC-... or EO-...). Raw 0x transaction hashes are not valid intent IDs, which is why this lookup step exists. To extract the ID for the next step with jq:

Terminal
export INTENT_ID=$(curl -s "https://api.ccip.chain.link/v2/intents/tx/$TX_HASH" \
  -H "accept: application/json" \
  -H "x-api-key: $INTENTS_API_KEY" | jq -r '.[0].intentId')

.[0] assumes the transaction created one intent; if you batch intents, select the right element instead. The lookup can also return 404 until the transaction is indexed, so retry after a few seconds if your submission just confirmed.

Once you have the intentId, prefer GET /intents/id/{intentId} for status queries. For the full schema, see Get all intent statuses for a transaction.

Step 4: Track the intent to completion

Prefer GET /intents/id/{intentId} to return the status of one intent:

Terminal
curl -s "https://api.ccip.chain.link/v2/intents/id/$INTENT_ID" \
  -H "accept: application/json" \
  -H "x-api-key: $INTENTS_API_KEY"

Poll until status reaches a terminal value: COMPLETED or FAILED. A 10-second interval is a reasonable default:

Terminal
while true; do
  STATUS=$(curl -s "https://api.ccip.chain.link/v2/intents/id/$INTENT_ID" \
    -H "accept: application/json" \
    -H "x-api-key: $INTENTS_API_KEY" | jq -r '.status')
  echo "$STATUS"
  [ "$STATUS" = "COMPLETED" ] || [ "$STATUS" = "FAILED" ] && break
  sleep 10
done

When the intent completes, the response includes outputAmount (the actual amount delivered) and fulfillmentTxHash (the fulfillment transaction on the destination chain). For refunded intents, fulfillmentTxHash is the refund transaction on the source chain and filledAt carries the refund timestamp:

JSON response
{
  "intentId": "EC-e429343c7b4c75e8c14ae8810b1fc8df20de32f05fa907c4839aa80a2c2db13e",
  "transactionHash": "0xabc1234567890def1234567890abcdef1234567890abcdef1234567890abcdef",
  "status": "COMPLETED",
  "subStatus": null,
  "sourceChainSelector": "16015286601757825753",
  "destChainSelector": "3478487238524512106",
  "fulfillmentTxHash": "0xdef4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
  "sender": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7",
  "receiver": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7",
  "inputToken": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
  "outputToken": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
  "inputAmount": "1000000",
  "outputAmount": "998500",
  "createdAt": 1734192060,
  "filledAt": 1734192240
}

For the full schema and playground, see Get intent status by ID.

Intent attribute reference

The values below cover what the tracking loop in this tutorial uses. For complete schemas, see the CCIP Tools API reference.

status: high-level intent state:

ValueMeaning
PENDINGThe intent is processing.
COMPLETEDThe intent has been fulfilled.
FAILEDThe intent has failed and cannot be fulfilled.

Other attributes:

AttributeValues / format
subStatusRoute-specific detail, for display only. Example: WaitingToFulfill.
intentIdProvider-prefixed string (EC-..., EO-...). Obtain it from GET /intents/tx/{txHash}.
chainFamilyEVM for all served routes.
transactionPurposeINTENT: the transaction is ready to submit onchain.
inputAmount, outputAmountDecimal strings in the token's smallest unit.
deadline, createdAt, filledAtUnix timestamps in seconds.

Supported routes and tokens

The Intents API serves mainnet routes only; no testnet routes are available. Route support is per chain/token pair and evolves as providers add support. The quote endpoint returns 404 ROUTE_NOT_FOUND when no provider serves a requested pair; confirm production availability with your Chainlink representative before integrating a specific pair.

Routes served at the time of writing (mainnet)
Source chainDestination chainTokens
EthereumRoninUSDC, WETH
RoninEthereumUSDC, WETH
BaseRoninUSDC, WETH
RoninBaseUSDC, WETH

Token contract addresses:

TokenEthereumBaseRonin
USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480x833589fCD6eDb6E08f4c7C32D4f71b54bdA029130x0B7007c13325C48911F73A2daD5FA5dCBf808aDc
WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20x42000000000000000000000000000000000000060xc99a6A985eD2Cac1ef41640596C5A5f9F4E19Ef5

Next steps

Get the latest Chainlink content straight to your inbox.