Skip to main content
Crosschain routing enables complex multichain workflows that execute DeFi operations across multiple blockchains atomically. Unlike simple token transfers, crosschain routing orchestrates sophisticated strategies that span several chains. Both route and bundle API operate in crosschain mode. For custom bundles, use the bridge action to facilitate cross-chain token transfers using one of four supported bridge protocols: CCIP, CCTP (USDC-only), Relay or Stargate. For example: bridge assets to chains where protocols exist, execute operations such as minting, then optionally bridge results back to your origin chain and deposit them in a yield-bearing position.

Quick Start

Use the GET /layerzero/pool API to find the correct pool address for Stargate, or GET /ccip/router for CCIP.Always start the post-bridging callback with a balance action.

  {
    protocol: "stargate",
    action: "bridge",
    args: {
      primaryAddress: rusdEthToBeraPools[0].pool,
      destinationChainId: ETHEREUM_ID,
      ...
      callback: [

        //  Mint e-rUSD using bridged USDC on Ethereum
        {
          protocol: "reservoir",
          action: "deposit",
          args: {...},
        },
        // Bridge newly minted e-rUSD back to Berachain
        {
          protocol: "stargate",
          action: "bridge",
          args: {
            destinationChainId: BERACHAIN_ID,
            ...
            // Callback executes on Berachain after e-rUSD arrives
            callback: [
              // Deposit e-rUSD into Euler vault on Berachain
              {
                protocol: "euler-v2",
                action: "deposit",
                args: {...},
              },
            ],
          },
        },
      ],
    },
  },

Core Concepts

Bridge Protocols

Enso supports four bridge protocols: CCIP (Chainlink), CCTP (Circle, USDC-only), Relay, and Stargate (LayerZero). Each has different characteristics for callback limits, native token support, and fee handling.

Protocol Discovery

Use protocol-specific APIs to discover bridge addresses: /ccip/router for CCIP, /cctp/bridge/tokenmessengerv2 for CCTP, /layerzero/pool for Stargate.

Native Drop

Parent bridge calls calculate gas fees required for all child bridge operations and include these costs in the initial transaction fee.

Post-Bridge Execution

Callback arrays of Enso Actions execute on the destination chain after bridge completion. All actions within a bundle execute atomically.

Bridge Protocols

Enso supports four bridge protocols, each with different characteristics:
FeatureCCIPCCTPRelayStargate
ProviderChainlinkCircleRelayLayerZero
Token CoverageWhitelisted ERC20USDC onlyERC20 + nativeOFT-enabled tokens
Callback Data Limit30KBNot supportedUnlimited~9.5KB
Native Token BridgingNoNo (USDC only)YesYes
Callback Gas Limit200k–3MN/ADynamic300k base
Fee TokenNativeUSDC (from minted amount)Token itselfNative
FinalizationWaits for finality (varies by chain)Fast (where available) or wait for finalityFastFast
Discovery API/ccip/router/cctp/bridge/tokenmessengerv2Not needed (token address is the primaryAddress)/layerzero/pool

When to Use Each Bridge

Best for: Native USDC transfers across chains where the recipient shouldn’t need destination gas.
  • USDC only — bridges burn-and-mint, not wrapped
  • No callback support (use CCIP/Stargate/Relay for post-bridge actions)
  • Auto-relayed by Circle’s Forwarding Service in both fast and standard modes
  • Fees deducted from minted amount: protocol fee (Fast only, ~Iris minimumFee * 1.20) + forwarding fee (low/med/high, always)
  • cctpTransferType defaults to fast where supported, otherwise silently falls back to standard
{
  protocol: "cctp",
  action: "bridge",
  args: {
    primaryAddress: "0x...", // TokenMessengerV2 from /cctp/bridge/tokenmessengerv2
    destinationChainId: 10,
    tokenIn: "0x...", // Source-chain USDC
    amountIn: "5000000", // 5 USDC (6 decimals)
    receiver: "0x...",
    cctpTransferType: "fast", // optional: "fast" | "standard"
    cctpForwardFee: "med",    // optional: "low" | "med" | "high"
  }
}
See the CCTP Bridge use case for full details on fees, fallbacks, and the manual claim flow. Per-chain Fast/Standard support comes from Circle’s supported blockchains matrix.
Best for: Flexible bridging with dynamic amounts and native token support.
  • Uses placeholder-based design for dynamic amount resolution
  • No callback data limit (amounts resolved at execution time)
  • Supports both ERC20 and native token bridging
  • API-based fee calculation
{
  protocol: "relay",
  action: "bridge",
  args: {
    primaryAddress: "0x...", // Token address
    destinationChainId: 42161,
    tokenIn: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native supported
    amountIn: { useOutputOfCallAt: 0 },
    receiver: "0x...",
    callback: [...]
  }
}
Best for: Native token bridging and LayerZero ecosystem integration.
  • Full native token support
  • Tight callback data limit (~9.5KB) - keep callbacks concise
  • Uses LayerZero messaging for cross-chain communication
{
  protocol: "stargate",
  action: "bridge",
  args: {
    primaryAddress: "0x...", // OFT pool from /layerzero/pool API
    destinationChainId: 80094,
    tokenIn: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native supported
    amountIn: "1000000000000000000",
    receiver: "0x...",
    callback: [...]
  }
}

When to use Route vs Bundle API?

Use Route API for: Limitations: The route API automatically selects the optimal bridge protocol - bridge protocol cannot be explicitly selected. Cannot handle custom post-bridge logic or multi-step protocol interactions. See Bridge Transaction Status to track delivery after submission. Use Bundle API for: Advantages: Allows explicit bridge protocol selection (CCIP, Relay, Stargate, or CCTP). Limitations: Single callback sequence with up to 10 chained actions. Callback data limits vary by protocol (see Bridge Protocols).

Examples

1. Simple Cross-Chain Swap

Use Route API for basic cross-chain operations with automatic pathfinding.
SDK
// ETH on Ethereum → USDC on Base
const route = await ensoClient.getRouteData({
  fromAddress: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  chainId: 1, // Ethereum
  destinationChainId: 8453, // Base
  tokenIn: ["0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"], // ETH
  tokenOut: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"], // USDC Base
  amountIn: ["1000000000000000000"], // 1 ETH
  slippage: "300", // 3%
  routingStrategy: "delegate"
});

2. Crosschain Vault Zap

In this example, we’ll bridge ETH from Ethereum to zap it to a Ether.fi weETH vault on Base. Try this route →
const route = await ensoClient.getRouteData({
  fromAddress: "0x...",
  chainId: 1, // Ethereum
  destinationChainId: 8453, // Base
  tokenIn: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], // USDC Ethereum
  tokenOut: ["0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A"], // weETH vault on Base
  amountIn: ["1000000000"], // 1,000 USDC
  slippage: "500", // 5% (complex operation)
  routingStrategy: "delegate",
});

// Bridge + vault entry in one transaction
await wallet.sendTransaction(route.tx);

3. Cross-Chain Position Minting

Crosschain routing enables you to mint positions on different chains and bridge them back, while signing only once. In this example, we’ll bridge USDC from Berachain to Ethereum, mint e-rUSD using Reservoir protocol, then bridge rUSD back to Berachain. About Reservoir: Reservoir is a stablecoin protocol that mints rUSD (a USD-pegged stablecoin) by accepting USDC as collateral on Ethereum mainnet.
Use the GET layerzero/pool API or client.getLayerZeroPool() from the SDK to find the correct pool address and use it as the primaryAddress for stargate.bridge operation.
What’s happening: This workflow demonstrates a complete round-trip bridge operation - taking USDC from Berachain, minting a stablecoin on Ethereum where the protocol exists, then bringing the newly minted e-rUSD back to the origin chain. Understanding callbacks execution Callbacks execute on the destination chain after the bridge completes, but they’re not separate transactions. The bridge operation includes encoded instructions that execute atomically using Enso’s crosschain execution engine. This means:
  • Atomic Safety: All callback actions execute as a single atomic transaction on the destination chain. If any action fails, the entire callback transaction reverts and bridged tokens are sent to the refundReceiver address. Funds never get stuck in an intermediate state.
  • Gas Management: The initial transaction on the source chain calculates and pays for all destination chain gas costs using LayerZero’s native drop feature. You don’t need to hold native tokens on every destination chain.
  • Output Chaining: Callbacks can reference outputs from previous callback actions using useOutputOfCallAt, enabling complex multi-step workflows that adapt to actual bridged amounts rather than fixed values.
mintOnBeraFromMainnet.ts
// Chain IDs
const BERACHAIN_ID = 80094;
const ETHEREUM_ID = 1;

// Common addresses
const WALLET_ADDRESS = "0x93621DCA56fE26Cdee86e4F6B18E116e9758Ff11"; // User wallet

// Token addresses
const USDC_BERACHAIN = "0x549943e04f40284185054145c6E4e9568C1D3241";
const USDC_ETHEREUM = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const RUSD_ETHEREUM = "0x09D4214C03D01F49544C0448DBE3A27f768F2b34";

// Protocol addresses
const RESERVOIR_MINTING_CONTRACT = "0x4809010926aec940b550D34a46A52739f996D75D";

const client = new EnsoClient({
  apiKey: process.env.ENSO_API_KEY || "your-api-key-here",
});

const usdcBeraToEthPools = await client.getLayerZeroPool({
  chainId: BERACHAIN_ID,
  token: USDC_BERACHAIN,
  destinationChainId: ETHEREUM_ID + "",
});

const rusdEthToBeraPools = await client.getLayerZeroPool({
  chainId: ETHEREUM_ID,
  token: RUSD_ETHEREUM,
  destinationChainId: BERACHAIN_ID + "",
});

if (!(usdcBeraToEthPools.length && usdcBeraToEthPools.length)) {
  throw new Error("Required pools not available");
}

const bundle = await client.getBundleData(
  {
    chainId: BERACHAIN_ID,
    fromAddress: WALLET_ADDRESS,
    spender: WALLET_ADDRESS,
    routingStrategy: "router",
    refundReceiver: WALLET_ADDRESS,
  },
  [
    {
      protocol: "stargate",
      action: "bridge",
      args: {
        primaryAddress: usdcBeraToEthPools[0].pool,
        destinationChainId: ETHEREUM_ID,
        tokenIn: USDC_BERACHAIN,
        amountIn: parseUnits("1000", 6).toString(), // 1000 USDC
        receiver: WALLET_ADDRESS,
        callback: [
          // Step 1: Check USDC balance on Ethereum after bridge
          {
            protocol: "enso",
            action: "balance",
            args: {
              token: USDC_ETHEREUM,
            },
          },
          // Step 2: Mint e-rUSD using bridged USDC
          {
            protocol: "reservoir",
            action: "deposit",
            args: {
              primaryAddress: RESERVOIR_MINTING_CONTRACT,
              tokenIn: USDC_ETHEREUM,
              tokenOut: RUSD_ETHEREUM,
              amountIn: { useOutputOfCallAt: 0 }, // Use USDC from balance check
              receiver: WALLET_ADDRESS,
            },
          },
          // Step 3: Bridge newly minted e-rUSD back to Berachain
          {
            protocol: "stargate",
            action: "bridge",
            args: {
              primaryAddress: rusdEthToBeraPools[0].pool,
              destinationChainId: BERACHAIN_ID,
              tokenIn: RUSD_ETHEREUM,
              amountIn: { useOutputOfCallAt: 1 }, // Use e-rUSD from minting
              receiver: WALLET_ADDRESS,
            },
          },
        ],
      },
    },
  ],
);

4. Crosschain Yield Strategy

Nested callbacks enable multi-hop workflows, allowing operations that span multiple chains where different protocols exist. In this example, we’ll do an Euler deposit of rUSD tokens minted on a Berachain by using Ethereum Mainnet assets. The user starts with USDC on Berachain and ends with yield-generating vault shares with a single signature.
mintOnBeraDepositOnMainnet.ts
const BERACHAIN_ID = 80094;
const ETHEREUM_ID = 1;

// Common addresses
const WALLET_ADDRESS = "0x93621DCA56fE26Cdee86e4F6B18E116e9758Ff11"; // User wallet

// Token addresses
const USDC_BERACHAIN = "0x549943e04f40284185054145c6E4e9568C1D3241";
const USDC_ETHEREUM = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const RUSD_ETHEREUM = "0x09D4214C03D01F49544C0448DBE3A27f768F2b34";
const RUSD_BERACHAIN = "0x09D4214C03D01F49544C0448DBE3A27f768F2b34";

// Protocol addresses
const RESERVOIR_MINTING_CONTRACT = "0x4809010926aec940b550D34a46A52739f996D75D";
const EULER_VAULT_E_RUSD_BERACHAIN =
  "0x109D6D1799f62216B4a7b0c6e245844AbD4DD281"; // Euler vault for e-rUSD on Berachain (need actual address)

const client = new EnsoClient({
  apiKey: process.env.ENSO_API_KEY!,
});

const usdcBeraToEthPools = await client.getLayerZeroPool({
  chainId: BERACHAIN_ID,
  token: USDC_BERACHAIN,
  destinationChainId: ETHEREUM_ID + "",
});

const rusdEthToBeraPools = await client.getLayerZeroPool({
  chainId: ETHEREUM_ID,
  token: RUSD_ETHEREUM,
  destinationChainId: BERACHAIN_ID + "",
});

if (!(usdcBeraToEthPools.length && usdcBeraToEthPools.length)) {
  throw new Error("Required pools not available");
}

const bundle = await client.getBundleData(
  {
    chainId: BERACHAIN_ID,
    fromAddress: WALLET_ADDRESS,
    spender: WALLET_ADDRESS,
    routingStrategy: "router",
    refundReceiver: WALLET_ADDRESS,
  },
  [
    {
      protocol: "stargate",
      action: "bridge",
      args: {
        primaryAddress: rusdEthToBeraPools[0].pool,
        destinationChainId: ETHEREUM_ID,
        tokenIn: USDC_BERACHAIN,
        amountIn: parseUnits("1000", 6).toString(), // 1000 USDC
        receiver: WALLET_ADDRESS,
        callback: [
          // Step 1: Check USDC balance on Ethereum after bridge
          {
            protocol: "enso",
            action: "balance",
            args: {
              token: USDC_ETHEREUM,
            },
          },
          // Step 2: Mint e-rUSD using bridged USDC on Ethereum
          {
            protocol: "reservoir",
            action: "deposit",
            args: {
              primaryAddress: RESERVOIR_MINTING_CONTRACT,
              tokenIn: USDC_ETHEREUM,
              tokenOut: RUSD_ETHEREUM,
              amountIn: { useOutputOfCallAt: 0 }, // Use USDC from balance check
              receiver: WALLET_ADDRESS,
            },
          },
          // Step 3: Bridge newly minted e-rUSD back to Berachain
          {
            protocol: "stargate",
            action: "bridge",
            args: {
              primaryAddress: rusdEthToBeraPools[0].pool,
              destinationChainId: BERACHAIN_ID,
              tokenIn: RUSD_ETHEREUM,
              amountIn: { useOutputOfCallAt: 1 }, // Use e-rUSD from minting
              receiver: WALLET_ADDRESS,
              // Callback executes on Berachain after e-rUSD arrives
              callback: [
                // Step 4: Check e-rUSD balance on Berachain
                {
                  protocol: "enso",
                  action: "balance",
                  args: {
                    token: RUSD_BERACHAIN,
                  },
                },
                // Step 5: Deposit e-rUSD into Euler vault on Berachain
                {
                  protocol: "euler-v2",
                  action: "deposit",
                  args: {
                    primaryAddress: EULER_VAULT_E_RUSD_BERACHAIN,
                    tokenIn: RUSD_BERACHAIN,
                    tokenOut: EULER_VAULT_E_RUSD_BERACHAIN, // ERC4626 vault token
                    amountIn: { useOutputOfCallAt: 0 }, // Use e-rUSD from balance check
                    receiver: WALLET_ADDRESS,
                  },
                },
              ],
            },
          },
        ],
      },
    },
  ],
);

5. CCIP Bridge with Callback

Use CCIP for ERC20 token bridging with larger callback payloads. This example bridges SolvBTC from BNB Chain to Base, then swaps it to native ETH.
Use the GET /ccip/router API to get the CCIP Router address for the primaryAddress parameter.
ccipBridgeWithCallback.ts
const BNB_CHAIN_ID = 56;
const BASE_CHAIN_ID = 8453;

const WALLET_ADDRESS = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";

// Token addresses
const SOLVBTC_BNB = "0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7";
const SOLVBTC_BASE = "0x3B86Ad95859b6AB773f55f8d94B4b9d443EE931f";

const client = new EnsoClient({
  apiKey: process.env.ENSO_API_KEY!,
});

// Get CCIP Router address for BNB Chain
const ccipRouter = await client.getCcipRouter({
  chainId: BNB_CHAIN_ID,
});

const bundle = await client.getBundleData(
  {
    chainId: BNB_CHAIN_ID,
    fromAddress: WALLET_ADDRESS,
    routingStrategy: "router",
  },
  [
    {
      protocol: "ccip",
      action: "bridge",
      args: {
        primaryAddress: ccipRouter.router,
        destinationChainId: BASE_CHAIN_ID,
        tokenIn: SOLVBTC_BNB,
        amountIn: "100000000000000", // 0.0001 SolvBTC
        receiver: WALLET_ADDRESS,
        callback: [
          // Step 1: Check SolvBTC balance on Base after bridge
          {
            protocol: "enso",
            action: "balance",
            args: {
              token: SOLVBTC_BASE,
            },
          },
          // Step 2: Swap SolvBTC to native ETH on Base
          {
            protocol: "enso",
            action: "route",
            args: {
              slippage: "100", // 1%
              tokenIn: SOLVBTC_BASE,
              tokenOut: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
              amountIn: { useOutputOfCallAt: 0 },
            },
          },
        ],
      },
    },
  ],
);

6. Mixed Bridge Protocols

Combine different bridge protocols in a single workflow. This example uses Stargate for the outbound bridge and CCIP for the return bridge, useful when different protocols support different token pairs.
mixedBridgeProtocols.ts
const PLASMA_CHAIN_ID = 9745;
const ETHEREUM_ID = 1;

const WALLET_ADDRESS = "0x...";

// Token addresses
const USDT_PLASMA = "0x...";
const USDT_ETHEREUM = "0xdac17f958d2ee523a2206206994597c13d831ec7";
const SYRUP_USDT_ETHEREUM = "0x...";
const SYRUP_USDT_PLASMA = "0x...";

const client = new EnsoClient({ apiKey: process.env.ENSO_API_KEY! });

// Get pool addresses for both bridges
const stargatePool = await client.getLayerZeroPool({
  chainId: PLASMA_CHAIN_ID,
  token: USDT_PLASMA,
  destinationChainId: ETHEREUM_ID + "",
});

const ccipRouter = await client.getCcipRouter({
  chainId: ETHEREUM_ID,
});

const bundle = await client.getBundleData(
  {
    chainId: PLASMA_CHAIN_ID,
    fromAddress: WALLET_ADDRESS,
    routingStrategy: "router",
  },
  [
    {
      protocol: "stargate",
      action: "bridge",
      args: {
        primaryAddress: stargatePool[0].pool,
        destinationChainId: ETHEREUM_ID,
        tokenIn: USDT_PLASMA,
        amountIn: "1000000000", // 1000 USDT
        receiver: WALLET_ADDRESS,
        callback: [
          // Step 1: Check USDT balance on Ethereum
          {
            protocol: "enso",
            action: "balance",
            args: { token: USDT_ETHEREUM },
          },
          // Step 2: Swap USDT to syrupUSDT on Ethereum
          {
            protocol: "enso",
            action: "route",
            args: {
              tokenIn: USDT_ETHEREUM,
              tokenOut: SYRUP_USDT_ETHEREUM,
              amountIn: { useOutputOfCallAt: 0 },
            },
          },
          // Step 3: Bridge syrupUSDT back to Plasma via CCIP
          {
            protocol: "ccip",
            action: "bridge",
            args: {
              primaryAddress: ccipRouter.router,
              destinationChainId: PLASMA_CHAIN_ID,
              tokenIn: SYRUP_USDT_ETHEREUM,
              amountIn: { useOutputOfCallAt: 1 },
              receiver: WALLET_ADDRESS,
              // Callback on Plasma after CCIP bridge
              callback: [
                {
                  protocol: "enso",
                  action: "balance",
                  args: { token: SYRUP_USDT_PLASMA },
                },
                {
                  protocol: "aave-v3",
                  action: "deposit",
                  args: {
                    primaryAddress: "0x...", // AAVE V3 Plasma pool
                    tokenIn: SYRUP_USDT_PLASMA,
                    amountIn: { useOutputOfCallAt: 0 },
                    receiver: WALLET_ADDRESS,
                  },
                },
              ],
            },
          },
        ],
      },
    },
  ],
);

7. Relay Bridge with Callback

Use Relay for bridging with dynamic amount resolution. This example bridges ETH from Ethereum to Arbitrum and wraps it to WETH.
relayBridge.ts
const ETHEREUM_ID = 1;
const ARBITRUM_ID = 42161;

const WALLET_ADDRESS = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
const USDC_ETHEREUM = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const WETH_ARBITRUM = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
const NATIVE_TOKEN = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";

const client = new EnsoClient({ apiKey: process.env.ENSO_API_KEY! });

const bundle = await client.getBundleData(
  {
    chainId: ETHEREUM_ID,
    fromAddress: WALLET_ADDRESS,
    routingStrategy: "router",
  },
  [
    // Step 1: Swap USDC to ETH on Ethereum
    {
      protocol: "enso",
      action: "route",
      args: {
        tokenIn: USDC_ETHEREUM,
        tokenOut: NATIVE_TOKEN,
        amountIn: "10000000", // 10 USDC
      },
    },
    // Step 2: Bridge ETH to Arbitrum via Relay
    {
      protocol: "relay",
      action: "bridge",
      args: {
        primaryAddress: "0xa5f565650890fba1824ee0f21ebbbf660a179934",
        destinationChainId: ARBITRUM_ID,
        tokenIn: NATIVE_TOKEN, // Relay supports native tokens
        amountIn: { useOutputOfCallAt: 0 },
        receiver: WALLET_ADDRESS,
        callback: [
          // Step 3: Check ETH balance on Arbitrum
          {
            protocol: "enso",
            action: "balance",
            args: { token: NATIVE_TOKEN },
          },
          // Step 4: Wrap ETH to WETH on Arbitrum
          {
            protocol: "wrapped-native",
            action: "deposit",
            args: {
              primaryAddress: WETH_ARBITRUM,
              tokenIn: NATIVE_TOKEN,
              tokenOut: WETH_ARBITRUM,
              amountIn: { useOutputOfCallAt: 0 },
            },
          },
        ],
      },
    },
  ],
);

8. CCTP Bridge

Bridge native USDC across chains with Circle’s CCTP — burn on the source, mint on the destination. Both fast and standard transfers are auto-relayed by Circle’s Forwarding Service, so the receiver never needs destination gas.
Use the GET /api/v1/cctp/bridge/tokenmessengerv2 API to fetch the TokenMessengerV2 address for the source chain.
cctpBridge.ts
import { EnsoClient } from "@ensofinance/sdk";

const ETHEREUM_ID = 1;
const ARBITRUM_ID = 42161;
const WALLET_ADDRESS = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
const USDC_ETHEREUM = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

const client = new EnsoClient({ apiKey: process.env.ENSO_API_KEY! });

// Fetch TokenMessengerV2 for the source chain
const { address: cctpTokenMessenger } = await fetch(
  `https://api.enso.finance/api/v1/cctp/bridge/tokenmessengerv2?chainId=${ETHEREUM_ID}`,
  { headers: { Authorization: `Bearer ${process.env.ENSO_API_KEY}` } },
).then((r) => r.json());

const bundle = await client.getBundleData(
  {
    chainId: ETHEREUM_ID,
    fromAddress: WALLET_ADDRESS,
    receiver: WALLET_ADDRESS,
    routingStrategy: "router",
  },
  [
    {
      protocol: "cctp",
      action: "bridge",
      args: {
        primaryAddress: cctpTokenMessenger,
        destinationChainId: ARBITRUM_ID,
        tokenIn: USDC_ETHEREUM,
        amountIn: "1000000000", // 1,000 USDC (6 decimals)
        receiver: WALLET_ADDRESS,
      },
    },
  ],
);

await wallet.sendTransaction(bundle.tx);
See CCTP Bridge for the full fee model, fast vs standard trade-offs, and the manual claim/recovery flow.

Reference

Bridge Action Parameters

All bridge protocols share a common set of parameters:
ParameterDescriptionRequired
protocolBridge protocol: "ccip", "cctp", "relay", or "stargate"Yes
actionAlways "bridge"Yes
primaryAddressProtocol-specific address (see below)Yes
destinationChainIdTarget blockchain network IDYes
tokenInToken address to bridge from source chainYes
amountInAmount to bridge (with full decimals) or reference to previous action outputYes
receiverAddress to receive bridged tokens on destination chainYes
callbackArray of actions to execute on destination chain after bridgingNo
refundReceiverAddress to receive bridged tokens if callback execution fails. Defaults to receiver.No

Protocol-Specific Parameters

primaryAddress: CCIP Router address from /ccip/router APICallback limits:
  • Data limit: ~30KB
  • Gas limit: 200k (default) to 3M (max)
Notes:
  • ERC20 tokens only (no native token bridging)
  • Fee paid in native token
// Get CCIP Router for the source chain
const ccipRouter = await client.getCcipRouter({
  chainId: sourceChainId,
});
// Use: primaryAddress: ccipRouter.router

Callback Requirements

Critical: All callback sequences must begin with a balance check action to verify the bridged token amount on the destination chain.
  1. First action must be the balance action:
{
  "protocol": "enso",
  "action": "balance",
  "args": { "token": "bridged_token_address" }
}
  1. Reference previous outputs: Use useOutputOfCallAt to chain actions together
{ "useOutputOfCallAt": 0 }
Or with specific output index:
{ "useOutputOfCallAt": 0, "index": 1 }
  1. Nested callbacks: Bridge actions within callbacks enable multi-hop workflows across multiple chains
  2. Callback data limits: Keep callbacks concise for Stargate (~9.5KB limit). CCIP supports larger payloads (~30KB). Relay has no practical limit. CCTP does not support callbacks.

Protocol Discovery APIs

ProtocolAPI EndpointReturns
CCIPGET /ccip/routerCCIP Router address
CCTPGET /cctp/bridge/tokenmessengerv2TokenMessengerV2 address per chain
RelayNot needed — use the bridged token address as primaryAddress
StargateGET /layerzero/poolOFT pool address

Updated