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:
Getting a quote from the Bungee API
Building the transaction from a selected route
Handling any required token approvals
Executing the crosschain transaction
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)
Complete Integration Example for ERC20 Using Viem
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 ();