> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Intent types

> The four shapes a Routes quote can take, how each is fulfilled, and what an integration has to read from the quote to fund, verify and track every case: direct fulfillment, CCTP fulfillment, swap and bridge, bridge and swap.

A quote from [`POST /v1/quotes`](/api-reference/v1/quotes) is executed as one or more intents. The shape is read from `steps[]`, and it decides where fulfillment happens, which transaction delivers the funds, and what has to be tracked. Four shapes are returned today. The examples below are from live quotes captured on 2026-10-01 with placeholder wallets; calldata is decoded and shortened.

## Reading a route

| Field | Meaning |
| - | - |
| `destination.chainId`, `destination.token`, `destination.minAmountOut` | The chain, token and guaranteed minimum that the recipient receives. This is the delivery chain. |
| `steps[].type`, `steps[].provider`, `steps[].aggregator` | What a leg does (`bridge`, `swap`, `deposit`, `transfer`) and who runs it. The Eco bridge leg is `provider: "eco"`. |
| `steps[].intents[]` | Every intent that realizes a step, with its `role` and full decoded `route` and `reward`. |
| `intent.route.destination` | The chain where that intent executes. It is not always the delivery chain. |
| `intent.route.calls` | The calls the destination Portal's Executor runs on fulfillment. The target and selector of each call identify the shape. |
| `intent.reward.prover` | The prover the fulfillment is proven through. Cross-chain intents use a messaging prover; same-chain intents use the Local prover. |

Three intent roles exist. `local` is an intent that executes on the chain where it was published. `bucket-candidate` is one of several pre-signed alternatives of which one executes. `stitched-destination` is the intent that completes delivery on the destination chain in a composite route.

The quote `signature` covers `intentHash` and every `steps[].intents[].intentHash`, so every intent in the route is attested before funding. See [Quote verification](/api-reference/quote-verification).

## Direct fulfillment

The default shape for a listed stablecoin pair. One intent is published and funded on the source chain. A solver delivers the destination token to the recipient from its own inventory and is paid the reward once the proof arrives.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Funder
    participant SP as Source Portal and Vault
    participant Solver
    participant DP as Destination Portal
    participant Prover as Hyperlane prover
    Funder->>SP: publishAndFund (execution.transaction)
    Solver->>DP: fulfill: USDC.transfer(recipient, amount)
    DP-->>Prover: fulfillment message
    Prover-->>SP: proof delivered
    Solver->>SP: withdraw reward
```

Base USDC to OP Mainnet USDC, 1 USDC. One `bridge` step, one `local` intent.

```json theme={null}
{
  "destination": { "chainId": 10, "token": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "minAmountOut": "1000000" },
  "steps": [{ "type": "bridge", "provider": "eco", "intents": [{ "role": "local", "intentHash": "0x1fc66480…" }] }],
  "execution": {
    "transaction": { "chainId": 8453, "to": "0xEC000769A73b70e16f361a442292500b3BCf4A85", "data": "0xdf00f8fa…" },
    "intent": {
      "route": { "source": 8453, "destination": 10, "tokens": [{ "token": "0x0b2C639c…", "amount": "1000000" }],
                 "calls": [{ "target": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "data": "0xa9059cbb…" }] },
      "reward": { "prover": "0xEC08fb4647f3f50d1162a578d481266687C60fc5", "tokens": [{ "token": "0x833589fC…", "amount": "1000000" }] }
    }
  }
}
```

| Call | Target | Decoded |
| - | - | - |
| `execution.transaction` | Source Portal | `publishAndFund(destination, route, reward, allowPartial)`, selector `0xdf00f8fa` |
| `route.calls[0]` | Destination USDC | `transfer(recipient, 1000000)`, selector `0xa9059cbb` |

What identifies it: `route.destination` equals `destination.chainId`, the route holds one `transfer` to the recipient for at least `minAmountOut`, and the prover reports `"Hyperlane"` from `getProofType()`. Fulfillment on the destination chain is delivery.

## CCTP fulfillment

Used when the route is settled over Circle's Cross-Chain Transfer Protocol instead of solver inventory, which today is how large USDC transfers are served. The intent is a same-chain intent on the source chain: its route approves USDC to Circle's TokenMessengerV2 and calls `depositForBurn` with the recipient as mint recipient. The solver fulfills it through the Local prover's `flashFulfill`, which withdraws the funder's USDC from the intent vault, runs the burn, and pays the solver the remainder in the same transaction. The solver then fetches Circle's attestation and submits `receiveMessage` on the destination chain, which mints USDC to the recipient.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Funder
    participant SP as Source Portal and Vault
    participant Solver
    participant LP as Local prover
    participant TM as TokenMessengerV2 (source)
    participant Circle as Circle attestation
    participant MT as MessageTransmitter (destination)
    Funder->>SP: publishAndFund
    Solver->>LP: flashFulfill(route, reward, claimant)
    LP->>SP: withdraw reward from vault, fulfill
    SP->>TM: approve, depositForBurn(amount, domain, recipient, USDC, …)
    Note over LP,Solver: intent is filled; remainder paid to solver
    Solver->>Circle: fetch attestation for the burn message
    Solver->>MT: receiveMessage(message, attestation)
    MT-->>Funder: USDC minted to recipient
```

Base USDC to Arbitrum USDC, 250,000 USDC. One `bridge` step, one `local` intent that executes on Base.

```json theme={null}
{
  "destination": { "chainId": 42161, "token": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "minAmountOut": "249967500000" },
  "fees": [{ "type": "protocol", "amount": "0" }, { "type": "cctp", "amount": "32500000", "estimate": true }],
  "steps": [{ "type": "bridge", "provider": "eco", "from": { "chainId": 8453 }, "to": { "chainId": 42161 },
              "intents": [{ "role": "local", "intentHash": "0x00f9b3de…" }] }],
  "execution": {
    "intent": {
      "route": { "source": 8453, "destination": 8453, "tokens": [{ "token": "0x833589fC…", "amount": "250000000000" }],
                 "calls": [{ "target": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x095ea7b3…" },
                           { "target": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", "data": "0x8e0250ee…" }] },
      "reward": { "prover": "0xEC064A2084D84155Da6690CE374Ffe0099278618", "tokens": [{ "token": "0x833589fC…", "amount": "250000000000" }] }
    }
  }
}
```

| Call | Target | Decoded |
| - | - | - |
| `route.calls[0]` | Base USDC | `approve(TokenMessengerV2, 250000000000)` |
| `route.calls[1]` | TokenMessengerV2 `0x28b5a0e9…` | `depositForBurn(amount 250000000000, destinationDomain 3, mintRecipient recipient, burnToken USDC, destinationCaller 0x0, maxFee 32500000, minFinalityThreshold 1000)` |
| Mint, sent by the solver | MessageTransmitter on Arbitrum | `receiveMessage(message, attestation)`, selector `0x57ecfd28` |

Nuances:

* `route.destination` equals `route.source`, not `destination.chainId`. The intent executes on the source chain; `destination.chainId` is where the recipient is paid.
* The reward equals the route amount, and the prover reports `"Same chain"`. The funder's own USDC is burned; solver inventory is not used.
* `destinationDomain` is Circle's domain for the delivery chain (3 is Arbitrum, 2 is OP Mainnet, 6 is Base), per [Circle's domain list](https://developers.circle.com/cctp/references/technical-guide). `maxFee` equals the `cctp` fee in `fees[]`.
* The intent reports `filled` when the burn has executed, not when the recipient has been credited. Delivery is the destination mint, so a `filled` status has to be followed by a check of the recipient's balance or the `receiveMessage` transaction.
* A fee of type `cctp` appears only on this shape. Fee types are an open set; an unknown type is not an error.
* Deadlines differ by shape and are read from the quote. In the captured quotes, a CCTP-settled intent carried a route deadline of about 90 minutes against a few minutes for a direct fill, leaving room to resubmit the mint.

## Swap and bridge

Returned when the source token is not a stablecoin Eco lists on that chain. The funded intent is a same-chain intent whose route swaps the source token through Eco's swap gateway and funds one of several pre-signed `bucket-candidate` intents, chosen by the swap output. In the captured quote each candidate is a CCTP burn to the recipient, so the second half of the route is a [CCTP fulfillment](#cctp-fulfillment).

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Funder
    participant SP as Source Portal and Vault
    participant Solver
    participant GW as EcoSwapGateway and DEX
    participant Cand as Selected candidate intent
    Funder->>SP: publishAndFund (WETH as reward)
    Solver->>SP: flashFulfill the local intent
    SP->>GW: approve WETH, swapAndSelectIntent(…)
    GW->>GW: swap WETH to USDC, pick the candidate matching the output
    GW->>Cand: fund the candidate's vault with the USDC
    Note over Cand: candidate executes as a CCTP fulfillment: burn on source, mint to recipient
```

Base WETH to OP Mainnet USDC, 0.001 WETH. A `swap` step naming the local intent, then a `bridge` step listing four candidates.

```json theme={null}
{
  "destination": { "chainId": 10, "token": "0x0b2C639c…", "minAmountOut": "2689183" },
  "steps": [
    { "type": "swap", "provider": "kyberswap", "aggregator": "kyberswap", "from": { "chainId": 8453, "token": "0x42000000…" }, "to": { "chainId": 8453, "token": "0x833589fC…", "amount": "2716700" },
      "intents": [{ "role": "local", "intentHash": "0x62959f40…",
        "intent": { "route": { "source": 8453, "destination": 8453,
          "calls": [{ "target": "0x4200000000000000000000000000000000000006", "data": "0x095ea7b3…" },
                    { "target": "0xEC0481370b138146FB2d3838D90F072501aba00A", "data": "0x505624b6…" }] } } }] },
    { "type": "bridge", "provider": "eco", "from": { "chainId": 8453 }, "to": { "chainId": 10 },
      "intents": [
        { "role": "bucket-candidate", "bucketIndex": 0, "intentHash": "0x3506cb82…", "intent": { "route": { "destination": 8453, "tokens": [{ "amount": "2689533" }], "calls": "approve, depositForBurn(…, mintRecipient recipient, domain 2)" } } },
        { "role": "bucket-candidate", "bucketIndex": 1, "intentHash": "0x3e50b824…", "intent": { "route": { "tokens": [{ "amount": "2698588" }] } } },
        { "role": "bucket-candidate", "bucketIndex": 2, "intentHash": "0x5acbd8ca…", "intent": { "route": { "tokens": [{ "amount": "2707644" }] } } },
        { "role": "bucket-candidate", "bucketIndex": 3, "intentHash": "0xf9fc888c…", "intent": { "route": { "tokens": [{ "amount": "2716700" }] } } }
      ] }
  ]
}
```

| Call | Target | Decoded |
| - | - | - |
| local `route.calls[0]` | WETH | `approve(EcoSwapGateway, 1000000000000000)` |
| local `route.calls[1]` | EcoSwapGateway `0xEC048137…` | `swapAndSelectIntent(…)`, selector `0x505624b6`, carrying the DEX calldata and the candidate set |
| each candidate `route.calls` | USDC, TokenMessengerV2 | `approve`, `depositForBurn(amount, domain 2, mintRecipient recipient, …)` |

Nuances:

* The funded intent never leaves the source chain, and the step `to.chainId` is the only place the delivery chain appears before `destination`.
* Exactly one candidate executes. The candidates differ only in amount, a fixed step apart, so the swap output always has a match. All of them are covered by the quote signature.
* `destination.minAmountOut` is the floor across candidates; the lowest candidate amount is at or above it.
* A `bridge` step with an empty `intents` array is also valid on this shape: the gateway then creates the bridging intent at execution time instead of selecting a pre-signed one.

## Bridge and swap

Returned when the destination token is not a stablecoin Eco lists on that chain. The route is two intents. The funded intent is a same-chain CCTP burn on the source chain whose mint recipient is the vault of a second, `stitched-destination` intent on the destination chain. When the mint lands, that second intent is funded; a solver then fulfills it through the Local prover, and its route swaps the USDC through a DEX to the recipient.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Funder
    participant SP as Source Portal and Vault
    participant Solver
    participant TM as TokenMessengerV2 (source)
    participant Circle as Circle attestation
    participant DV as Destination intent vault
    participant DEX as DEX router (destination)
    Funder->>SP: publishAndFund
    Solver->>SP: flashFulfill the source intent
    SP->>TM: approve, depositForBurn(amount, domain, mintRecipient = destination vault)
    Solver->>Circle: fetch attestation
    Solver->>DV: receiveMessage mints USDC into the vault
    Solver->>DEX: flashFulfill the destination intent: approve, swap, output to recipient
```

Base USDC to Arbitrum ARB, 5 USDC. A `bridge` step naming the source intent and a `swap` step naming the destination intent.

```json theme={null}
{
  "destination": { "chainId": 42161, "token": "0x912CE59144191C1204E64559FE8253a0e49E6548", "minAmountOut": "24169407277665106574" },
  "fees": [{ "type": "protocol", "amount": "0" }, { "type": "cctp", "amount": "650", "estimate": true }],
  "steps": [
    { "type": "bridge", "provider": "eco", "from": { "chainId": 8453 }, "to": { "chainId": 42161, "token": "0xaf88d065…", "amount": "4999350" },
      "intents": [{ "role": "local", "intentHash": "0xefe9eac0…",
        "intent": { "route": { "source": 8453, "destination": 8453, "tokens": [{ "token": "0x833589fC…", "amount": "5000000" }],
          "calls": [{ "target": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x095ea7b3…" },
                    { "target": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", "data": "0x8e0250ee…" }] } } }] },
    { "type": "swap", "provider": "kyberswap", "aggregator": "kyberswap", "from": { "chainId": 42161, "token": "0xaf88d065…" }, "to": { "chainId": 42161, "token": "0x912CE591…" },
      "intents": [{ "role": "stitched-destination", "intentHash": "0xafb2bbdc…",
        "intent": { "route": { "source": 42161, "destination": 42161, "tokens": [{ "token": "0xaf88d065…", "amount": "4999350" }],
          "calls": [{ "target": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "data": "0x095ea7b3…" },
                    { "target": "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5", "data": "0xe21fd0e9…" },
                    { "target": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "data": "0x095ea7b3…" }] },
          "reward": { "prover": "0xEC064A20…", "tokens": [{ "token": "0xaf88d065…", "amount": "4999350" }] } } }] }
  ]
}
```

| Call | Target | Decoded |
| - | - | - |
| source `route.calls[1]` | TokenMessengerV2 on Base | `depositForBurn(5000000, domain 3, mintRecipient 0x6066a8ff…, USDC, 0x0, maxFee 650, 1000)` |
| destination `route.calls[0..2]` | Arbitrum USDC, KyberSwap router `0x6131B5fa…` | `approve`, `swap(…)`, selector `0xe21fd0e9`, then `approve` reset |

Nuances:

* `mintRecipient` is not the recipient. `0x6066a8ff…` is the destination intent's vault, which the Arbitrum Portal returns from `intentVaultAddress` for the quoted intent hash. The mint funds the second intent rather than paying the user.
* The destination intent is a same-chain intent on the delivery chain: `route.source`, `route.destination` and `destination.chainId` are all the same, its reward is the minted USDC, and its route delivers ARB to the recipient inside the swap calldata.
* Delivery is the fulfillment of the `stitched-destination` intent. The source intent's `filled` status means the burn executed.
* The recipient sits inside the swap calldata, so a `transfer` check does not apply; the output floor is `destination.minAmountOut` in the destination token.

## Refunds

A refund returns an intent's reward to its `reward.creator` on the chain where the intent was published. It is a separate Portal transaction that any account can submit once the intent is eligible. An unfulfilled intent becomes eligible in one of two ways: a cancellation on its execution chain, allowed after `route.deadline`, is proven to the source chain, or `reward.deadline` passes with no proof. A fulfilled intent is not refundable. Both deadlines are read from the quote.

What a refund returns depends on where the route stops:

| Shape | Where the route stops | Refund |
| - | - | - |
| Direct fulfillment | Not fulfilled by the route deadline | Source token on the source chain |
| CCTP fulfillment | Burn not executed by the route deadline | Source token on the source chain |
| Swap and bridge | Source swap not executed | Source token on the source chain |
| Bridge and swap | Destination swap not executed after the bridge | USDC on the destination chain, to the recipient, who is the `reward.creator` of the destination intent |

Once a funded intent has executed it is fulfilled and no refund applies; where a leg settles over CCTP, the mint is completed with Circle's attestation.

An integration that shows refund status to a user therefore reads the asset and chain from the intent that holds the funds, not from the original request. See [Portal](/routes/architecture/portal#withdrawal-and-refunds) for the contract calls.

## Handling every shape

| | Direct fulfillment | CCTP fulfillment | Swap and bridge | Bridge and swap |
| - | - | - | - | - |
| Intents in the quote | 1 `local` | 1 `local` | 1 `local` + N `bucket-candidate` | 1 `local` + 1 `stitched-destination` |
| Funded intent executes on | destination chain | source chain | source chain | source chain |
| Prover of the funded intent | Hyperlane | Local | Local | Local |
| What to verify in `route.calls` | `transfer` to recipient | `depositForBurn` with `mintRecipient` = recipient | gateway call, then candidates' `depositForBurn` to recipient | `depositForBurn` with `mintRecipient` = destination vault; swap on the destination intent |
| Delivery evidence | destination fulfillment | mint on destination | mint on destination | fulfillment of the destination intent |

For every shape: fund `execution.transaction` as returned, verify the signature over all listed hashes, and poll [intent status](/api-reference/v1/intent-status) by `quoteId`, which returns every step with its own status and transactions. Treat `filled` on a same-chain intent as the source side only, and confirm the destination credit before marking the transfer complete. An unknown `steps[].type`, `role` or fee type must be handled explicitly rather than read as a direct transfer.

## Next steps

* [Integrate the Routes API](/get-started/integrate-routes-api)
* [Quote verification](/api-reference/quote-verification)
* [Provers](/routes/architecture/provers/overview)
* [Destination calls](/routes/capabilities/destination-calls)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.