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
Feature DEX CEX (Private/Standard) Execution On-chain via smart contracts Off-chain via exchanges Wallet Required (MetaMask, etc.) Not required Custody User keeps custody User sends to deposit address Speed Varies by network (seconds to minutes) 3-45 minutes Approvals May require token approvals Not 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:
Field Type Description swapstring DEX identifier code used in subsequent API calls swapNamestring Human-readable DEX name (e.g., “CowSwap”, “Uniswap”) quoteIdstring Unique quote ID - pass this to /dexExchange amountOutnumber Expected output amount (human-readable) amountOutUsdnumber USD value of output amount netAmountOutnumber Net output after fees feeUsdnumber Fee amount in USD durationnumber Estimated swap duration in minutes typestring Always "dex" for DEX swaps logoUrlstring DEX logo image URL for UI display rawobject Full route data - pass this as route to subsequent endpoints supportsSignaturesboolean Whether this DEX supports gasless signatures markupSupportedboolean Whether partner markup is supported rewardsAvailableboolean Whether rewards are available for this route filteredboolean If 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:
Have the user broadcast the transaction (if offChain: false)
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' );
}
Backend handles execution (e.g., Cowswap), but you still need to confirm: if ( order . metadata . offChain ) {
// No user transaction needed - Houdini backend will execute the swap
// But still need to call dexConfirmTx to start processing
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: undefined // No transaction hash for off-chain swaps
})
});
console . log ( 'Off-chain swap 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