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

# How-to: Build a hook receiver

> Implement executeWithCrossChainToken so logic runs atomically when a CCTP for non-USDC transfer arrives.

A hook receiver is a destination-side contract that receives CCTP for non-USDC
tokens and runs custom logic in the same transaction that delivers them. The
receiver must implement `executeWithCrossChainToken` and return
[`EXECUTE_SUCCESS`](/cctp/expanded-assets/references/contract-reference#execute_success).
For background, see [Hooks](/cctp/expanded-assets/concepts/hooks).

## Prerequisites

Before you begin, ensure that you've:

* Set up a Solidity development environment (Foundry, Hardhat, or similar)
* Located the local
  [`CrossChainTokenService` address](/cctp/expanded-assets/references/contract-addresses)

## Steps

<Steps>
  <Step title="Implement executeWithCrossChainToken">
    Create a contract that exposes the hook entrypoint the destination service
    calls. Restrict the caller to the local `CrossChainTokenService`, run your
    logic, and return `EXECUTE_SUCCESS`.

    ```solidity Solidity theme={null}
    import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

    contract VaultDepositReceiver {
        bytes32 public constant EXECUTE_SUCCESS =
            keccak256("circle-cctpx-execute-success");

        address public immutable service;
        address public immutable vault;

        error OnlyService();

        constructor(address _service, address _vault) {
            service = _service;
            vault = _vault;
        }

        function executeWithCrossChainToken(
            uint32 /* sourceDomain */,
            bytes calldata /* sourceAddress */,
            bytes32 /* tokenId */,
            address token,
            uint256 amount,
            uint32 /* finalityThresholdExecuted */,
            bytes calldata hookData
        ) external returns (bytes32) {
            if (msg.sender != service) revert OnlyService();

            address recipient = abi.decode(hookData, (address));
            IERC20(token).approve(vault, amount);
            IVault(vault).depositFor(recipient, token, amount);

            return EXECUTE_SUCCESS;
        }
    }
    ```

    <Warning>
      Reverting inside `executeWithCrossChainToken` rolls back the entire crosschain
      transfer. Choose deliberately whether you want a failure to preserve the
      source-domain transfer or to deliver tokens without running your logic.
    </Warning>
  </Step>

  <Step title="Deploy the receiver on every destination blockchain">
    Deploy the receiver on every blockchain where transfers will arrive with hook
    data. Each deployment must bind to the local `CrossChainTokenService`.
  </Step>

  <Step title="Send a transfer with hook data">
    On the source blockchain, encode the hook payload and call
    [`crossChainTransfer`](/cctp/expanded-assets/references/contract-reference) with
    `destinationAddress` set to the receiver contract and `autoExecuteHookData` set
    to `true`.

    ```typescript TypeScript theme={null}
    import { encodeAbiParameters, encodePacked } from "viem";

    // hookData is an application payload — ABI encoding is fine here.
    const hookData = encodeAbiParameters([{ type: "address" }], [recipient]);

    // destinationAddress must be raw packed address bytes (20 bytes on EVM).
    // Do not use encodeAbiParameters — that pads to 32 bytes and breaks delivery.
    const destinationAddress = encodePacked(["address"], [receiverAddress]);

    await walletClient.writeContract({
      address: serviceAddress,
      abi: serviceAbi,
      functionName: "crossChainTransfer",
      args: [
        tokenId,
        100n * 10n ** 18n,
        destinationDomain,
        destinationAddress,
        "0x0000000000000000000000000000000000000000000000000000000000000000",
        2000, // standard finality; prefer this for hooks
        claim,
        true, // autoExecuteHookData
        hookData,
      ],
      value: feeTotalAmount,
    });
    ```

    The `claim` and `feeTotalAmount` values are returned by the Iris fee-quote
    endpoint. Use a standard quote body (`amount` + `feeToken` only) unless your
    token is enabled for fast transfer. Do not include `FORWARD` when
    `autoExecuteHookData` is `true`—hook transfers require self-relay. See
    [`QuoteClaim`](/cctp/expanded-assets/references/contract-reference).

    For `BURN_MINT` or `LOCK_UNLOCK` tokens, approve the source `TokenManager` (not
    the service) before calling `crossChainTransfer`.
  </Step>

  <Step title="Relay the message to the destination">
    Iris does not relay transfers that carry hook data with `autoExecuteHookData`
    set to `true`. After Iris attests the source blockchain message, you or your own
    relayer must call `receiveMessage` on the destination `MessageTransmitterV2` to
    deliver the transfer and trigger the hook. See
    [Self-relay required for hook transfers](/cctp/expanded-assets/concepts/hooks#self-relay-required-for-hook-transfers).

    When the destination service receives the message, it delivers the tokens and
    calls your receiver's hook in the same transaction.
  </Step>
</Steps>
