DEV Community

Cover image for How to Build a DCA Bot with a Swap API
Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Build a DCA Bot with a Swap API

Dollar-cost averaging (DCA) is the practice of investing a fixed amount at regular intervals regardless of price. It is one of the most studied strategies in both traditional and crypto markets. A Vanguard analysis of rolling 10-year periods found that DCA reduced maximum drawdown by 30-40% compared to lump-sum entries. In crypto specifically, a Bitcoin DCA strategy of $100/week from January 2019 through December 2024 turned $31,400 in contributions into approximately $150,000 — a 378% return — despite buying through two major bear markets. According to CoinDCA data, investors who DCA'd through the 2022 crash saw their cost basis drop 45% below the eventual recovery price, turning paper losses into outsized gains. A Bitwise study showed that 83% of investors who attempted to time the crypto market underperformed a simple weekly DCA over any three-year window. Meanwhile, the global crypto trading bot market reached $2.1 billion in 2025, growing at 12% annually as more traders automate execution.

The problem: most DCA tools are custodial platforms that hold your keys, charge 1-2% fees per trade, and support limited tokens. Building your own DCA bot gives you full custody, zero platform fees, and the ability to DCA into any token on any chain.

This guide shows you how to build a non-custodial DCA bot in TypeScript that executes recurring swaps using swapapi.dev — a free API that returns executable swap calldata from a single GET request across 46 EVM chains, with no API key required.

What You'll Need

  • Node.js 18+ or Bun runtime
  • ethers.js v6 for wallet and transaction management
  • A wallet with a private key (use a dedicated bot wallet, not your main wallet)
  • Native gas tokens on your target chain (ETH, MATIC, BNB, etc.)
  • No API keys — swapapi.dev is free and keyless

Install the dependency:

bun add ethers
Enter fullscreen mode Exit fullscreen mode

Step 1: Configure Your DCA Parameters

Create a file called dca-bot.ts. Start by defining the configuration that controls what your bot buys, how often, and how much.

const CONFIG = {
  chainId: 1,
  tokenIn: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  amountPerSwap: "10000000000000000",
  intervalMs: 24 * 60 * 60 * 1000,
  rpcUrl: "https://cloudflare-eth.com",
  maxSlippage: 0.01,
};
Enter fullscreen mode Exit fullscreen mode

This configuration buys USDC with 0.01 ETH every 24 hours on Ethereum mainnet. The amountPerSwap is in wei (the token's smallest unit) — 10000000000000000 equals 0.01 ETH.

To DCA on a different chain, change chainId and the token addresses. For example, to DCA into WETH on Arbitrum, set chainId: 42161 and use the Arbitrum WETH address.

Step 2: Fetch a Swap Quote

The core of the bot is a function that calls the swap API to get executable calldata. The API returns a complete transaction object ready to submit on-chain.

async function fetchSwap(sender: string) {
  const url = `https://api.swapapi.dev/v1/swap/${CONFIG.chainId}?tokenIn=${CONFIG.tokenIn}&tokenOut=${CONFIG.tokenOut}&amount=${CONFIG.amountPerSwap}&sender=${sender}&maxSlippage=${CONFIG.maxSlippage}`;
  const res = await fetch(url);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

That single GET request returns the swap route, expected output amount, price impact, and a ready-to-execute transaction. No SDK installation, no authentication, no complex multi-step flows.

Step 3: Validate the Response

Before executing any swap, your bot needs to verify the response and check for unfavorable conditions. The API returns three possible statuses: Successful, Partial, and NoRoute.

function validateSwap(data: any): boolean {
  if (data.status === "NoRoute") return false;
  if (data.status === "Partial") return false;
  if (data.priceImpact < -0.05) return false;
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Rejecting swaps with price impact worse than -5% protects your bot from executing during low-liquidity conditions. For a DCA bot, skipping one interval is better than taking a bad fill.

Step 4: Execute the Swap Transaction

With a validated quote, submit the transaction on-chain using ethers.js. The API response includes the complete tx object — your bot just forwards it.

import { ethers } from "ethers";

async function executeSwap(wallet: ethers.Wallet, tx: any) {
  const gasEstimate = await wallet.estimateGas({
    to: tx.to,
    data: tx.data,
    value: tx.value,
  });

  const receipt = await wallet.sendTransaction({
    to: tx.to,
    data: tx.data,
    value: tx.value,
    gasLimit: gasEstimate * 12n / 10n,
  });

  return receipt.wait();
}
Enter fullscreen mode Exit fullscreen mode

The gas estimate is multiplied by 1.2 (using BigInt arithmetic: * 12n / 10n) to add a 20% buffer. This prevents out-of-gas reverts on complex multi-hop routes. The API's gasPrice field is omitted intentionally — ethers.js automatically uses EIP-1559 pricing on supported chains.

Step 5: Wire Up the DCA Loop

Combine everything into a loop that runs on your configured interval.

async function runDCA() {
  const provider = new ethers.JsonRpcProvider(CONFIG.rpcUrl);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
  const sender = wallet.address;

  console.log(`DCA bot started for ${sender}`);

  async function executeCycle() {
    const quote = await fetchSwap(sender);

    if (!quote.success) {
      console.log(`API error: ${quote.error.message}`);
      return;
    }

    if (!validateSwap(quote.data)) {
      console.log(`Skipping: status=${quote.data.status}`);
      return;
    }

    const amountOut = Number(quote.data.expectedAmountOut) / 10 ** quote.data.tokenTo.decimals;
    console.log(`Swapping for ~${amountOut} ${quote.data.tokenTo.symbol}`);

    const receipt = await executeSwap(wallet, quote.data.tx);
    console.log(`TX confirmed: ${receipt.hash}`);
  }

  await executeCycle();
  setInterval(executeCycle, CONFIG.intervalMs);
}

runDCA();
Enter fullscreen mode Exit fullscreen mode

Run the bot with your private key as an environment variable:

PRIVATE_KEY=0xYOUR_KEY bun dca-bot.ts
Enter fullscreen mode Exit fullscreen mode

The bot executes the first swap immediately, then repeats every 24 hours.

Step 6: Add Error Handling and Retries

Production DCA bots need to handle transient failures gracefully. Network issues, RPC downtime, and temporary API unavailability should not crash the bot.

async function executeCycleWithRetry(sender: string, wallet: ethers.Wallet) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const quote = await fetchSwap(sender);

      if (!quote.success && quote.error?.code === "RATE_LIMITED") {
        await new Promise((r) => setTimeout(r, 10000 * attempt));
        continue;
      }

      if (!quote.success || !validateSwap(quote.data)) return;

      const receipt = await executeSwap(wallet, quote.data.tx);
      console.log(`TX confirmed: ${receipt.hash}`);
      return;
    } catch (err) {
      console.log(`Attempt ${attempt} failed: ${err}`);
      if (attempt < 3) await new Promise((r) => setTimeout(r, 5000 * attempt));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This retry logic handles RATE_LIMITED responses with exponential backoff (10s, 20s, 30s) and catches network errors with a separate backoff (5s, 10s). After three failures, the cycle is skipped — the next interval will try again.

Step 7: Support ERC-20 to ERC-20 DCA

The examples above use native ETH as the input token. If you want to DCA from a stablecoin (e.g., USDC) into ETH or another token, you need to handle ERC-20 approvals first.

async function ensureApproval(wallet: ethers.Wallet, token: string, spender: string, amount: string) {
  const erc20 = new ethers.Contract(token, [
    "function allowance(address,address) view returns (uint256)",
    "function approve(address,uint256) returns (bool)",
  ], wallet);

  const current = await erc20.allowance(wallet.address, spender);
  if (current >= BigInt(amount)) return;

  const tx = await erc20.approve(spender, ethers.MaxUint256);
  await tx.wait();
}
Enter fullscreen mode Exit fullscreen mode

Call ensureApproval before each swap when your tokenIn is not the native gas token. The function checks the current allowance and only sends an approval transaction if needed. Using MaxUint256 for the approval amount means you only need to approve once per token-spender pair.

After approval confirms, fetch a fresh quote before executing — the API's calldata includes a 30-second deadline, so stale quotes will revert.

Step 8: Deploy and Run Continuously

For a DCA bot, uptime matters. A missed interval is a missed buy. Here are practical deployment options:

Systemd service (Linux VPS): Create a service file that auto-restarts on failure. A $5/month VPS is sufficient.

Cron job approach: Instead of a long-running process, schedule execution via cron. This eliminates memory leaks and process management concerns.

0 9 * * * cd /path/to/bot && PRIVATE_KEY=0xKEY bun dca-bot.ts --once
Enter fullscreen mode Exit fullscreen mode

Add a --once flag to your bot that executes a single cycle and exits. Cron handles the scheduling.

Docker: Containerize the bot for portable deployment across cloud providers.

Adapting for Other Chains

One advantage of building on swapapi.dev is that switching chains requires changing only three values in your config. The API covers 46 EVM chains with the same endpoint format.

To DCA into WETH on Base:

const CONFIG = {
  chainId: 8453,
  tokenIn: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  tokenOut: "0x4200000000000000000000000000000000000006",
  amountPerSwap: "10000000000000000",
  intervalMs: 24 * 60 * 60 * 1000,
  rpcUrl: "https://mainnet.base.org",
  maxSlippage: 0.01,
};
Enter fullscreen mode Exit fullscreen mode

The native token address (0xEeee...EeE) works on every chain. Only tokenOut, chainId, and rpcUrl change. The API response even includes recommended RPCs in the rpcUrls field, so you can use those instead of hardcoding one.

FAQ

How much does the swap API cost?
Nothing. swapapi.dev is completely free with no API key required. The rate limit is approximately 30 requests per minute per IP, which is more than enough for a DCA bot running hourly or daily intervals.

Is this bot custodial?
No. Your private key never leaves your machine. The API returns transaction calldata that your local wallet signs and submits. swapapi.dev never has access to your funds.

What happens if the bot misses an interval?
It simply executes on the next interval. DCA's effectiveness comes from consistency over months and years — missing a single buy has negligible impact on long-term returns.

Can I DCA on multiple chains simultaneously?
Yes. Run multiple instances with different configs, or modify the bot to iterate over an array of chain configurations in each cycle.

How do I handle the USDT approval quirk?
USDT on Ethereum requires setting the allowance to zero before setting a new value. If your tokenIn is USDT on Ethereum, call approve(spender, 0) before approve(spender, MaxUint256).

What if the API returns a Partial status?
The bot's validation function rejects partial fills. For a DCA bot, this is the safest approach — partial fills indicate low liquidity, and it is better to retry on the next interval when conditions may improve.

Get Started

The complete DCA bot is under 100 lines of TypeScript. Clone the pattern, adjust the config for your target chain and token pair, and deploy. swapapi.dev handles the routing complexity — your bot just needs to call one endpoint and submit the transaction.

Start building at swapapi.dev.

Top comments (0)