Skip to main content

Overview

DEX (Decentralized Exchange) swaps execute on-chain through smart contracts. Unlike CEX swaps that use deposit addresses, DEX swaps require users to connect their wallet, sign transactions, and broadcast them directly to the blockchain.
Best For: Users who want true decentralized swaps, keep custody of their funds, and interact directly with on-chain liquidity sources like Uniswap, Cowswap, and 1inch.

Supported Networks

DEX swaps are currently supported on the following networks:
  • EVM (Ethereum, BSC, Polygon, etc.)
  • Solana
  • SUI
  • TRON
  • TON
  • Stellar
Stellar Trustline Requirement: For swaps involving Stellar assets, the integrator is responsible for ensuring that the destination account has the required trustlines established before initiating the swap. Houdini does not handle trustline creation automatically. If the trustline is missing, the swap will fail.

Key Characteristics

FeatureDEXCEX (Private/Standard)
ExecutionOn-chain via smart contractsOff-chain via exchanges
WalletRequired (MetaMask, etc.)Not required
CustodyUser keeps custodyUser sends to deposit address
SpeedVaries by network (seconds to minutes)3-45 minutes
ApprovalsMay require token approvalsNot required

How It Works

DEX swaps follow this flow:

Technical Flow Diagram

This diagram shows the complete integration flow with conditional logic:

Integration Steps

Step 1: Get Supported Assets

Discover which tokens and networks are available for DEX swaps. Learn more about DEX tokens and network identifiers.
Performance Note: The /tokens endpoint can take several seconds to minute to respond due to the large token list.
  • Save tokens in your backend database
  • Load tokens on server startup or via scheduled job
  • Serve token list from your database to frontend
  • Refresh cache periodically (e.g., every 24 hours)
const tokens = await fetchFromHoudini('/dexTokens');

// Get supported networks
const networks = await fetchFromHoudini('/networks');

Step 2: Get DEX Quote

Request a quote for the DEX swap using token IDs from /dexTokens:
const quotes = await fetchFromHoudini('/dexQuote', {
  tokenIdFrom: '6689b73ec90e45f3b3e51566',  // ETH token _id from /dexTokens
  tokenIdTo: '6689b73ec90e45f3b3e51553',    // USDT token _id from /dexTokens
  amount: 1,
});

// Select the best quote (first in array)
const selectedQuote = quotes[0];
Quote Parameters:
  • tokenIdFrom: Source token _id from /dexTokens endpoint
  • tokenIdTo: Destination token _id from /dexTokens endpoint
  • amount: Amount to swap (float: 1 = 1 token)
  • slippage (optional): Slippage percentage (e.g., 0.5 for 0.5%)
  • fromAddress (optional): Specific source address for the swap
  • toAddress (optional): Specific destination address for receiving tokens
Remember to use the _id field from DEX tokens, not the id field. Learn more about token identifiers.

Quote Response

The response is an array of quote options from different DEX aggregators, sorted by best rate (highest output first). Each quote object contains:
FieldTypeDescription
swapstringDEX identifier code used in subsequent API calls
swapNamestringHuman-readable DEX name (e.g., “CowSwap”, “Uniswap”)
quoteIdstringUnique quote ID - pass this to /dexExchange
amountOutnumberExpected output amount (human-readable)
amountOutUsdnumberUSD value of output amount
netAmountOutnumberNet output after fees
feeUsdnumberFee amount in USD
durationnumberEstimated swap duration in minutes
typestringAlways "dex" for DEX swaps
logoUrlstringDEX logo image URL for UI display
rawobjectFull route data - pass this as route to subsequent endpoints
supportsSignaturesbooleanWhether this DEX supports gasless signatures
markupSupportedbooleanWhether partner markup is supported
rewardsAvailablebooleanWhether rewards are available for this route
filteredbooleanIf true and sender/receiver addresses differ on same-chain swaps, this route is not supported and should be disabled in UI
Filtering Routes: When filtered: true, the route does not support different sender and receiver addresses for same-chain swaps. You must either:
  • Filter out these routes from the UI when addressFrom !== addressTo
  • Disable the route option and show a message explaining it’s not available for this configuration

Step 3: Check Approvals and Signatures

Before executing a swap, check what’s needed from the user:
const approvalCheck = await fetch(`${API_BASE_URL}/dexApprove`, {
  method: 'POST',
  headers: {
    'Authorization': `${API_KEY}:${API_SECRET}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tokenIdFrom: '6689b73ec90e45f3b3e51566',
    tokenIdTo: '6689b73ec90e45f3b3e51553',
    amount: 1,
    addressFrom: '0x45CF73349a4895fabA18c0f51f06D79f0794898D', // user's address
    swap: selectedQuote.swap, // From step 2
    route: selectedQuote.raw  // From step 2
  })
});

const { approvals, signatures } = await approvalCheck.json();
Response Contains:
  • approvals: Array of on-chain approval transactions (may be empty)
  • signatures: Array of signatures needed (may be empty)
Both Arrays Can Exist: You may receive both approvals AND signatures. Handle approvals first, then signatures.

Understanding Approvals

If the approvals array is not empty, the user must approve the DEX to spend their tokens. See more in Step 4.

Understanding Signatures

The signatures array can contain two types: 1. SINGLE Type - Simple one-time signature: Action: User signs once, add to results, done. 2. CHAINED Type - Multi-step signature (currently only for Cowswap): Action: User signs, call /chainSignatures, repeat until isComplete: true. See more in Step 5.

Step 4: Send Approval Transactions (if needed)

If the approvals array is not empty, broadcast approval transactions:
// Send each approval transaction
if (approvals && approvals.length > 0) {
  for (const approval of approvals) {
    // Send approval transaction through user's wallet
    const approvalTx = await walletProvider.sendTransaction({
      to: approval.to,      // Token contract address
      data: approval.data   // Encoded approve() call
    });

    // Wait for transaction to be mined
    await approvalTx.wait();
    console.log('Approval confirmed:', approvalTx.hash);
  }
}
Skip This Step If: No approvals were required (empty approvals array in Step 3).

Step 5: Process Signatures (if needed)

Handle signature requests from Step 3:
// Helper function to process all signatures
async function processSignatures(signatures) {
  if (!signatures || signatures.length === 0) return [];

  const results = [];

  for (const sig of signatures) {
    // Prompt user to sign
    const signatureResult = await walletProvider.signTypedData({
      domain: sig.data.domain,
      types: sig.data.types,
      primaryType: sig.data.primaryType,
      message: sig.data.message
    });

    const signatureObject = {
      signature: signatureResult,
      key: sig.key,
      swapRequiredMetadata: sig.swapRequiredMetadata
    };

    // If CHAINED and not complete, get next signature
    if (sig.type === 'CHAINED' && !sig.isComplete) {
      const nextSigResponse = await fetch(`${API_BASE_URL}/chainSignatures`, {
        method: 'POST',
        headers: {
          'Authorization': `${API_KEY}:${API_SECRET}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          tokenIdFrom,
          tokenIdTo,
          addressFrom: userWalletAddress,
          route: quote.route,
          swap: quote.swap,
          previousSignature: signatureObject,
          signatureKey: sig.key,
          signatureStep: sig.step
        })
      });

      const { chainSignatures } = await nextSigResponse.json();

      // Recursively process next signature
      if (chainSignatures?.length > 0) {
        const chainResults = await processSignatures(chainSignatures);
        if (chainResults.length > 0) {
          results.push(chainResults[chainResults.length - 1]); // Only add final
        }
      }
    } else {
      // SINGLE type or final CHAINED signature
      results.push(signatureObject);
    }
  }

  return results;
}

// Usage
const collectedSignatures = await processSignatures(signatures);
Key Points:
  • SINGLE: User signs once, done
  • CHAINED: User signs → API call → User signs again → Repeat until complete
  • Only keep the final signature from CHAINED sequences
Skip This Step If: No signatures were required (empty signatures array in Step 3).

Step 6: Check Allowance (if approvals were sent)

If you sent approval transactions in Step 4, verify they are confirmed on-chain:
// Poll until approval is confirmed on-chain
if (approvals && approvals.length > 0) {
  let hasAllowance = false;
  while (!hasAllowance) {
    const allowanceCheck = await fetchFromHoudini('/dexHasEnoughAllowance', {
      tokenIdFrom: '6689b73ec90e45f3b3e51566',
      tokenIdTo: '6689b73ec90e45f3b3e51553',
      amount: 1,
      addressFrom: "0x45CF73349a4895fabA18c0f51f06D79f0794898D",
      swap: quote.swap,
      route: quote.raw
    });

    hasAllowance = allowanceCheck.hasEnoughAllowance;

    if (!hasAllowance) {
      await sleep(30000); // Wait 30 seconds before checking again
    }
  }
  console.log('Allowance confirmed!');
}
Skip This Step If: No approvals were sent in Step 4.

Step 7: Execute the Swap

Now execute the swap with any collected signatures:
const swapResponse = await fetch(`${API_BASE_URL}/dexExchange`, {
  method: 'POST',
  headers: {
    'Authorization': `${API_KEY}:${API_SECRET}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tokenIdFrom: '6689b73ec90e45f3b3e51566',
    tokenIdTo: '6689b73ec90e45f3b3e51553',
    amount: 26.2244,
    addressFrom: userWalletAddress,  // User's wallet address
    addressTo: destination address,    // Destination address to receive fund
    route: quote.raw,                // Route object from quote response
    swap: quote.swap,                // DEX identifier (e.g., "zx", "cs", "un")
    quoteId: quote.quoteId,          // Quote ID from quote response
    signatures: collectedSignatures, // From Step 5 (can be empty array)
    destinationTag: '',              // For chains that require memo/tag
    deviceInfo: 'web',               // Device type: 'web', 'ios', 'android'
    isMobile: false,                 // Boolean indicating mobile device
    walletInfo: 'MetaMask',          // Wallet name being used (e.g., "MetaMask", "Rabby Wallet")
    slippage: null                   // Custom slippage percentage (null = use default) 0.5 = 0.5%
  })
});

const { order } = await swapResponse.json();
console.log('Swap created:', order.houdiniId);
console.log('Off-chain?', order.metadata.offChain);
Request Parameters:
  • tokenIdFrom: Source token ID
  • tokenIdTo: Destination token ID
  • amount: Amount to swap (float)
  • addressFrom: User’s wallet address (source)
  • addressTo: Destination address for receiving tokens
  • route: Complete route object from quote response (quote.raw)
  • swap: DEX identifier from quote (e.g., “zx” for 0x, “cs” for Cowswap)
  • quoteId: Quote ID from quote response
  • signatures: Array of signature objects from Step 5 (empty if none required)
  • destinationTag: Memo/tag for chains that require it (empty string if not needed)
  • deviceInfo: Device type - “web”, “ios”, “android”, or custom identifier
  • isMobile: Boolean indicating if request is from mobile device
  • walletInfo: Name of wallet being used (e.g., “MetaMask”, “Rabby Wallet”)
  • slippage: Custom slippage tolerance (null to use default from quote)
Response Contains:
  • order.houdiniId: Unique swap identifier for tracking
  • order.metadata.offChain: Boolean indicating if user transaction is needed
  • order.metadata.to: DEX router address (if offChain: false)
  • order.metadata.data: Encoded swap call (if offChain: false)
  • order.metadata.value: ETH value for native swaps (if offChain: false)

Broadcast Transaction and Confirm

After receiving the swap order, you need to:
  1. Have the user broadcast the transaction (if offChain: false)
  2. Call /dexConfirmTx to notify Houdini of the transaction hash
User must broadcast the transaction, then confirm with Houdini:
if (!order.metadata.offChain) {
  // Step 1: User sends transaction via wallet
  const tx = await walletProvider.sendTransaction({
    to: order.metadata.to,
    data: order.metadata.data,
    value: order.metadata.value || '0'
  });

  const receipt = await tx.wait();
  const txHash = receipt.hash;

  console.log('Transaction broadcast:', txHash);

  // Step 2: Confirm with Houdini API
  await fetch(`${API_BASE_URL}/dexConfirmTx`, {
    method: 'POST',
    headers: {
      'Authorization': `${API_KEY}:${API_SECRET}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      houdiniId: order.houdiniId,
      txHash: txHash
    })
  });

  console.log('Transaction confirmed with Houdini');
}
Critical Step: You MUST call /dexConfirmTx after creating the swap order:
  • On-chain swaps: Pass the transaction hash after user broadcasts
  • Off-chain swaps: Pass txHash: undefined to start backend processing
Without this call, the swap will not be processed and status tracking will not work.

Step 8: Track Swap Status

Monitor the swap progress:
const status = await fetchFromHoudini('/status', {
  id: order.houdiniId
});

console.log('Status:', status.status);
console.log('Transaction hash:', status.txHash);
Status Codes:
  • 0 = WAITING - Awaiting transaction
  • 1 = CONFIRMING - Transaction submitted, waiting for confirmations
  • 2 = EXCHANGING - Processing swap
  • 4 = COMPLETED - Swap complete ✅
  • 6 = FAILED - Swap failed ❌
Polling: Check status every 30 seconds for on-chain swaps. They typically complete in seconds to a few minutes depending on network congestion.

Complete Example

For a complete, runnable Node.js example:

DEX Swap Example

Complete DEX swap script with approval handling and wallet simulation

Next Steps