Skip to main content
This page covers how to integrate Bungee Manual for crosschain swaps as it is useful when:
  • You need more control over the crosschain process
  • You want to compare multiple routes before executing
This method involves:
  1. Getting a quote from the Bungee API
  2. Building the transaction from a selected route
  3. Handling any required token approvals
  4. Executing the crosschain transaction
  5. Monitoring the status via the API
Slippage behavior (Manual vs Auto)
  • In Bungee Manual, the slippage you provide applies only to the swap that may occur on the origin chain before bridging. If no origin swap is needed, the slippage setting does not apply. The bridge step itself does not use swap-style slippage because bridges have different mechanics.
  • In Bungee Auto, the slippage parameter is applied to the full end-to-end route (origin swap, bridge step, and any destination swap where applicable).

Integration Steps

Step 1: Get a Quote

For ERC20 & Native tokens, request a quote with all required parameters, including the option to enable manual routes.
The enableManual: true parameter is required to receive manual routes in the response. Without this parameter, only auto routes will be returned.
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

const quoteParamsERC20 = {
  userAddress: USER_ADDRESS,
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // USDC on Optimism
  inputAmount: "10000000", // 10 USDC (with decimals)
  receiverAddress: RECEIVER_ADDRESS,
  outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
  enableManual: true, // Important: enables manual routes
};

async function getQuote(params) {
  const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
  const queryParams = new URLSearchParams(params);
  const queryUrl = `${url}?${queryParams}`;

  const response = await fetch(queryUrl);
  const data = await response.json();
  const serverReqId = response.headers.get("server-req-id");

  if (!data.success) {
    throw new Error(
      `Quote error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
    );
  }

  if (!data.result.manualRoutes || data.result.manualRoutes.length === 0) {
    throw new Error(
      `No manual routes available. server-req-id: ${serverReqId}`
    );
  }

  return {
    manualRoutes: data.result.manualRoutes,
    completeResponse: data,
    serverReqId,
  };
}

Step 2: Build the Transaction from Selected Route

The quote response contains multiple manual routes. Select one and build the transaction data by submitting the quote ID of the selected route.
async function buildTransaction(quoteId) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/build-tx?quoteId=${quoteId}`
  );
  const data = await response.json();
  const serverReqId = response.headers.get("server-req-id");

  if (!data.success) {
    throw new Error(
      `Build TX error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
    );
  }

  return data.result;
}

Step 3: Handle Token Approvals (if needed)

Check if the token needs to be approved for spending and create an approval transaction if required. Approval data is included in the build transaction response.
const ERC20_ABI = parseAbi([
  "function approve(address spender, uint256 amount) returns (bool)",
  "function allowance(address owner, address spender) view returns (uint256)",
]);

async function buildApprovalTransaction(chainId, approvalData) {
  // Check current allowance
  const currentAllowance = await publicClient.readContract({
    address: approvalData.tokenAddress,
    abi: ERC20_ABI,
    functionName: "allowance",
    args: [approvalData.userAddress, approvalData.spenderAddress],
  });

  // If allowance is sufficient, no approval needed
  if (currentAllowance >= BigInt(approvalData.amount)) {
    return null;
  }

  // Create approval transaction data
  const data = encodeFunctionData({
    abi: ERC20_ABI,
    functionName: "approve",
    args: [approvalData.spenderAddress, approvalData.amount],
  });

  return {
    to: approvalData.tokenAddress,
    data,
    chainId: chainId,
    value: "0x00",
  };
}

Step 4: Execute the Transactions

From the build transaction response, you can get the transaction data and send it to the blockchain.
async function sendTransaction(txData) {
  const hash = await walletClient.sendTransaction({
    to: txData.to,
    data: txData.data,
    value: txData.value ? BigInt(txData.value) : BigInt(0),
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  return { hash, receipt };
}
If routing via Stargate, include a msg.value equal to the protocolFees.amount and ensure the address has enough native tokens to cover it.

Step 5: Monitor the Transaction Status

After submitting the request, check its status to track progress.
async function checkStatus(txHash) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?txHash=${txHash}`
  );
  const data = await response.json();
  const serverReqId = response.headers.get("server-req-id");

  if (!data.success) {
    throw new Error(
      `Status error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
    );
  }

  return data.result;
}

async function pollForCompletion(
  txHash,
  interval = 5000,
  maxAttempts = 60
) {
  let attempts = 0;

  while (attempts < maxAttempts) {
    const status = await checkStatus(txHash);
    const code = status[0]?.bungeeStatusCode;

    // Success state
    if (code === 3) {
      return status; // Transaction complete
    }

    // Terminal failure states
    if (code === 5 || code === 6 || code === 7) {
      const statusMessage = status[0]?.message || "No additional message";
      const statusNames = { 5: "EXPIRED", 6: "CANCELLED", 7: "REFUNDED" };
      throw new Error(
        `Transaction terminated with status ${code} (${statusNames[code]}): ${statusMessage}`
      );
    }

    attempts++;
    await new Promise((resolve) => setTimeout(resolve, interval));
  }

  throw new Error("Polling timed out. Transaction may not have completed.");
}
For a detailed explanation of all status codes, see the Request Status Codes guide.

Complete Integration Example (ERC20)

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { optimism } from "viem/chains";

// Check if PRIVATE_KEY is set
if (!process.env.PRIVATE_KEY) {
  console.error("Error: PRIVATE_KEY environment variable is not set");
  process.exit(1);
}

// Create account from private key
// Normalize private key to ensure it has exactly one "0x" prefix (required by viem)
const normalizedPrivateKey = `0x${process.env.PRIVATE_KEY.replace(/^0x/i, '')}`;
const account = privateKeyToAccount(normalizedPrivateKey);

const publicClient = createPublicClient({
  chain: optimism,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: optimism,
  transport: http(),
});

const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

const quoteParams = {
  userAddress: account.address,
  receiverAddress: account.address,
  originChainId: 10,
  destinationChainId: 42161,
  inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85",
  outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  inputAmount: "10000000",
  enableManual: true,
};

async function getQuote(params) {
  const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
  const queryParams = new URLSearchParams(params);
  const response = await fetch(`${url}?${queryParams}`);
  const data = await response.json();

  if (!data.success || !data.result.manualRoutes?.length) {
    throw new Error("No manual routes available");
  }

  return data.result.manualRoutes;
}

async function buildTransaction(quoteId) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/build-tx?quoteId=${quoteId}`
  );
  const data = await response.json();

  if (!data.success) {
    throw new Error("Failed to build transaction");
  }

  return data.result;
}

async function checkStatus(txHash) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?txHash=${txHash}`
  );
  const data = await response.json();
  return data.result[0];
}

async function main() {
  console.log("Starting Bungee Manual Route Test...");

  // Get routes
  const routes = await getQuote(quoteParams);
  console.log(`Found ${routes.length} routes`);

  // Select first route
  const selectedRoute = routes[0];
  const quoteId = selectedRoute.quoteId;

  // Build transaction
  const txResult = await buildTransaction(quoteId);

  // Handle approval if needed
  if (txResult.approvalData) {
    // ... handle approval
  }

  // Send transaction
  const hash = await walletClient.sendTransaction({
    to: txResult.txData.to,
    data: txResult.txData.data,
    value: txResult.txData.value ? BigInt(txResult.txData.value) : BigInt(0),
  });

  console.log("Transaction sent:", hash);

  // Poll for completion
  // Terminal states: 3 = FULFILLED, 4 = SETTLED (success), 5 = EXPIRED, 6 = CANCELLED, 7 = REFUNDED (failure)
  const TERMINAL_SUCCESS = [3, 4];
  const TERMINAL_FAILURE = [5, 6, 7];
  const TERMINAL_STATES = [...TERMINAL_SUCCESS, ...TERMINAL_FAILURE];

  let status;
  do {
    await new Promise((r) => setTimeout(r, 5000));
    status = await checkStatus(hash);
  } while (!TERMINAL_STATES.includes(status?.bungeeStatusCode));

  if (TERMINAL_SUCCESS.includes(status?.bungeeStatusCode)) {
    console.log("Complete:", status.destinationData?.txHash);
  } else {
    const statusNames = { 5: "EXPIRED", 6: "CANCELLED", 7: "REFUNDED" };
    throw new Error(`Request ${statusNames[status?.bungeeStatusCode]}: ${JSON.stringify(status)}`);
  }
}

main();