A Node.js application for swapping ADA and USDM tokens on the SundaeSwap DEX using the SundaeSwap SDK. This project provides both a programmatic API and a server endpoint for executing token swaps on Cardano.
This application enables automated token swaps on the SundaeSwap protocol, specifically for ADA/USDM pairs. It uses the SundaeSwap SDK with Lucid Cardano for transaction building and signing.
- Token Swapping: Swap ADA to USDM and USDM to ADA
- REST API: Express server with
/api/swapendpoint - SundaeSwap V3 Integration: Uses latest V3 contracts
- Slippage Protection: Configurable slippage tolerance (default: 3%)
- Mainnet Support: Configured for Cardano mainnet
- Node.js 18 or higher
- Cardano wallet with mnemonic seed phrase
- Blockfrost API key (mainnet)
- Sufficient ADA balance for transaction fees and swaps
# Clone the repository
git clone <repository-url>
cd ada-swap
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your Blockfrost API key and wallet mnemonicCreate a .env file in the root directory:
# Blockfrost API Key (required)
BLOCKFROST_API_KEY=your_blockfrost_api_key_here
# Wallet Mnemonic (required - 12 or 24 words)
WALLET_MNEMONIC="your twelve word seed phrase here"
# Server Port (optional, defaults to 3002)
PORT=3002- Go to Blockfrost.io
- Sign up for a free account
- Create a new project
- Select Mainnet network
- Copy your API key
Security Note: Never commit your .env file or share your mnemonic seed phrase.
npm startThe server will start on http://localhost:3002 (or the port specified in PORT).
This project can be integrated into your own Node.js backend application in several ways:
Install the project as a dependency or copy the swap functionality into your project:
# If publishing as npm package
npm install ada-swap
# Or copy the files directly into your projectThen import and use the swap function:
import { swapTokens } from "./adaUsdmSwap.js";
// or
const { swapTokens } = require("./adaUsdmSwap.js");
// In your route handler or service
async function handleSwap(req, res) {
try {
const { mnemonic, amount, isFromAda, fromToken, toToken, poolId } =
req.body;
const result = await swapTokens(
mnemonic,
amount,
isFromAda,
fromToken,
toToken,
poolId
);
res.json({
success: true,
txHash: result.txHash,
walletAddress: result.walletAddress,
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
}Create a middleware function that wraps the swap functionality:
// swapMiddleware.js
import { swapTokens } from "./adaUsdmSwap.js";
export const swapMiddleware = async (req, res, next) => {
try {
const { mnemonic, amount, isFromAda, fromToken, toToken, poolId } =
req.body;
// Validate required fields
if (
!mnemonic ||
!amount ||
typeof isFromAda !== "boolean" ||
!fromToken ||
!toToken ||
!poolId
) {
return res.status(400).json({
error: "Missing required parameters",
});
}
// Execute swap
const result = await swapTokens(
mnemonic,
amount,
isFromAda,
fromToken,
toToken,
poolId
);
// Attach result to request object
req.swapResult = result;
next();
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
// In your routes
import express from "express";
import { swapMiddleware } from "./swapMiddleware.js";
const router = express.Router();
router.post("/swap", swapMiddleware, (req, res) => {
res.json({
success: true,
data: req.swapResult,
});
});Wrap the swap functionality in a service class for better organization:
// SwapService.js
import { swapTokens } from "./adaUsdmSwap.js";
export class SwapService {
constructor(config = {}) {
this.defaultPoolId =
config.defaultPoolId ||
"64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef";
this.defaultSlippage = config.defaultSlippage || 0.03;
}
async swapAdaToToken(mnemonic, amount, token, poolId = this.defaultPoolId) {
return await swapTokens(
mnemonic,
amount,
true, // isFromAda
token,
token,
poolId
);
}
async swapTokenToAda(mnemonic, amount, token, poolId = this.defaultPoolId) {
return await swapTokens(
mnemonic,
amount,
false, // isFromAda
token,
token,
poolId
);
}
async swapCustom(mnemonic, amount, isFromAda, fromToken, toToken, poolId) {
return await swapTokens(
mnemonic,
amount,
isFromAda,
fromToken,
toToken,
poolId
);
}
}
// Usage
import { SwapService } from "./SwapService.js";
const swapService = new SwapService({
defaultPoolId: "64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef",
});
// In your controller
const result = await swapService.swapAdaToToken(
mnemonic,
10, // 10 ADA
{
policyId: "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad",
assetName: "5553444d",
name: "USDM",
}
);Create a configuration module for different environments:
// config.js
export const swapConfig = {
mainnet: {
blockfrostUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
network: "Mainnet",
defaultPoolId: "64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef",
slippage: 0.03,
},
preview: {
blockfrostUrl: "https://cardano-preview.blockfrost.io/api/v0",
network: "Preview",
defaultPoolId: "your-preview-pool-id",
slippage: 0.05, // Higher slippage for testnet
},
};
// Get config based on environment
export function getSwapConfig() {
const env = process.env.NODE_ENV || "mainnet";
return swapConfig[env] || swapConfig.mainnet;
}Here's a complete example of integrating the swap functionality into an Express backend:
// app.js
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { swapTokens } from "./adaUsdmSwap.js";
const app = express();
app.use(cors());
app.use(bodyParser.json());
// Swap endpoint
app.post("/api/swap", async (req, res) => {
try {
const { mnemonic, amount, isFromAda, fromToken, toToken, poolId } =
req.body;
// Validation
if (!mnemonic || !amount || typeof isFromAda !== "boolean") {
return res.status(400).json({
error: "Missing required parameters",
});
}
// Execute swap
const result = await swapTokens(
mnemonic,
amount,
isFromAda,
fromToken || {
policyId: "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad",
assetName: "5553444d",
name: "USDM",
},
toToken || {
policyId: "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad",
assetName: "5553444d",
name: "USDM",
},
poolId || "64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef"
);
res.json({
success: true,
data: result,
});
} catch (error) {
console.error("Swap error:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});When integrating into your project, ensure you have these dependencies:
{
"dependencies": {
"@sundaeswap/core": "^1.3.47",
"lucid-cardano": "0.10.7",
"@blockfrost/blockfrost-js": "^6.0.0",
"dotenv": "^16.4.7"
}
}Implement proper error handling in your integration:
import { swapTokens } from "./adaUsdmSwap.js";
async function safeSwap(params) {
try {
const result = await swapTokens(
params.mnemonic,
params.amount,
params.isFromAda,
params.fromToken,
params.toToken,
params.poolId
);
return { success: true, data: result };
} catch (error) {
// Log error for monitoring
console.error("Swap failed:", {
error: error.message,
stack: error.stack,
params: { ...params, mnemonic: "***" }, // Don't log mnemonic
});
// Return user-friendly error
return {
success: false,
error: error.message || "Swap failed",
code: error.code || "UNKNOWN_ERROR",
};
}
}When integrating into your backend:
- Never log mnemonics: Always redact sensitive data in logs
- Validate inputs: Always validate and sanitize user inputs
- Rate limiting: Implement rate limiting to prevent abuse
- Environment variables: Store sensitive data in environment variables
- Transaction monitoring: Log transaction hashes for audit trails
// Example with rate limiting
import rateLimit from "express-rate-limit";
const swapLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Limit each IP to 10 swap requests per windowMs
});
app.post("/api/swap", swapLimiter, async (req, res) => {
// Swap logic here
});Execute a token swap on SundaeSwap.
Request Body:
{
"mnemonic": "your twelve word seed phrase here",
"amount": 1,
"isFromAda": true,
"fromToken": {
"policyId": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad",
"assetName": "5553444d",
"name": "USDM"
},
"toToken": {
"policyId": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad",
"assetName": "5553444d",
"name": "USDM"
},
"poolId": "64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef"
}Parameters:
mnemonic(string, required): Wallet mnemonic seed phraseamount(number, required): Amount to swap (in ADA or token units)isFromAda(boolean, required):truefor ADA → Token,falsefor Token → ADAfromToken(object, required): Source token informationtoToken(object, required): Destination token informationpoolId(string, required): SundaeSwap pool identifier
Success Response:
{
"success": true,
"data": {
"txHash": "abc123...",
"walletAddress": "addr1..."
}
}Error Response:
{
"success": false,
"error": "Error message here"
}You can also use the swap function directly in your code:
import { swapTokens } from "./adaUsdmSwap.js";
const result = await swapTokens(
mnemonic,
amount, // Amount to swap
isFromAda, // true for ADA → Token, false for Token → ADA
fromToken, // Token object
toToken, // Token object
poolId // Pool identifier
);
console.log("Transaction Hash:", result.txHash);The application queries SundaeSwap for pool data using the pool identifier:
const queryProvider = new QueryProviderSundaeSwap("mainnet");
const poolData = await queryProvider.findPoolData({
ident: poolId,
});The swap is configured with:
- Swap Type: Market order with configurable slippage (default: 3%)
- Pool Data: The queried pool information
- Destination Address: Your wallet address (no datum required)
- Supplied Asset: The amount and asset you're swapping
const args = {
swapType: {
type: ESwapType.MARKET,
slippage: 0.03, // 3% slippage tolerance
},
pool: poolData,
orderAddresses: {
DestinationAddress: {
address: wallet.address,
datum: {
type: EDatumType.NONE,
},
},
},
suppliedAsset: new AssetAmount(
BigInt(amount * 1_000_000),
isFromAda ? poolData.assetA : poolData.assetB
),
};The transaction is built using the SundaeSwap V3 transaction builder:
const txBuilder = new TxBuilderLucidV3(
lucid,
new DatumBuilderLucidV3("mainnet")
);
const { build } = await txBuilder.swap(args);
const builtTx = await build();
const { submit } = await builtTx.sign();
const txHash = await submit();ada-swap/
├── adaUsdmSwap.js # Core swap functionality
├── server.js # Express API server
├── test.js # Test file
├── package.json # Dependencies
├── .env # Environment variables (not committed)
└── README.md # This file
@sundaeswap/core- SundaeSwap SDKlucid-cardano- Cardano transaction building@blockfrost/blockfrost-js- Blockfrost API clientexpress- Web server frameworkdotenv- Environment variable management
The application is configured for the ADA/USDM pool:
- Pool ID:
64f35d26b237ad58e099041bc14c687ea7fdc58969d7d5b66e2540ef - USDM Token:
- Policy ID:
c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad - Asset Name:
5553444d
- Policy ID:
Default slippage is set to 3% (0.03). This can be modified in adaUsdmSwap.js:
swapType: {
type: ESwapType.MARKET,
slippage: 0.03, // Change this value (0.01 = 1%, 0.05 = 5%)
}- Never commit your
.envfile or mnemonic seed phrase - Use environment variables for sensitive data
- Test on testnet first before using mainnet
- Verify transaction details before signing
- Keep your mnemonic secure - anyone with access can control your wallet
- Ensure you have enough ADA for:
- The swap amount
- Transaction fees (~0.17 ADA)
- SundaeSwap scooper fees
- Verify the pool ID is correct
- Check that the pool exists on mainnet
- Ensure you're using the correct network configuration
- Check your wallet balance
- Verify the pool has sufficient liquidity
- Ensure slippage tolerance is appropriate
- Check Blockfrost API key is valid
- Verify your mnemonic is 12 or 24 words
- Ensure words are separated by spaces
- Check for typos in the seed phrase
Currently configured for Cardano Mainnet. To switch to testnet:
-
Update Blockfrost endpoint in
adaUsdmSwap.js:"https://cardano-preview.blockfrost.io/api/v0";
-
Change network parameter:
"Preview";
-
Use testnet pool IDs and tokens
Blockfrost free tier limits:
- 100 requests per second
- 10,000 requests per day
Monitor your usage to avoid rate limiting.
- SundaeSwap Documentation
- Lucid Cardano Documentation
- Blockfrost API Documentation
- Cardano Documentation
Warning: This application interacts with real funds on Cardano mainnet. Always test thoroughly and use at your own risk. Never share your mnemonic seed phrase or commit sensitive credentials to version control.