Swap tokens on Raydium DEX without paying gas fees - LazorKit handles everything
This advanced recipe demonstrates how to integrate an existing DeFi protocol (Raydium) with LazorKit's gasless transaction infrastructure on mobile. Users can swap tokens without needing SOL for gas fees, providing a seamless mobile trading experience.
Environment: Expo 54 + React Native. See
_layout.tsxfor required polyfills.
- Integrate Raydium Trade API for swap quotes and transactions
- Handle legacy transactions from external protocols
- Work around LazorKit validation requirements
- Manage token accounts and balances for DEX trading
- Build a gasless mobile swap interface
- Use the
processInstructionsForLazorKitutility
┌─────────────────────────────────────────────────────────────────────┐
│ MOBILE GASLESS SWAP FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ Mobile UI │───▶│ Raydium API │───▶│ Get Quote + Build Tx │ │
│ │ (Expo) │ │ (Trade API) │ │ (Legacy Transaction) │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────────────────────────────────┐ │
│ └──────────▶│ processInstructionsForLazorKit() │ │
│ │ 1. Deserialize legacy tx │ │
│ │ 2. Remove ComputeBudget instructions │ │
│ │ 3. Add smart wallet to all instructions │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ LazorKit Mobile SDK + Paymaster │ │
│ │ - Opens portal for passkey signature │ │
│ │ - Pays all gas fees │ │
│ │ - Submits to Solana network │ │
│ └──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
- Completed Recipe 01 and Recipe 02
- Understanding of DEX mechanics and token swaps
- Devnet SOL and USDC for testing
- Required packages:
npm install axios @raydium-io/raydium-sdk-v2import { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { PublicKey, Transaction } from '@solana/web3.js';
import axios from 'axios';
import { DEV_API_URLS } from '@raydium-io/raydium-sdk-v2';
import { useLazorkitWalletConnect } from '@/hooks/useLazorkitWalletConnect';
import { useBalances } from '@/hooks/useBalances';
import { getConnection, getAssociatedTokenAddressSync } from '@/lib/solana-utils';
import { processInstructionsForLazorKit } from '@/lib/lazorkit-utils';const TOKENS = {
SOL: {
symbol: 'SOL',
name: 'Solana',
mint: 'So11111111111111111111111111111111111111112',
decimals: 9,
},
USDC: {
symbol: 'USDC',
name: 'USD Coin',
mint: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // Devnet
decimals: 6,
},
};export default function RaydiumSwapScreen() {
const { wallet, isConnected, connect, signAndSendTransaction, connecting } = useLazorkitWalletConnect();
const [inputToken, setInputToken] = useState<'SOL' | 'USDC'>('SOL');
const [outputToken, setOutputToken] = useState<'SOL' | 'USDC'>('USDC');
const [inputAmount, setInputAmount] = useState('');
const [outputAmount, setOutputAmount] = useState('');
const [quoteError, setQuoteError] = useState('');
const [swapping, setSwapping] = useState(false);
const [lastTxSignature, setLastTxSignature] = useState('');
const { solBalance, usdcBalance, fetchBalances } = useBalances(
isConnected ? wallet?.smartWallet : null
);
const balances = {
SOL: solBalance ?? 0,
USDC: usdcBalance ?? 0,
};
}Use Raydium's Trade API with debounced input:
const calculateOutputAmount = async () => {
if (!wallet || !inputAmount || parseFloat(inputAmount) <= 0) {
setOutputAmount('');
setQuoteError('');
return;
}
setQuoteError('');
try {
const inputMint = TOKENS[inputToken].mint;
const outputMint = TOKENS[outputToken].mint;
const amount = parseFloat(inputAmount) * Math.pow(10, TOKENS[inputToken].decimals);
const quoteResponse = await fetch(
`${DEV_API_URLS.SWAP_HOST}/compute/swap-base-in?` +
`inputMint=${inputMint}&` +
`outputMint=${outputMint}&` +
`amount=${Math.floor(amount)}&` +
`slippageBps=50&` +
`txVersion=LEGACY`
);
if (!quoteResponse.ok) {
throw new Error('Failed to get quote from Raydium API');
}
const quoteData = await quoteResponse.json();
if (!quoteData.success || !quoteData.data) {
throw new Error('No liquidity available for this pair');
}
const outputAmountRaw = parseFloat(quoteData.data.outputAmount);
const formattedOutput = (outputAmountRaw / Math.pow(10, TOKENS[outputToken].decimals)).toFixed(6);
setOutputAmount(formattedOutput);
} catch (err: any) {
setQuoteError(err.message || 'Failed to get quote');
setOutputAmount('');
}
};
// Debounce the quote calculation
useEffect(() => {
const timeoutId = setTimeout(calculateOutputAmount, 500);
return () => clearTimeout(timeoutId);
}, [inputAmount, inputToken, outputToken]);The key is using processInstructionsForLazorKit to adapt Raydium's transaction:
const handleSwap = async () => {
if (!wallet || !inputAmount) {
Alert.alert('Error', 'Please enter an amount');
return;
}
const amount = parseFloat(inputAmount);
if (amount > (balances[inputToken] || 0)) {
Alert.alert('Error', `Insufficient ${inputToken} balance`);
return;
}
setSwapping(true);
try {
const inputMint = TOKENS[inputToken].mint;
const outputMint = TOKENS[outputToken].mint;
const amountIn = Math.floor(amount * Math.pow(10, TOKENS[inputToken].decimals));
// 1. Get quote
const { data: swapResponse } = await axios.get(
`${DEV_API_URLS.SWAP_HOST}/compute/swap-base-in?` +
`inputMint=${inputMint}&outputMint=${outputMint}&` +
`amount=${amountIn}&slippageBps=50&txVersion=LEGACY`
);
// 2. Get priority fee
const { data: priorityFeeData } = await axios.get(
`${DEV_API_URLS.BASE_HOST}${DEV_API_URLS.PRIORITY_FEE}`
);
// 3. Build transaction from Raydium (request LEGACY format)
const { data: swapData } = await axios.post(
`${DEV_API_URLS.SWAP_HOST}/transaction/swap-base-in`,
{
computeUnitPriceMicroLamports: String(priorityFeeData.data.default.h),
swapResponse,
txVersion: 'LEGACY', // Important: Request legacy format
wallet: wallet.smartWallet,
wrapSol: inputMint === TOKENS.SOL.mint,
unwrapSol: outputMint === TOKENS.SOL.mint,
inputAccount: inputMint === TOKENS.SOL.mint
? undefined
: getAssociatedTokenAddressSync(new PublicKey(inputMint), new PublicKey(wallet.smartWallet)).toBase58(),
outputAccount: outputMint === TOKENS.SOL.mint
? undefined
: getAssociatedTokenAddressSync(new PublicKey(outputMint), new PublicKey(wallet.smartWallet)).toBase58(),
}
);
// 4. Deserialize as Legacy Transaction
const txBuffer = Buffer.from(swapData.data[0].transaction, 'base64');
const legacyTx = Transaction.from(txBuffer);
// 5. Process instructions for LazorKit (handles all the complexity)
const instructions = processInstructionsForLazorKit(
legacyTx.instructions,
wallet.smartWallet
);
// 6. Send gasless transaction
const signature = await signAndSendTransaction({
instructions,
transactionOptions: { computeUnitLimit: 600_000 }
});
setLastTxSignature(signature);
Alert.alert(
'Swap Successful!',
`Swapped ${amount} ${inputToken} for ~${outputAmount} ${outputToken}\n\nNo gas fees paid!`
);
setInputAmount('');
setOutputAmount('');
await fetchBalances();
} catch (err: any) {
console.error('Swap error:', err);
Alert.alert('Swap Failed', getSwapErrorMessage(err));
} finally {
setSwapping(false);
}
};This utility (in lib/lazorkit-utils.ts) handles the complexity of adapting external protocol transactions:
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
const COMPUTE_BUDGET_PROGRAM = new PublicKey('ComputeBudget111111111111111111111111111111');
export function processInstructionsForLazorKit(
instructions: TransactionInstruction[],
smartWalletAddress: string
): TransactionInstruction[] {
const smartWallet = new PublicKey(smartWalletAddress);
return instructions
// 1. Remove ComputeBudget instructions (LazorKit handles compute)
.filter(ix => !ix.programId.equals(COMPUTE_BUDGET_PROGRAM))
// 2. Add smart wallet to instructions that don't have it
.map(ix => {
const hasSmartWallet = ix.keys.some(k => k.pubkey.equals(smartWallet));
if (!hasSmartWallet) {
ix.keys.push({
pubkey: smartWallet,
isSigner: false,
isWritable: false
});
}
return ix;
});
}Create a swap interface with token selectors:
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.content}>
{!isConnected ? (
<TouchableOpacity onPress={() => connect('examples/03-raydium-swap')}>
<Text>Connect Wallet</Text>
</TouchableOpacity>
) : (
<View>
{/* Balances */}
<View style={styles.balanceCard}>
<Text>SOL: {balances.SOL.toFixed(4)}</Text>
<Text>USDC: {balances.USDC.toFixed(2)}</Text>
</View>
{/* Input Token */}
<View style={styles.tokenSection}>
<Text>You Pay</Text>
<TextInput
value={inputAmount}
onChangeText={setInputAmount}
placeholder="0.0"
keyboardType="decimal-pad"
/>
<View style={styles.tokenSelector}>
<TouchableOpacity
onPress={() => setInputToken('SOL')}
style={[styles.tokenBtn, inputToken === 'SOL' && styles.active]}
>
<Text>SOL</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setInputToken('USDC')}
style={[styles.tokenBtn, inputToken === 'USDC' && styles.active]}
>
<Text>USDC</Text>
</TouchableOpacity>
</View>
</View>
{/* Flip Button */}
<TouchableOpacity onPress={handleFlipTokens} style={styles.flipBtn}>
<Text>⇅</Text>
</TouchableOpacity>
{/* Output Token */}
<View style={styles.tokenSection}>
<Text>You Receive</Text>
<Text style={styles.outputValue}>{outputAmount || '0.0'}</Text>
<Text style={styles.outputToken}>{outputToken}</Text>
</View>
{/* Error */}
{quoteError && (
<View style={styles.errorCard}>
<Text style={styles.errorText}>{quoteError}</Text>
</View>
)}
{/* Swap Button */}
<TouchableOpacity
onPress={handleSwap}
disabled={swapping || !inputAmount || !!quoteError}
style={styles.swapButton}
>
<Text>{swapping ? 'Swapping...' : 'Swap (Gas-Free)'}</Text>
</TouchableOpacity>
<Text style={styles.poweredBy}>No gas fees - Powered by LazorKit</Text>
</View>
)}
</ScrollView>
</KeyboardAvoidingView>
);When integrating external protocols like Raydium with LazorKit:
Problem: Versioned transactions (V0) may not work with LazorKit.
Solution: Request legacy transactions: txVersion: 'LEGACY'
Problem: LazorKit manages compute budget automatically.
Solution: Filter out ComputeBudget instructions using processInstructionsForLazorKit.
Problem: LazorKit's execute_cpi expects the smart wallet in ALL instruction key lists.
Solution: Add the smart wallet to any instruction that doesn't include it.
The complete implementation uses centralized hooks and the shared utility function.
Custom Hooks Used:
| Hook | Description |
|---|---|
useLazorkitWalletConnect() |
Mobile wallet connection with deep link flow |
useBalances() |
Automatic SOL/USDC balance management |
Utility Functions:
| Function | Description |
|---|---|
processInstructionsForLazorKit() |
Adapts external protocol transactions for LazorKit |
getAssociatedTokenAddressSync() |
Derives token account addresses |
getConnection() |
Returns a cached Solana connection instance |
Key Pattern - Protocol Integration:
// 1. Get transaction from external protocol (Raydium)
const { data: swapData } = await axios.post(RAYDIUM_API, {
txVersion: 'LEGACY', // Request legacy format
wallet: wallet.smartWallet,
// ... other params
});
// 2. Deserialize the transaction
const legacyTx = Transaction.from(Buffer.from(swapData.transaction, 'base64'));
// 3. Process for LazorKit (removes ComputeBudget, adds smart wallet)
const instructions = processInstructionsForLazorKit(
legacyTx.instructions,
wallet.smartWallet
);
// 4. Send gasless
const signature = await signAndSendTransaction({
instructions,
transactionOptions: { computeUnitLimit: 600_000 }
});Source: See the full implementation at
index.tsx
This recipe currently supports the SOL/USDC pair on Devnet. Important notes:
- Devnet liquidity pools can be unreliable
- Some swap directions may fail due to imbalanced pools
- If USDC→SOL fails, try SOL→USDC instead
- The same code works reliably on Mainnet where pools are properly maintained
| Issue | Solution |
|---|---|
| "No liquidity" | Devnet pools may have limited liquidity - try smaller amounts |
| "Transaction too large" | Complex routes may exceed limits - try a direct pair |
| "Slippage exceeded" | Increase slippage tolerance or try again |
| "Pool error" | Try swapping in the opposite direction |
| "Insufficient balance" | Ensure you have enough of the input token |
| Deep link not returning | Verify app scheme configuration |
- Apply this pattern to integrate other DeFi protocols (Jupiter, Orca, etc.)
- Build your own gasless mobile DeFi application
- Check the web examples for more advanced recipes