Skip to main content
  • 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
  • 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.
API Reference: Quote Endpoint
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.
API Reference: Build Transaction Endpoint
// After getting the quote response with manualRoutes
const { manualRoutes } = await getQuote(quoteParamsERC20);

// Inspect the manualRoutes array and let the user pick a route
// For example, selecting the first route:
const chosenManualRoute = manualRoutes[0];

// Extract the quoteId from the chosen manual route
const quoteId = chosenManualRoute.quoteId;

// Build the transaction using the quoteId
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}`
    );
  }

  // The build-tx response contains txData, approvalData, and requestHash
  return data.result;
}

// Call build-tx to get the transaction data
const buildResult = await buildTransaction(quoteId);
const txData = buildResult.txData; // Transaction data to execute
const requestHash = buildResult.requestHash; // Use this for status checking

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.
API Reference: Status Endpoint
async function checkStatus(requestHash) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
  );
  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(
  requestHash,
  interval = 5000,
  maxAttempts = 60
) {
  let attempts = 0;

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

    // Terminal success states
    if (code === 3) {
      // FULFILLED
      return status;
    }
    if (code === 4) {
      // SETTLED
      return status;
    }

    // Terminal failure states
    if (code === 5) {
      // EXPIRED
      throw new Error(`Request expired. Status: ${JSON.stringify(status)}`);
    }
    if (code === 6) {
      // CANCELLED
      throw new Error(`Request cancelled. Status: ${JSON.stringify(status)}`);
    }
    if (code === 7) {
      // REFUNDED
      throw new Error(`Request refunded. Status: ${JSON.stringify(status)}`);
    }

    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, set in console, i.e.: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>
if (!process.env.PRIVATE_KEY) {
  console.error("Error: PRIVATE_KEY environment variable is not set");
  console.error(
    "Example: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>"
  );
  process.exit(1);
}

// Create account from private key
const normalizedPrivateKey = process.env.PRIVATE_KEY.startsWith("0x")
  ? process.env.PRIVATE_KEY
  : `0x${process.env.PRIVATE_KEY}`;
const account = privateKeyToAccount(normalizedPrivateKey);

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

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

// API and token parameters
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

// Quote parameters for ERC20 token transfer (USDC from Optimism to Arbitrum)
const quoteParamsERC20 = {
  userAddress: account.address,
  receiverAddress: account.address,
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // USDC on Optimism
  outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
  inputAmount: "10000000", // 10 USDC (with decimals)
  enableManual: true, // Important: enables manual routes
};

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

    const response = await fetch(fullUrl);
    const data = await response.json();

    if (!data.success) {
      throw new Error(`Quote error: ${data.error?.message || "Unknown error"}`);
    }

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

    return {
      manualRoutes: data.result.manualRoutes,
      fullResponse: data,
    };
  } catch (error) {
    console.error("Failed to get quote:", error);
    throw error;
  }
}

// Function to build transaction from quote ID
async function buildTransaction(quoteId) {
  try {
    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(
        `Build TX error: ${data.error?.message || "Unknown error"}`
      );
    }

    return data.result;
  } catch (error) {
    console.error("Failed to build transaction:", error);
    throw error;
  }
}

// Function to submit the ERC20 transaction
async function submitERC20Transaction(txData) {
  try {
    console.log("- Submitting transaction to inbox contract...");
    console.log("  To:", txData.to);
    console.log("  Value:", txData.value);
    console.log("  Data:", txData.data);

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

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

    // Wait for transaction to be mined
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    console.log("- Transaction mined in block:", receipt.blockNumber);

    return {
      hash,
      receipt,
    };
  } catch (error) {
    console.error("Failed to submit transaction:", error);
    throw error;
  }
}

// Function to check the status of a request
async function checkStatus(requestHash) {
  try {
    const response = await fetch(
      `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
    );
    const data = await response.json();

    if (!data.success) {
      throw new Error(
        `Status error: ${data.error?.message || "Unknown error"}`
      );
    }

    return data.result[0];
  } catch (error) {
    console.error("Failed to check status:", error);
    throw error;
  }
}

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

    // Step 1: Get the quote with manual routes
    console.log("\n1. Getting quote with manual routes...");
    const { manualRoutes } = await getQuote(quoteParamsERC20);

    if (!manualRoutes || manualRoutes.length === 0) {
      throw new Error("No manual routes available");
    }

    // Step 2: Select a route (using the first route as an example)
    console.log("\n2. Selecting route...");
    const selectedRoute = manualRoutes[0];
    const quoteId = selectedRoute.quoteId;
    console.log("- Selected Quote ID:", quoteId);
    console.log("- Available routes:", manualRoutes.length);

    // Step 3: Build the transaction from the selected route
    console.log("\n3. Building transaction...");
    const buildResult = await buildTransaction(quoteId);
    const txData = buildResult.txData;
    const approvalData = buildResult.approvalData;
    const requestHash = buildResult.requestHash;

    // Step 4: Handle token approvals if needed
    if (approvalData) {
      console.log("\n4. Handling token approvals...");
      // For ERC20 tokens, approvals are required before transfer
      // Check current allowance and create approval transaction if needed
      console.log("- Approval required:", approvalData);
      // TODO: Implement approval transaction if needed
    } else {
      console.log("\n4. No approvals required");
    }

    // Step 5: Submit the transaction
    console.log("\n5. Submitting transaction...");
    const { hash, receipt } = await submitERC20Transaction(txData);

    console.log(
      "\n6. Transaction submitted:",
      "\n- Hash:",
      hash,
      "\n- Status:",
      receipt.status
    );
    console.log("- Request Hash:", requestHash);

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

    let status;
    do {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      console.log("Checking status...");
      try {
        status = await checkStatus(requestHash);
        console.log("- Status code:", status.bungeeStatusCode);
      } catch (error) {
        console.error(
          "Failed to check status:",
          error?.message || "Unknown error"
        );
      }
    } while (!TERMINAL_STATES.includes(status?.bungeeStatusCode));

    // Handle terminal states
    if (TERMINAL_SUCCESS.includes(status?.bungeeStatusCode)) {
      console.log(
        "\n8. Transaction complete:",
        "\n- Hash:",
        status.destinationData?.txHash || "Transaction hash not available"
      );
    } else {
      const statusNames = { 5: "EXPIRED", 6: "CANCELLED", 7: "REFUNDED" };
      console.error(
        "\n8. Transaction failed:",
        `\n- Status: ${statusNames[status?.bungeeStatusCode]} (${status?.bungeeStatusCode})`,
        "\n- Details:", JSON.stringify(status, null, 2)
      );
      throw new Error(`Request ${statusNames[status?.bungeeStatusCode]}`);
    }
  } catch (error) {
    console.error("Error in processing:", error?.shortMessage || error.message);
    throw error;
  }
}

// Execute the main function
main();