It All Started With An Idea

Automated Bid Optimization Script for Google Ads

  1. Script Outline:Fetch Data: Retrieves conversion data and cost data for a specified campaign and time window.Calculate Moving Average CPA: Computes the moving average CPA based on the conversion and cost data.Calculate Prior 7-Day Average CPA: Calculates the average CPA for the previous 7 days.Calculate Expected Next 7-Day CPA: Estimates the CPA for the next 7 days based on the moving average.Adjust Threshold: Adjusts the threshold based on the moving average CPA.Calculate New Bid: Determines a new bid based on the target CPA and moving average CPA.Update Bid: Updates the campaign’s bid with the new calculated bid.Log Factors: Logs various factors influencing bid adjustment, such as moving average CPA, target CPA, prior 7-day average CPA, expected CPA for the next 7 days, threshold, and expected change in CPA.
  2. Impact on Advertising:Bid Optimization: The script optimizes bids based on historical performance data, helping to achieve the desired target CPA.Budget Efficiency: By adjusting bids based on moving average CPA and target CPA, the script ensures efficient allocation of budget, maximizing conversions within the specified cost constraints.Performance Monitoring: By logging various factors influencing bid adjustments, the script provides insights into campaign performance trends, enabling advertisers to make data-driven decisions.Time-Saving: Automating bid adjustments and performance monitoring tasks saves time and effort for advertisers, allowing them to focus on other strategic aspects of campaign management.

Overall, the script facilitates bid optimization and performance monitoring, contributing to more effective and efficient advertising campaigns on Google Ads.

Pros:

  1. Automated Bid Adjustment: The script automatically adjusts bidding based on historical performance, ensuring bids align with target CPA goals.
  2. Efficiency: Saves time by automating bid adjustments, freeing up resources for other strategic tasks.
  3. Accuracy: Utilizes historical data to make informed bid decisions, potentially improving campaign performance.
  4. Flexibility: Parameters like target CPA, moving average window size, and threshold can be easily adjusted to suit specific campaign requirements.
  5. Real-time Insights: Provides real-time insights into bid adjustments and expected changes in CPA, allowing for proactive campaign management.

Cons:

  1. Dependency on Historical Data: Relies heavily on historical performance data, which may not always accurately predict future performance, especially in dynamic market conditions.
  2. Lack of Human Judgment: Automated scripts may lack the intuition and strategic insights that human advertisers bring to bid management.
  3. Complexity: Implementing and fine-tuning bid optimization scripts requires technical expertise and ongoing monitoring to ensure effectiveness.
  4. Potential for Errors: Errors in data collection or scripting logic could lead to suboptimal bidding decisions or unintended consequences.
  5. Limited Scope: While bid optimization is important, it’s just one aspect of campaign management, and relying solely on automated bidding scripts may overlook other critical optimization opportunities.
var previousMovingAvgCPA; // Define the variable outside the main function

function main() {
    var campaignId = '20467244611'; // Replace with your campaign ID
    var targetCPA = 195; // Replace with your target CPA value
    var windowSize = 6; // select the number of days moving average window size
    var threshold = 0.2; // Initial threshold

    var conversionData = getConversionData(campaignId, windowSize);
    var costData = getCostData(campaignId, windowSize);
    var movingAvgCPA = calculateMovingAverageCPA(conversionData, costData);
    var newTargetCPA = calculateNewTargetCPA(targetCPA, movingAvgCPA, 0.5); // Using a weight factor of 0.5 for a moderate change
    var newBid = calculateNewBid(newTargetCPA, movingAvgCPA);

    // Log factors influencing bid adjustment
    Logger.log('Moving Average CPA: ' + movingAvgCPA.toFixed(2));
    Logger.log('Target CPA: ' + newTargetCPA.toFixed(2));

    // Log prior 7-day average CPA
    var prior7DayAvgCPA = calculatePrior7DayAvgCPA(costData);
    Logger.log('Prior 7-Day Average CPA: ' + prior7DayAvgCPA.toFixed(2));

    // Log expected CPA for the next 7 days
    var expectedNext7DayCPA = calculateExpectedNext7DayCPA(movingAvgCPA);
    Logger.log('Expected CPA for the Next 7 Days: ' + expectedNext7DayCPA.toFixed(2));

    // Check and adjust threshold if necessary
    threshold = adjustThreshold(movingAvgCPA, threshold);

    // Log threshold value
    Logger.log('Threshold: ' + threshold.toFixed(2));

    // Log expected change for the next day or hour
    var expectedChange = newBid - newTargetCPA; // Current bid - new bid
    Logger.log('Expected change for the next day/hour: $' + expectedChange.toFixed(2));

    // Log the calculated new bid
    Logger.log('Calculated New Bid: ' + newBid.toFixed(2));

    // Update the bid
    updateBid(campaignId, newBid);

    // Update previousMovingAvgCPA for next iteration
    previousMovingAvgCPA = movingAvgCPA;
}


function getConversionData(campaignId, windowSize) {
    var today = new Date();
    var ninetyDaysAgo = new Date(today.getTime() - (windowSize * 24 * 60 * 60 * 1000));
    var startDateString = Utilities.formatDate(ninetyDaysAgo, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');
    var endDateString = Utilities.formatDate(today, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');

    var query = 'SELECT Date, Conversions FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignId = ' + campaignId + ' AND Conversions > 0 DURING ' + startDateString + ',' + endDateString;
    var report = AdsApp.report(query);
    var rows = report.rows();
    var data = [];

    while (rows.hasNext()) {
        var row = rows.next();
        data.push({ date: row['Date'], conversions: parseInt(row['Conversions']) });
    }

    return data;
}

function getCostData(campaignId, windowSize) {
    var today = new Date();
    var ninetyDaysAgo = new Date(today.getTime() - (windowSize * 24 * 60 * 60 * 1000));
    var startDateString = Utilities.formatDate(ninetyDaysAgo, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');
    var endDateString = Utilities.formatDate(today, AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');

    var query = 'SELECT Date, Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignId = ' + campaignId + ' DURING ' + startDateString + ',' + endDateString;
    var report = AdsApp.report(query);
    var rows = report.rows();
    var data = [];

    while (rows.hasNext()) {
        var row = rows.next();
        data.push({ date: row['Date'], cost: parseFloat(row['Cost']) });
    }

    return data;
}

function calculateMovingAverageCPA(conversionData, costData) {
    var totalCost = 0;
    var totalConversions = 0;

    for (var i = 0; i < conversionData.length; i++) {
        totalCost += costData.cost;
        totalConversions += conversionData.conversions;
    }

    return totalCost / totalConversions;
}

function calculatePrior7DayAvgCPA(costData) {
    // Assuming costData is sorted from oldest to newest
    var totalCost = 0;
    var numDays = Math.min(7, costData.length); // Consider at most the last 7 days
    for (var i = 0; i < numDays; i++) {
        totalCost += costData.cost; // Start from the most recent day
    }
    return totalCost / numDays;
}

function calculateExpectedNext7DayCPA(movingAvgCPA) {
    // Assuming that the next 7 days will have a similar CPA as the 90-day moving average
    // You can apply more sophisticated forecasting methods based on historical trends if needed
    return movingAvgCPA;
}

function adjustThreshold(movingAvgCPA, threshold) {
    // Here you can implement your logic to adjust the threshold based on moving average CPA
    // For example, you can increase the threshold if the moving average CPA is too volatile
    // or decrease it if the moving average CPA is stable
    return threshold; // Placeholder implementation, adjust as per your requirements
}

function calculateNewTargetCPA(currentTargetCPA, expectedCPAForNext7Days, weightFactor) {
    // Calculate the new target CPA based on a weighted average
    return (currentTargetCPA * (1 - weightFactor)) + (expectedCPAForNext7Days * weightFactor);
}

function calculateNewBid(targetCPA, movingAvgCPA) {
    var adjustmentFactor = 1.0; // Default to no adjustment

    // Calculate the difference between target CPA and moving average CPA
    var difference = targetCPA - movingAvgCPA;

    // Calculate the damping factor based on the difference
    var dampingFactor = Math.min(Math.abs(difference) / targetCPA, 1.0);

    // Define a cap on the bid adjustment (percentage of target CPA)
    var adjustmentCap = 0.2; // 20% of target CPA, adjust as needed

    // Apply the cap on the bid adjustment
    var cappedAdjustment = Math.min(Math.abs(difference), adjustmentCap * targetCPA);

    // Define a scaling factor to control the influence of damping factor on adjustment
    var dampingScale = 0.2; // Adjust as needed to control the damping effect

    // If the moving average CPA is lower than the target CPA,
    // apply the damping factor to increase the bid adjustment
    if (difference > 0) {
        adjustmentFactor += (1 - dampingFactor * dampingScale) * (cappedAdjustment / targetCPA);
    }
    // If the moving average CPA is higher than the target CPA,
    // apply the damping factor to decrease the bid adjustment
    else if (difference < 0) {
        adjustmentFactor -= (1 - dampingFactor * dampingScale) * (cappedAdjustment / targetCPA);
    }

    // Calculate the new bid based on the adjusted adjustment factor
    return Math.round(adjustmentFactor * targetCPA * 10000) / 10000; // Round to 4 decimal places
}

function updateBid(campaignId, newBid) {
    var campaignIterator = AdsApp.campaigns().withIds().get();
    if (campaignIterator.hasNext()) {
        var campaign = campaignIterator.next();
        campaign.bidding().setTargetCpa(newBid);
    } else {
        Logger.log('Campaign with ID ' + campaignId + ' not found.');
    }
} 

About the author

John M. Williams — Founder, It All Started With An Idea. Senior paid media specialist with 15+ years and $350M+ in managed ad spend across Google, Meta, LinkedIn, and Amazon. Creator of Buddy, the open-source AI Google Ads agent. Hero Conf 2025 + 2026 speaker.

More about John · LinkedIn ↗ · GitHub ↗

Working notes

More from the blog.

Read the blog  Get a free audit