Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/eco/eco-routes/llms.txt

Use this file to discover all available pages before exploring further.

What is Intent Fulfillment?

Intent fulfillment is the process by which solvers execute user intents on destination chains. Solvers:
  1. Monitor published intents across chains
  2. Evaluate profitability based on rewards vs. execution costs
  3. Provide required tokens/assets on the destination chain
  4. Execute the intent’s calls on the destination chain
  5. Prove fulfillment to claim rewards on the source chain

Fulfillment Contract

The Inbox contract (contracts/Inbox.sol) handles intent fulfillment on destination chains.

Key Functions

Inbox.sol
/**
 * @notice Fulfills an intent to be proven via storage proofs
 * @dev Validates intent hash, executes calls, and marks as fulfilled
 * @param intentHash The hash of the intent to fulfill
 * @param route The route of the intent
 * @param rewardHash The hash of the reward details
 * @param claimant Cross-VM compatible claimant identifier
 * @return Array of execution results from each call
 */
function fulfill(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant
) external payable returns (bytes[] memory);

/**
 * @notice Fulfills an intent and initiates proving in one transaction
 * @dev Executes intent actions and sends proof message to source chain
 * @param intentHash The hash of the intent to fulfill
 * @param route The route of the intent
 * @param rewardHash The hash of the reward details
 * @param claimant Cross-VM compatible claimant identifier
 * @param prover Address of prover on the destination chain
 * @param sourceChainDomainID Domain ID of the source chain where the intent was created
 * @param data Additional data for message formatting
 * @return Array of execution results
 */
function fulfillAndProve(
    bytes32 intentHash,
    Route memory route,
    bytes32 rewardHash,
    bytes32 claimant,
    address prover,
    uint64 sourceChainDomainID,
    bytes memory data
) public payable returns (bytes[] memory);

Fulfillment Process

Here’s a complete guide to fulfilling intents as a solver.
1

Monitor Published Intents

Listen for IntentPublished events on source chains:
// Listen to IntentSource contract events
event IntentPublished(
    bytes32 indexed intentHash,
    uint64 destination,
    bytes route,
    address creator,
    address prover,
    uint64 deadline,
    uint256 nativeAmount,
    TokenAmount[] tokens
);
Filter for intents on chains you support:
const filter = intentSource.filters.IntentPublished(
    null,  // any intentHash
    destinationChainId,  // your supported chain
);

intentSource.on(filter, (intentHash, destination, route, creator, prover, deadline, nativeAmount, tokens) => {
    // Evaluate if profitable
    evaluateIntent(intentHash, route, tokens);
});
2

Evaluate Profitability

Calculate if the intent is worth executing:
async function evaluateIntent(intentHash, routeData, rewardTokens) {
    // Decode route
    const route = decodeRoute(routeData);

    // Calculate costs
    const executionGas = estimateGas(route.calls);
    const tokenCosts = calculateTokenCosts(route.tokens);
    const gasCost = executionGas * gasPrice;

    // Calculate rewards
    const rewardValue = calculateRewardValue(rewardTokens);

    // Check profitability (including profit margin)
    const minProfit = 0.05; // 5% minimum profit
    if (rewardValue > (tokenCosts + gasCost) * (1 + minProfit)) {
        await fulfillIntent(intentHash, route, rewardHash);
    }
}
3

Verify Intent is Funded

Check that the reward vault has sufficient funds:
// On the source chain
bool isFunded = intentSource.isIntentFunded(intent);
require(isFunded, "Intent not funded");

// Or check vault balance directly
address vaultAddress = intentSource.intentVaultAddress(intent);
uint256 balance = IERC20(rewardToken).balanceOf(vaultAddress);
4

Approve Required Tokens

The Portal needs approval to pull tokens for execution:
// Approve Portal to spend your tokens
for (uint256 i = 0; i < route.tokens.length; i++) {
    IERC20(route.tokens[i].token).approve(
        route.portal,
        route.tokens[i].amount
    );
}
5

Fulfill the Intent

Execute the intent on the destination chain:
// Calculate hashes
bytes32 routeHash = keccak256(abi.encode(route));
bytes32 intentHash = keccak256(
    abi.encodePacked(destinationChainId, routeHash, rewardHash)
);

// Your address as claimant (for EVM)
bytes32 claimant = bytes32(uint256(uint160(msg.sender)));

// Fulfill the intent
bytes[] memory results = portal.fulfill{value: route.nativeAmount}(
    intentHash,
    route,
    rewardHash,
    claimant
);

Complete Fulfillment Example

Here’s a complete Solidity example of fulfilling a token transfer intent:
pragma solidity ^0.8.26;

import {IInbox} from "./interfaces/IInbox.sol";
import {Route, TokenAmount, Call} from "./types/Intent.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Solver {
    IInbox public portal;

    constructor(address _portal) {
        portal = IInbox(_portal);
    }

    /**
     * @notice Fulfill a cross-chain token transfer intent
     * @param intentHash Hash of the intent
     * @param route Route details
     * @param rewardHash Hash of reward details
     */
    function fulfillTokenTransfer(
        bytes32 intentHash,
        Route calldata route,
        bytes32 rewardHash
    ) external {
        // 1. Verify route is for this chain
        require(route.portal == address(portal), "Wrong portal");

        // 2. Verify not expired
        require(block.timestamp <= route.deadline, "Intent expired");

        // 3. Approve tokens for Portal to pull
        for (uint256 i = 0; i < route.tokens.length; i++) {
            IERC20(route.tokens[i].token).approve(
                address(portal),
                route.tokens[i].amount
            );
        }

        // 4. Fulfill intent
        bytes32 claimant = bytes32(uint256(uint160(address(this))));

        portal.fulfill{value: route.nativeAmount}(
            intentHash,
            route,
            rewardHash,
            claimant
        );

        // Intent is now fulfilled!
        // Next step: prove fulfillment to claim rewards
    }
}

Fulfill and Prove in One Transaction

For faster reward claiming, use fulfillAndProve to combine fulfillment and proof initiation:
// Calculate domain ID for the bridge
// WARNING: Domain ID != Chain ID for some bridges!
// Check your bridge's documentation
uint64 sourceChainDomainID = getHyperlaneDomainId(sourceChainId);

// Prover-specific data (e.g., bridge fees)
bytes memory proverData = abi.encode(
    bridgeFeeAmount,
    otherProverParams
);

// Fulfill and prove in one call
bytes[] memory results = portal.fulfillAndProve{value: totalValue}(
    intentHash,
    route,
    rewardHash,
    claimant,
    proverAddress,        // Prover contract on destination chain
    sourceChainDomainID,  // Bridge domain ID for source chain
    proverData            // Bridge-specific data
);
Domain ID vs Chain ID: The sourceChainDomainID parameter is NOT necessarily the same as the chain ID. Different bridge protocols use different domain ID systems:
  • Hyperlane: Custom domain IDs (e.g., Ethereum mainnet = 1, but Polygon = 137)
  • LayerZero: Uses endpoint IDs that differ from chain IDs
  • Polymer: Uses chain IDs directly
Always consult your bridge provider’s documentation for the correct domain ID mapping.

Real-World Examples

Example 1: Simple Token Transfer

// Intent: Transfer 1000 USDC to user on Arbitrum
// Solver has USDC on Arbitrum and wants to earn rewards on Ethereum

function fulfillUSDCTransfer(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash
) external {
    // Verify this is a token transfer to the expected recipient
    require(route.calls.length == 1, "Expected single call");

    // Approve USDC for portal
    IERC20 usdc = IERC20(route.tokens[0].token);
    usdc.approve(address(portal), route.tokens[0].amount);

    // Fulfill
    bytes32 claimant = bytes32(uint256(uint160(msg.sender)));
    portal.fulfill(intentHash, route, rewardHash, claimant);

    // The portal will:
    // 1. Pull 1000 USDC from solver
    // 2. Execute the transfer to recipient
    // 3. Mark intent as fulfilled with our address as claimant
}

Example 2: DeFi Interaction

// Intent: Deposit into Aave on Optimism
// Solver executes multi-call intent

function fulfillAaveDeposit(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash,
    address hyperProver,
    uint64 ethereumDomainId
) external payable {
    // Verify calls are for Aave
    require(route.calls.length == 2, "Expected approve + supply");

    // Approve WETH for portal to pull
    IERC20 weth = IERC20(route.tokens[0].token);
    weth.approve(address(portal), route.tokens[0].amount);

    // Fulfill and prove in one tx
    bytes memory proverData = "";
    bytes32 claimant = bytes32(uint256(uint160(address(this))));

    portal.fulfillAndProve{value: msg.value}(
        intentHash,
        route,
        rewardHash,
        claimant,
        hyperProver,
        ethereumDomainId,
        proverData
    );

    // Portal will:
    // 1. Pull WETH from solver
    // 2. Approve Aave pool
    // 3. Supply to Aave on behalf of user
    // 4. Send proof to Ethereum via Hyperlane
}

Example 3: Cross-Chain Swap

// Intent: Swap USDC (Ethereum) for USDT (Arbitrum)
// Solver provides USDT on Arbitrum, claims USDC on Ethereum

function fulfillCrossChainSwap(
    bytes32 intentHash,
    Route calldata route,
    bytes32 rewardHash
) external {
    // Approve USDT for portal
    IERC20 usdt = IERC20(route.tokens[0].token);
    usdt.approve(address(portal), route.tokens[0].amount);

    // Fulfill
    bytes32 claimant = bytes32(uint256(uint160(address(this))));
    portal.fulfill(intentHash, route, rewardHash, claimant);

    // Solver provides: 1000 USDT on Arbitrum
    // Solver receives: 1005 USDC on Ethereum (5 USDC profit)
}

Handling Fulfillment Results

The fulfill function returns execution results for each call:
bytes[] memory results = portal.fulfill(
    intentHash,
    route,
    rewardHash,
    claimant
);

// Check results
for (uint256 i = 0; i < results.length; i++) {
    // Decode based on expected return type
    if (route.calls[i].target == usdcAddress) {
        bool success = abi.decode(results[i], (bool));
        require(success, "Transfer failed");
    }
}

Claimant Identifiers

The claimant parameter is a cross-VM compatible identifier:
For EVM chains, convert your address to bytes32:
// Your EVM address as claimant
bytes32 claimant = bytes32(uint256(uint160(msg.sender)));

// Or for a specific address
bytes32 claimant = bytes32(uint256(uint160(solverAddress)));

Intent Validation

The Portal validates intents before execution:
Inbox.sol
// From _fulfill function
if (block.timestamp > route.deadline) {
    revert IntentExpired();
}

if (route.portal != address(this)) {
    revert InvalidPortal(route.portal);
}

if (computedIntentHash != intentHash) {
    revert InvalidHash(intentHash);
}

if (claimants[intentHash] != bytes32(0)) {
    revert IntentAlreadyFulfilled(intentHash);
}

if (claimant == bytes32(0)) {
    revert ZeroClaimant();
}
This ensures:
  • Intent hasn’t expired
  • Intent is for the correct portal
  • Intent hash is valid
  • Intent hasn’t been fulfilled already
  • Claimant is valid

Gas Optimization Tips

Batch Processing: Monitor multiple intents and fulfill them in batches using multicall patterns to save gas.
Token Approval: Use approve(type(uint256).max) for tokens you frequently use to avoid repeated approval transactions.
Fulfill and Prove: Use fulfillAndProve instead of separate fulfill and prove calls to save one transaction.

Error Handling

Common errors and their solutions:
// Intent expired
if (block.timestamp > route.deadline) {
    // Skip this intent, monitor for new ones
}

// Insufficient token balance
try IERC20(token).balanceOf(address(this)) returns (uint256 balance) {
    if (balance < requiredAmount) {
        // Rebalance or skip
    }
} catch {
    // Token doesn't exist or error
}

// Intent already fulfilled
try portal.fulfill(...) {
    // Success
} catch (bytes memory reason) {
    if (bytes4(reason) == IInbox.IntentAlreadyFulfilled.selector) {
        // Someone else fulfilled it first
    }
}

Next Steps

After fulfilling an intent, you need to prove the fulfillment to claim rewards:

Proving Intents

Learn how to prove fulfillment and claim your rewards

Creating Intents

Understand the user perspective of creating intents

ERC-7683 Integration

Use the standardized interface for fulfillment

Build docs developers (and LLMs) love