DCA.fun Pig LogoDCA.fun
Filler guide

Execution Strategies

Advanced strategies for profitable DCA order execution

Understanding the Dutch Auction Mechanism

DCA.fun implements a Dutch auction system that creates predictable arbitrage opportunities for fillers while ensuring users get fair execution prices over time.

How the Auction Works

The protocol adjusts the effective price of the input token based on time elapsed since the order became executable:

adjustedTokenInPrice = tokenInPrice × (10000 + slippage - scalingFactor) / 10000

where:
- scalingFactor = (timeElapsed / scalingInterval) × slippage × 2
- timeElapsed = current time - order execution time
- slippage and scalingFactor are in basis points

Timeline Example

For an order with 100 bp (1%) slippage and 120-second scaling interval:

Time    | Scaling | User Price    | Filler Opportunity
--------|---------|---------------|-------------------
T+0s    | 0%      | Market +1%    | No profit
T+30s   | 25%     | Market +0.5%  | 0.5% profit
T+60s   | 50%     | Market        | 1% profit
T+90s   | 75%     | Market -0.5%  | 1.5% profit
T+120s  | 100%    | Market -1%    | 2% profit (max)

Key Parameters

  • Slippage: User-defined max price deviation (typically 50-200 bp)
  • Scaling Interval: Time to reach max discount (max 300 seconds)
  • Frequency Interval: Time between order executions
  • Protocol Fee: Small fee taken from output tokens

Profit Calculation

Basic Formula

function calculateFillerProfit(orderParams, currentPrices, timeElapsed) {
  // Calculate scaling factor
  const scalingFactor = Math.min(
    (timeElapsed / orderParams.scalingInterval) * orderParams.slippage * 2,
    orderParams.slippage * 2
  );

  // Adjusted price user pays
  const adjustedPrice = currentPrices.tokenIn *
    (10000 + orderParams.slippage - scalingFactor) / 10000;

  // Value calculations
  const inputValue = orderParams.amount * adjustedPrice;
  const outputValue = orderParams.amount * currentPrices.tokenIn;

  // Profit calculation
  const grossProfit = inputValue - outputValue;
  const gasEstimate = estimateGasInUSD();
  const netProfit = grossProfit - gasEstimate;

  return {
    grossProfit,
    netProfit,
    profitPercentage: (grossProfit / outputValue) * 100,
    optimalWaitTime: orderParams.scalingInterval // Maximum profit point
  };
}

Advanced Considerations

  1. Gas Costs: Must be factored into profitability
  2. Price Volatility: Market movements during execution
  3. Competition: Other fillers may execute first
  4. Slippage on DEX: Your swap execution costs

Optimal Timing Strategy

The Timing Dilemma

Fillers face a trade-off:

  • Wait longer: Higher profit per fill
  • Execute early: Guarantee execution before competitors

Optimal Strategy Framework

class OptimalFillingStrategy {
  constructor(config) {
    this.minProfitUSD = config.minProfitUSD || 1;
    this.gasPrice = config.gasPrice;
    this.competitionFactor = config.competitionFactor || 0.7; // 0-1
  }

  calculateOptimalExecutionTime(order, currentTime) {
    const timeSinceExecutable = currentTime - order.nextExecutionTime;

    // Base optimal time (maximum profit)
    let optimalTime = order.scalingInterval;

    // Adjust for competition
    optimalTime *= this.competitionFactor;

    // Adjust for order value
    const orderValueUSD = this.calculateOrderValue(order);
    if (orderValueUSD > 1000) {
      // High-value orders: execute earlier
      optimalTime *= 0.5;
    } else if (orderValueUSD < 100) {
      // Low-value orders: wait for max profit
      optimalTime *= 0.9;
    }

    // Check if we've reached optimal time
    return timeSinceExecutable >= optimalTime;
  }

  shouldExecute(order, quote) {
    const profit = this.calculateProfit(quote);
    const optimalTimeReached = this.calculateOptimalExecutionTime(
      order,
      Date.now() / 1000
    );

    // Execute if:
    // 1. Profit exceeds minimum AND optimal time reached
    // 2. OR profit is exceptionally high (>2x minimum)
    return (
      (profit.netProfit > this.minProfitUSD && optimalTimeReached) ||
      (profit.netProfit > this.minProfitUSD * 2)
    );
  }
}

MEV Protection Strategies

Understanding MEV Risks

Fillers face several MEV-related risks:

  1. Front-running: Competitors seeing and copying your transaction
  2. Sandwich attacks: Being trapped between buy and sell orders
  3. Block stuffing: Validators filling blocks to exclude your tx

Protection Mechanisms

1. Private Mempools

Use services like Flashbots to keep transactions private:

const { FlashbotsBundleProvider } = require("@flashbots/ethers-provider-bundle");

async function submitPrivateTransaction(tx) {
  const flashbotsProvider = await FlashbotsBundleProvider.create(
    provider,
    authSigner,
    "https://relay.flashbots.net"
  );

  const bundle = [
    {
      signedTransaction: tx
    }
  ];

  const bundleSubmission = await flashbotsProvider.sendBundle(
    bundle,
    targetBlockNumber
  );

  return bundleSubmission;
}

2. Commit-Reveal Schemes

For high-value orders, use a two-phase approach:

class CommitRevealFiller {
  async commitToFill(orderId, secretHash) {
    // Phase 1: Commit to filling with a hash
    const commitment = ethers.utils.keccak256(
      ethers.utils.defaultAbiCoder.encode(
        ["uint256", "address", "bytes32"],
        [orderId, this.address, secretHash]
      )
    );

    // Store commitment on-chain or off-chain
    await this.storeCommitment(commitment);

    // Wait for commitment period
    await this.wait(COMMITMENT_PERIOD);

    // Phase 2: Reveal and execute
    return this.revealAndFill(orderId, secretHash);
  }
}

3. Dynamic Gas Pricing

Adjust gas dynamically to avoid being priced out:

async function calculateOptimalGasPrice() {
  const baseGasPrice = await provider.getGasPrice();
  const profitMargin = calculateExpectedProfit();

  // Scale gas price with profit opportunity
  const maxGasPrice = baseGasPrice.mul(150).div(100); // 1.5x base
  const optimalGasPrice = baseGasPrice.add(
    profitMargin.mul(10).div(100) // Use 10% of profit for gas
  );

  return optimalGasPrice.lt(maxGasPrice) ? optimalGasPrice : maxGasPrice;
}

Competition Analysis

Monitoring Competitors

Track other fillers to optimize your strategy:

class CompetitorAnalyzer {
  constructor(dcaContract) {
    this.competitors = new Map();
    this.dcaContract = dcaContract;

    // Monitor fill events
    dcaContract.on("FillOrder", this.handleFillEvent.bind(this));
  }

  handleFillEvent(orderId, filler, recipient, fillableAmount) {
    if (!this.competitors.has(filler)) {
      this.competitors.set(filler, {
        fills: 0,
        volume: BigNumber.from(0),
        avgResponseTime: 0
      });
    }

    const competitor = this.competitors.get(filler);
    competitor.fills++;
    competitor.volume = competitor.volume.add(fillableAmount);

    // Calculate average response time
    this.updateResponseTime(filler, orderId);
  }

  getCompetitionLevel(orderId) {
    // Analyze how many competitors typically fill similar orders
    const similarOrders = this.findSimilarOrders(orderId);
    const uniqueFillers = new Set(
      similarOrders.map(o => o.filler)
    ).size;

    return {
      level: uniqueFillers > 10 ? "high" : uniqueFillers > 5 ? "medium" : "low",
      count: uniqueFillers,
      recommendation: this.getStrategyRecommendation(uniqueFillers)
    };
  }
}

Portfolio Management

Diversification Strategy

Don't put all eggs in one basket:

class FillerPortfolio {
  constructor(config) {
    this.maxExposurePerToken = config.maxExposurePerToken || 0.2; // 20%
    this.maxExposurePerOrder = config.maxExposurePerOrder || 0.05; // 5%
    this.reserveRatio = config.reserveRatio || 0.3; // 30% reserve
  }

  async evaluateOrder(order, quote) {
    const portfolio = await this.getCurrentPortfolio();
    const orderValue = this.calculateOrderValue(quote);

    // Check token exposure
    const tokenExposure = portfolio.tokenExposure[order.tokenOut] || 0;
    const newExposure = tokenExposure + orderValue;

    if (newExposure / portfolio.totalValue > this.maxExposurePerToken) {
      return { shouldFill: false, reason: "Token exposure limit" };
    }

    // Check order size
    if (orderValue / portfolio.totalValue > this.maxExposurePerOrder) {
      return { shouldFill: false, reason: "Order size limit" };
    }

    // Check reserves
    const availableCapital = portfolio.totalValue * (1 - this.reserveRatio);
    if (orderValue > availableCapital) {
      return { shouldFill: false, reason: "Insufficient reserves" };
    }

    return { shouldFill: true };
  }
}

Automated Execution Framework

Complete Bot Implementation

class DCAFillerBot {
  constructor(config) {
    this.dcaContract = config.dcaContract;
    this.strategy = new OptimalFillingStrategy(config.strategy);
    this.portfolio = new FillerPortfolio(config.portfolio);
    this.competitors = new CompetitorAnalyzer(config.dcaContract);
    this.activeOrders = new Map();
  }

  async start() {
    // Monitor new orders
    this.dcaContract.on("CreateOrder", this.handleNewOrder.bind(this));

    // Periodic scanning
    setInterval(() => this.scanOrders(), 30000); // Every 30 seconds

    console.log("Filler bot started");
  }

  async handleNewOrder(orderId, creator, params) {
    const order = {
      id: orderId,
      creator,
      nextExecution: Date.now() / 1000 + params.freqInterval,
      ...params
    };

    this.activeOrders.set(orderId, order);
    this.scheduleExecution(order);
  }

  async scheduleExecution(order) {
    const optimalDelay = this.strategy.calculateOptimalExecutionTime(
      order,
      Date.now() / 1000
    );

    setTimeout(
      () => this.attemptFill(order.id),
      optimalDelay * 1000
    );
  }

  async attemptFill(orderId) {
    try {
      // 1. Get quote
      const quote = await this.getQuote(orderId);

      // 2. Evaluate profitability
      if (!this.strategy.shouldExecute(this.activeOrders.get(orderId), quote)) {
        console.log(`Order ${orderId} not profitable yet`);
        return;
      }

      // 3. Check portfolio limits
      const portfolioCheck = await this.portfolio.evaluateOrder(
        this.activeOrders.get(orderId),
        quote
      );

      if (!portfolioCheck.shouldFill) {
        console.log(`Order ${orderId} rejected: ${portfolioCheck.reason}`);
        return;
      }

      // 4. Check competition
      const competition = this.competitors.getCompetitionLevel(orderId);
      if (competition.level === "high") {
        // Use MEV protection for high competition
        await this.fillWithMEVProtection(orderId, quote);
      } else {
        // Standard fill
        await this.standardFill(orderId, quote);
      }

    } catch (error) {
      console.error(`Failed to fill order ${orderId}:`, error);
    }
  }

  async standardFill(orderId, quote) {
    const gasPrice = await calculateOptimalGasPrice();

    const tx = await this.dcaContract.fillOrder(
      quote.encodedData,
      this.wallet.address,
      { gasPrice }
    );

    console.log(`Filled order ${orderId}: ${tx.hash}`);
    return tx.wait();
  }

  async fillWithMEVProtection(orderId, quote) {
    // Use Flashbots or similar
    return submitPrivateTransaction(
      await this.prepareFillTransaction(orderId, quote)
    );
  }
}

// Start the bot
const bot = new DCAFillerBot({
  dcaContract: dcaDotFun,
  strategy: {
    minProfitUSD: 1,
    competitionFactor: 0.7
  },
  portfolio: {
    maxExposurePerToken: 0.2,
    maxExposurePerOrder: 0.05,
    reserveRatio: 0.3
  }
});

bot.start();

Performance Optimization

1. Batch Processing

Process multiple orders in single transaction:

async function batchFillOrders(orderIds) {
  const quotes = await Promise.all(
    orderIds.map(id => getQuote(id))
  );

  // Filter profitable orders
  const profitableOrders = quotes.filter(
    q => calculateProfit(q).netProfit > MIN_PROFIT
  );

  // Use multicall for batch execution
  const multicall = new Multicall(provider);
  const calls = profitableOrders.map(q => ({
    target: DCA_CONTRACT,
    callData: dcaInterface.encodeFunctionData("fillOrder", [
      q.encodedData,
      fillerAddress
    ])
  }));

  return multicall.aggregate(calls);
}

2. Caching Strategy

Cache frequently accessed data:

class CachedDataProvider {
  constructor(ttl = 30000) { // 30 second TTL
    this.cache = new Map();
    this.ttl = ttl;
  }

  async getTokenPrice(token) {
    const cacheKey = `price_${token}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.value;
    }

    const price = await fetchTokenPrice(token);
    this.cache.set(cacheKey, {
      value: price,
      timestamp: Date.now()
    });

    return price;
  }
}

Risk Management

Stop-Loss Mechanisms

Implement safeguards to limit losses:

class RiskManager {
  constructor(config) {
    this.maxDailyLoss = config.maxDailyLoss || 100; // USD
    this.maxConsecutiveLosses = config.maxConsecutiveLosses || 5;
    this.dailyStats = { profit: 0, losses: 0, consecutiveLosses: 0 };
  }

  async evaluateFill(order, expectedProfit) {
    // Check daily loss limit
    if (this.dailyStats.losses > this.maxDailyLoss) {
      throw new Error("Daily loss limit reached");
    }

    // Check consecutive losses
    if (this.dailyStats.consecutiveLosses >= this.maxConsecutiveLosses) {
      throw new Error("Too many consecutive losses");
    }

    return true;
  }

  recordResult(profit) {
    if (profit < 0) {
      this.dailyStats.losses += Math.abs(profit);
      this.dailyStats.consecutiveLosses++;
    } else {
      this.dailyStats.profit += profit;
      this.dailyStats.consecutiveLosses = 0;
    }
  }
}

Best Practices Summary

  1. Start Conservative: Begin with small fills to understand dynamics
  2. Monitor Constantly: Track performance metrics and adjust strategies
  3. Diversify: Don't focus on single token pairs or order sizes
  4. Protect Against MEV: Use private mempools for valuable fills
  5. Manage Risk: Implement stop-losses and exposure limits
  6. Optimize Gas: Balance speed with cost efficiency
  7. Stay Updated: Monitor protocol changes and market conditions
  8. Test Thoroughly: Use testnets before mainnet deployment
  9. Document Everything: Keep detailed logs for analysis
  10. Collaborate: Share strategies with the community (keeping alpha private)