Skip to main content

Routing & Simulation

The routing API allows you to simulate deposits before sending real funds. This helps you show users exact fees, output amounts, and verify that a route exists for their desired token pair.

Simulate Deposit

Preview a cross-chain route without moving funds. Uses the user’s stored Deposit Intent to determine the destination, or allows you to override it for testing. Endpoint: POST /api/routing/simulate-deposit Authentication: Required (API Key)

Request

Headers:
x-api-key: your_api_key
Content-Type: application/json
Body:
{
  "userId": "string",
  "fromChainId": number,
  "fromTokenSymbol": "string",
  "amountInAtomic": "string",
  "overrideTargetChainId": number,
  "overrideTargetTokenAddress": "string",
  "overrideTargetAddress": "string"
}
Parameters:
FieldTypeRequiredDescription
userIdstringYesUser identifier (must have a Deposit Intent)
fromChainIdnumberYesSource chain ID (e.g., 8453 for Base)
fromTokenSymbolstringYesToken symbol on source chain (e.g., “USDC”)
amountInAtomicstringYesDeposit amount in atomic units (as string)
overrideTargetChainIdnumberNoTemporarily override destination chain
overrideTargetTokenAddressstringNoTemporarily override destination token
overrideTargetAddressstringNoTemporarily override recipient address
About Atomic Units:
  • Always use the smallest unit of the token (like “wei” for ETH)
  • Pass as a string to preserve precision
  • Examples:
    • 10 USDC (6 decimals) = "10000000"
    • 0.5 ETH (18 decimals) = "500000000000000000"
    • 100 MATIC (18 decimals) = "100000000000000000000"

Response

Status: 200 OK
{
  "userId": "user-123",
  "hasIntent": true,
  "fromChainId": 8453,
  "fromChainKey": "base",
  "fromTokenSymbol": "USDC",
  "amountInAtomic": "10000000",
  "targetChainId": 137,
  "targetChainKey": "polygon",
  "targetTokenSymbol": "USDC",
  "targetAddress": "0x5555555555555555555555555555555555555555",
  "amountOutExpectedAtomic": "9950000",
  "gasFeeRaw": "25000",
  "gasFeeSkipReason": "NONE",
  "amountForSwapAtomic": "9925000",
  "provider": "lifi",
  "policy": {
    "slippageBps": 50,
    "minOutputBps": 9800,
    "maxDurationSeconds": 600,
    "maxHops": 3,
    "requiresSponsoredGas": false
  }
}
Response Fields:
FieldTypeDescription
userIdstringUser identifier
hasIntentbooleanWhether user has a Deposit Intent configured
fromChainIdnumberSource chain ID
fromChainKeystringSource chain key (human-readable)
fromTokenSymbolstringSource token symbol
amountInAtomicstringInput amount in atomic units
targetChainIdnumberDestination chain ID
targetChainKeystringDestination chain key
targetTokenSymbolstringDestination token symbol
targetAddressstringRecipient address on destination chain
amountOutExpectedAtomicstringExpected output after all fees (atomic units)
gasFeeRawstringGas fee in source token atomic units (0 if sponsored)
gasFeeSkipReasonstringWhy gas fee wasn’t charged (if sponsored)
amountForSwapAtomicstringAmount after protocol/gas fees, before bridging
providerstringRouting provider used (e.g., “lifi”)
policyobjectRouting policy applied
Policy Object:
FieldTypeDescription
slippageBpsnumberMaximum slippage tolerance in basis points
minOutputBpsnumberMinimum output percentage (10000 = 100%)
maxDurationSecondsnumberMaximum estimated completion time
maxHopsnumberMaximum number of bridge/swap hops
requiresSponsoredGasbooleanWhether gas sponsorship is required

Example

Basic Simulation:
curl -X POST https://api.bridgfy.com/api/routing/simulate-deposit \
  -H "x-api-key: bfy_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-abc-123",
    "fromChainId": 8453,
    "fromTokenSymbol": "USDC",
    "amountInAtomic": "10000000"
  }'
Response:
{
  "userId": "user-abc-123",
  "hasIntent": true,
  "fromChainId": 8453,
  "fromChainKey": "base",
  "fromTokenSymbol": "USDC",
  "amountInAtomic": "10000000",
  "targetChainId": 137,
  "targetChainKey": "polygon",
  "targetTokenSymbol": "USDC",
  "targetAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8",
  "amountOutExpectedAtomic": "9950000",
  "gasFeeRaw": "25000",
  "gasFeeSkipReason": "NONE",
  "amountForSwapAtomic": "9925000",
  "provider": "lifi",
  "policy": {
    "slippageBps": 50,
    "minOutputBps": 9800,
    "maxDurationSeconds": 600,
    "maxHops": 3,
    "requiresSponsoredGas": false
  }
}
With Overrides:
curl -X POST https://api.bridgfy.com/api/routing/simulate-deposit \
  -H "x-api-key: bfy_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-abc-123",
    "fromChainId": 8453,
    "fromTokenSymbol": "USDC",
    "amountInAtomic": "10000000",
    "overrideTargetChainId": 1,
    "overrideTargetTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
  }'
This tests routing from Base USDC to Ethereum USDC without changing the user’s stored Deposit Intent.

Error Responses

No Deposit Intent Status: 404 Not Found
{
  "statusCode": 404,
  "message": "No deposit intent found for user: user-123",
  "error": "Not Found"
}
No Route Available Status: 400 Bad Request
{
  "statusCode": 400,
  "message": "No route found for token pair",
  "error": "Bad Request",
  "details": {
    "errorCode": "FAILED_UNROUTABLE_MAIN"
  }
}
Invalid Chain ID Status: 400 Bad Request
{
  "statusCode": 400,
  "message": "Invalid chain ID: 999",
  "error": "Bad Request"
}
Token Blacklisted Status: 400 Bad Request
{
  "statusCode": 400,
  "message": "Token is blacklisted",
  "error": "Bad Request",
  "details": {
    "errorCode": "TOKEN_BLACKLISTED"
  }
}
Insufficient After Fees Status: 400 Bad Request
{
  "statusCode": 400,
  "message": "Amount insufficient to cover fees",
  "error": "Bad Request",
  "details": {
    "errorCode": "FAILED_INSUFFICIENT_AFTER_FEES",
    "amountInAtomic": "1000000",
    "protocolFeeRaw": "50000",
    "gasFeeRaw": "1000000"
  }
}

Understanding Simulation Results

Fee Breakdown

The simulation shows exactly what fees will be charged: Protocol Fee:
protocolFeeRaw = amountInAtomic * (protocolFeeBps / 10000)
Gas Fee:
  • If sponsoredGas: true: gasFeeRaw = "0", gasFeeSkipReason explains why
  • If sponsoredGas: false: gasFeeRaw is the estimated gas cost in source token
Amount for Swap:
amountForSwapAtomic = amountInAtomic - protocolFeeRaw - gasFeeRaw
Expected Output:
amountOutExpectedAtomic = amountForSwapAtomic - bridgeFees - slippage

Gas Fee Skip Reasons

When gasFeeRaw = "0", the gasFeeSkipReason field explains why:
ReasonDescription
SPONSOREDGas is sponsored by the platform (API key setting)
AA_ESTIMATION_FAILEDGas estimation failed, falling back to sponsored mode
GAS_MANAGER_REQUIREDGas Manager is required for this execution
NONEGas fee is being charged (not skipped)

Route Viability

A successful simulation means:
  1. ✅ A route exists for the token pair
  2. ✅ Fees can be covered by the deposit amount
  3. ✅ The routing provider supports this transfer
  4. ✅ Expected output meets minimum thresholds
If simulation fails, the actual deposit will also fail with the same error.

Use Cases

1. Calculate Minimum Deposit

Use simulation to find the minimum viable deposit amount:
async function findMinimumDeposit(userId, fromChainId, fromTokenSymbol) {
  // Start with a small amount
  let amount = "1000000"; // 1 USDC
  
  while (true) {
    try {
      const result = await simulateDeposit({
        userId,
        fromChainId,
        fromTokenSymbol,
        amountInAtomic: amount
      });
      
      // If successful, this is a viable amount
      return amount;
      
    } catch (error) {
      if (error.details?.errorCode === 'FAILED_INSUFFICIENT_AFTER_FEES') {
        // Double the amount and try again
        amount = (BigInt(amount) * 2n).toString();
      } else {
        throw error; // Other error, not amount-related
      }
    }
  }
}

2. Show Expected Output

Display to users how much they’ll receive:
const simulation = await simulateDeposit({
  userId: user.id,
  fromChainId: 8453,
  fromTokenSymbol: 'USDC',
  amountInAtomic: depositAmount
});

// Convert atomic to human-readable (USDC has 6 decimals)
const outputAmount = Number(simulation.amountOutExpectedAtomic) / 1e6;

showMessage(`You will receive approximately ${outputAmount} USDC on Polygon`);

3. Test Different Routes

Try different destination chains to find the best rate:
const chains = [1, 137, 42161]; // Ethereum, Polygon, Arbitrum
const simulations = await Promise.all(
  chains.map(chainId => 
    simulateDeposit({
      userId: user.id,
      fromChainId: 8453,
      fromTokenSymbol: 'USDC',
      amountInAtomic: '10000000',
      overrideTargetChainId: chainId
    }).catch(err => null) // Ignore failures
  )
);

// Find best output
const best = simulations
  .filter(s => s !== null)
  .sort((a, b) => 
    BigInt(b.amountOutExpectedAtomic) - BigInt(a.amountOutExpectedAtomic)
  )[0];

console.log(`Best route: ${best.targetChainKey} - ${best.amountOutExpectedAtomic} output`);

4. Verify Route Availability

Before creating a Deposit Intent, verify the route exists:
async function canRoute(userId, fromChainId, fromToken, toChainId, toToken) {
  try {
    await simulateDeposit({
      userId,
      fromChainId,
      fromTokenSymbol: fromToken,
      amountInAtomic: "1000000", // Small test amount
      overrideTargetChainId: toChainId,
      overrideTargetTokenAddress: toToken
    });
    return true;
  } catch (error) {
    if (error.details?.errorCode === 'FAILED_UNROUTABLE_MAIN') {
      return false;
    }
    // Other errors - re-throw
    throw error;
  }
}

Best Practices

Always Simulate First

Before showing a deposit address to users:
  1. Simulate the deposit with a typical amount
  2. Verify route exists (simulation succeeds)
  3. Calculate minimums based on fee structure
  4. Show expected output to users

Handle All Error Cases

try {
  const sim = await simulateDeposit({...});
  showDepositUI(sim);
} catch (error) {
  switch (error.details?.errorCode) {
    case 'FAILED_UNROUTABLE_MAIN':
      showError('This token pair is not supported');
      break;
    case 'FAILED_INSUFFICIENT_AFTER_FEES':
      showError(`Minimum deposit: ${calculateMinimum(error.details)}`);
      break;
    case 'TOKEN_BLACKLISTED':
      showError('This token cannot be processed');
      break;
    default:
      showError('Unable to calculate route. Please try again.');
  }
}

Use Overrides for Testing

Test different scenarios without affecting user’s intent:
// Test if routing to Ethereum would be better
const ethSimulation = await simulateDeposit({
  userId: user.id,
  fromChainId: 8453,
  fromTokenSymbol: 'USDC',
  amountInAtomic: depositAmount,
  overrideTargetChainId: 1 // Ethereum
});

// Compare with user's actual intent
const actualSimulation = await simulateDeposit({
  userId: user.id,
  fromChainId: 8453,
  fromTokenSymbol: 'USDC',
  amountInAtomic: depositAmount
  // No override - uses their stored intent
});

Cache Simulation Results

Simulation results are valid for a short time (typically 1-2 minutes):
const cache = new Map();

async function getCachedSimulation(params) {
  const key = JSON.stringify(params);
  const cached = cache.get(key);
  
  if (cached && Date.now() - cached.timestamp < 60000) { // 1 minute
    return cached.result;
  }
  
  const result = await simulateDeposit(params);
  cache.set(key, { result, timestamp: Date.now() });
  return result;
}
Note: Don’t cache for too long - gas prices and bridge liquidity change frequently.

Next Steps