Dynamic Pricing and Algorithmic Marketing: The Complete Technical Playbook

Let me tell you what most ecommerce businesses are actually doing. They set a price. They set an ad budget. They run some campaigns. They wait to see what happens. Then they look at a dashboard, make some guesses about what worked, adjust a few things manually, and repeat. This is not marketing. This is expensive hoping. Real marketing in 2026 is a closed loop system where pricing, advertising, inventory, competitor positioning, and margin optimisation all talk to each other in real time. Where the price a customer sees depends on inventory levels, competitor pricing, their likelihood to convert, and your margin targets. Where your ad spend automatically flows toward the products that actually make you money, not just the ones that sell. Where you are not reacting to the market. You are actively shaping it, pulling multiple levers simultaneously, running a profit maximisation engine rather than a series of disconnected campaigns. This article is the technical playbook for building exactly that. I am going to cover the economics, the formulas, the system architecture, and then I am going to show you working Ruby on Rails code that implements it in a real Solidus ecommerce system. This is what I build. This is how it works.

Let me tell you what most ecommerce businesses are actually doing.

They set a price. They set an ad budget. They run some campaigns. They wait to see what happens. Then they look at a dashboard, make some guesses about what worked, adjust a few things manually, and repeat.

This is not marketing. This is expensive hoping.

Real marketing in 2026 is a closed loop system where pricing, advertising, inventory, competitor positioning, and margin optimisation all talk to each other in real time. Where the price a customer sees depends on inventory levels, competitor pricing, their likelihood to convert, and your margin targets. Where your ad spend automatically flows toward the products that actually make you money, not just the ones that sell. Where you are not reacting to the market. You are actively shaping it, pulling multiple levers simultaneously, running a profit maximisation engine rather than a series of disconnected campaigns.

This article is the technical playbook for building exactly that.

The economics: why static pricing is leaving money everywhere

Let me start with a fundamental truth that most businesses ignore: different customers will pay different amounts for the same product.

This is not controversial in economics. It is called price discrimination, and every successful business practices some form of it. Airlines do it. Hotels do it. Software companies do it with their pricing tiers. The question is not whether to do it, but how sophisticated your implementation is.

The three degrees of price discrimination

First degree price discrimination is perfect price discrimination: charging each customer exactly their maximum willingness to pay. This is theoretically optimal but practically impossible because you cannot read minds. However, you can approximate it through personalisation, negotiation, and dynamic pricing based on behavioural signals.

Second degree price discrimination is offering different pricing tiers or bundles that let customers self select based on their preferences. Buy more, pay less per unit. Choose the premium tier for more features. This captures some of the variance in willingness to pay without requiring you to identify it directly.

Third degree price discrimination is charging different prices to different market segments based on observable characteristics. Student discounts. Geographic pricing. Business versus consumer pricing. You identify the segment and apply segment level pricing.

Dynamic pricing combines elements of all three. You adjust prices based on observable factors (third degree), offer quantity discounts and bundles (second degree), and increasingly use behavioural signals to approximate individual willingness to pay (approaching first degree).

The profit mathematics

Let me show you why this matters with actual numbers.

Assume you sell a product with a cost of £20. At a price of £40, you sell 1000 units. Your profit is:

Profit=(PC)×Q=(4020)×1000=£20,000\text{Profit} = (P - C) \times Q = (40 - 20) \times 1000 = £20,000

Now imagine you could identify two segments. Segment A (400 customers) would pay up to £50. Segment B (600 customers) would pay up to £35. With a single price of £40, you capture Segment A but lose Segment B entirely.

With differentiated pricing:

ProfitA=(5020)×400=£12,000\text{Profit}_A = (50 - 20) \times 400 = £12,000 ProfitB=(3520)×600=£9,000\text{Profit}_B = (35 - 20) \times 600 = £9,000 Total Profit=£21,000\text{Total Profit} = £21,000

That is a 5% profit increase from the same underlying demand, just by recognising that willingness to pay varies and pricing accordingly.

In practice, the gains are often much larger because static pricing typically sits at a point that misses both high willingness customers (who would have paid more) and price sensitive customers (who would have bought at a lower price).

Price elasticity: the number you actually need

Price elasticity of demand measures how quantity demanded responds to price changes:

ϵ=%ΔQ%ΔP=QP×PQ\epsilon = \frac{\%\Delta Q}{\%\Delta P} = \frac{\partial Q}{\partial P} \times \frac{P}{Q}

If elasticity is -2, a 10% price increase leads to a 20% quantity decrease. If elasticity is -0.5, a 10% price increase leads to only a 5% quantity decrease.

The optimal markup over cost, assuming constant elasticity, is:

Optimal Markup=PCP=1ϵ\text{Optimal Markup} = \frac{P - C}{P} = \frac{-1}{\epsilon}

Rearranging to find the optimal price:

P=C×ϵϵ+1P^* = \frac{C \times \epsilon}{\epsilon + 1}

For a product with cost £20 and elasticity -2:

P=20×(2)2+1=401=£40P^* = \frac{20 \times (-2)}{-2 + 1} = \frac{-40}{-1} = £40

For the same product with elasticity -4 (more price sensitive):

P=20×(4)4+1=803=£26.67P^* = \frac{20 \times (-4)}{-4 + 1} = \frac{-80}{-3} = £26.67

Knowing your elasticity for each product, each segment, each time period, lets you calculate the profit maximising price rather than guessing.

From passive to active: the paradigm shift

Here is where it gets interesting. Traditional marketing is passive. You set parameters and observe outcomes. Dynamic pricing makes marketing active. You are continuously adjusting multiple variables based on real time feedback.

In the passive model, feedback loops take days or weeks. You change something, wait for data to accumulate, analyse it, decide on adjustments, implement them. By the time you react, the market has moved.

In the active model, the system is continuously optimising. Competitor dropped their price? Your system sees it within hours and either matches, undercuts, or holds position based on your strategy. Inventory building up? Prices adjust downward automatically. High margin product converting well? Ad budget shifts toward it without human intervention.

You are not managing campaigns. You are tuning an algorithm.

Competitor intelligence: the information advantage

You cannot price intelligently without knowing what competitors charge. This is obvious, yet most businesses check competitor prices manually, sporadically, and react slowly.

A proper competitor intelligence system:

  1. Crawls competitor sites on a schedule (daily, hourly, or more frequently for high velocity categories)
  2. Matches competitor products to your catalog (by SKU, UPC, or fuzzy matching on name and attributes)
  3. Stores historical pricing data to identify patterns and seasonality
  4. Triggers alerts or automatic responses when significant changes occur

Competitive response strategies

Once you have competitor data, you need a strategy for responding to it. Common approaches:

Undercutting: Always price below the lowest competitor by a fixed amount or percentage. Aggressive but potentially margin destroying. Best for commoditised products where you have a cost advantage.

Matching: Price at or very close to the market leader or average. Removes price as a differentiator, competition moves to other factors.

Premium positioning: Maintain a consistent premium over competitors. Requires non price differentiation (brand, service, quality) but protects margins.

Dynamic response: Different strategies for different situations. Undercut when you have excess inventory. Hold premium when demand is strong. Match when competitor drops price but you are already selling well.

The key insight is that these are not mutually exclusive. You can undercut on some products (loss leaders), match on others (competitive staples), and hold premium on others (differentiated products). The system should know which strategy applies to which product.

Margin aware advertising: spending where it matters

Here is a mistake I see constantly: optimising ad spend for revenue or ROAS without considering margin.

Imagine two products:

Product A: Sells for £100, costs £80, margin £20 (20%) Product B: Sells for £100, costs £40, margin £60 (60%)

If both have the same conversion rate and same cost per acquisition, your ROAS is identical. But Product B generates three times the profit per sale. Your ad budget should flow toward Product B, not be split evenly.

The correct metric is not ROAS (Return on Ad Spend measured in revenue). It is POAS (Profit on Ad Spend) or margin weighted ROAS:

POAS=Gross Profit from AdsAd Spend=(RevenueCOGS)Ad Spend\text{POAS} = \frac{\text{Gross Profit from Ads}}{\text{Ad Spend}} = \frac{(\text{Revenue} - \text{COGS})}{\text{Ad Spend}}

Or equivalently:

POAS=ROAS×Gross Margin %\text{POAS} = \text{ROAS} \times \text{Gross Margin \%}

A campaign with 4x ROAS on 20% margin products generates 0.8x POAS (losing money on ad spend). A campaign with 2x ROAS on 60% margin products generates 1.2x POAS (profitable).

Automatic budget reallocation

The budget allocation algorithm I use considers:

  1. Product margin: Higher margin products get more budget, all else equal
  2. Conversion rate: Products that convert better get more budget
  3. Inventory level: Overstocked products get budget boost, understocked products get budget reduction
  4. Competitive position: Products where you are price competitive get more budget than products where competitors undercut you

The allocation formula:

Budget Sharei=ScoreijScorej\text{Budget Share}_i = \frac{\text{Score}_i}{\sum_j \text{Score}_j}

Where the score for product ii is:

Scorei=Margini×ConvRatei×InventoryFactori×CompetitiveFactori\text{Score}_i = \text{Margin}_i \times \text{ConvRate}_i \times \text{InventoryFactor}_i \times \text{CompetitiveFactor}_i

Inventory factor might be:

InventoryFactor={1.5if stock>2×average weekly sales1.0if stock is normal0.5if stock<1×average weekly sales\text{InventoryFactor} = \begin{cases} 1.5 & \text{if stock} > 2 \times \text{average weekly sales} \\ 1.0 & \text{if stock is normal} \\ 0.5 & \text{if stock} < 1 \times \text{average weekly sales} \end{cases}

Competitive factor might be:

CompetitiveFactor={1.2if your price<lowest competitor1.0if your pricemarket average0.7if your price>highest competitor\text{CompetitiveFactor} = \begin{cases} 1.2 & \text{if your price} < \text{lowest competitor} \\ 1.0 & \text{if your price} \approx \text{market average} \\ 0.7 & \text{if your price} > \text{highest competitor} \end{cases}

The complete system architecture

Let me show you how all these pieces fit together:

Ruby on Rails Solidus implementation

Now let me show you actual code. I am assuming you have:

  1. A Solidus ecommerce installation
  2. A competitor crawler that feeds data to a CompetitorPriceApi class
  3. Google and Meta API clients configured

The competitor price model and sync

First, we need a model to store competitor prices:

# db/migrate/20260806000001_create_competitor_prices.rb
class CreateCompetitorPrices < ActiveRecord::Migration[7.1]
  def change
    create_table :competitor_prices do |t|
      t.references :variant, null: false, foreign_key: { to_table: :spree_variants }
      t.string :competitor_name, null: false
      t.string :competitor_sku
      t.decimal :price, precision: 10, scale: 2, null: false
      t.decimal :shipping_cost, precision: 10, scale: 2, default: 0
      t.boolean :in_stock, default: true
      t.datetime :last_seen_at
      t.timestamps
    end
    
    add_index :competitor_prices, [:variant_id, :competitor_name], unique: true
  end
end
# app/models/competitor_price.rb
class CompetitorPrice < ApplicationRecord
  belongs_to :variant, class_name: 'Spree::Variant'
  
  validates :competitor_name, presence: true
  validates :price, presence: true, numericality: { greater_than: 0 }
  
  scope :recent, -> { where('last_seen_at > ?', 24.hours.ago) }
  scope :in_stock, -> { where(in_stock: true) }
  
  def total_price
    price + (shipping_cost || 0)
  end
  
  def self.lowest_for_variant(variant_id)
    recent.in_stock
          .where(variant_id: variant_id)
          .order(:price)
          .first
  end
  
  def self.market_average_for_variant(variant_id)
    recent.in_stock
          .where(variant_id: variant_id)
          .average(:price)
          &.to_f
  end
end

The sync job that pulls from your crawler:

# app/jobs/competitor_price_sync_job.rb
class CompetitorPriceSyncJob < ApplicationJob
  queue_as :default
  
  def perform
    competitors = CompetitorConfig.active.pluck(:name)
    
    competitors.each do |competitor_name|
      sync_competitor(competitor_name)
    end
    
    # Trigger pricing engine after sync
    PricingEngineJob.perform_later
  end
  
  private
  
  def sync_competitor(competitor_name)
    # Your crawler API returns product data
    products = CompetitorPriceApi.fetch_all(competitor: competitor_name)
    
    products.each do |product_data|
      variant = match_to_variant(product_data)
      next unless variant
      
      CompetitorPrice.upsert(
        {
          variant_id: variant.id,
          competitor_name: competitor_name,
          competitor_sku: product_data[:sku],
          price: product_data[:price],
          shipping_cost: product_data[:shipping] || 0,
          in_stock: product_data[:in_stock],
          last_seen_at: Time.current,
          updated_at: Time.current
        },
        unique_by: [:variant_id, :competitor_name]
      )
    end
  rescue CompetitorPriceApi::Error => e
    Rails.logger.error("Competitor sync failed for #{competitor_name}: #{e.message}")
    Alerting.notify("Competitor sync failed", competitor: competitor_name, error: e.message)
  end
  
  def match_to_variant(product_data)
    # Match by UPC/EAN first
    if product_data[:upc].present?
      variant = Spree::Variant.find_by(sku: product_data[:upc])
      return variant if variant
    end
    
    # Fall back to product mapping table
    mapping = CompetitorProductMapping.find_by(
      competitor_name: product_data[:competitor],
      competitor_sku: product_data[:sku]
    )
    mapping&.variant
  end
end

The pricing engine

The core pricing logic:

# app/services/pricing_engine.rb
class PricingEngine
  STRATEGIES = {
    undercut: :apply_undercut_strategy,
    match: :apply_match_strategy,
    premium: :apply_premium_strategy,
    dynamic: :apply_dynamic_strategy
  }.freeze
  
  def initialize(variant)
    @variant = variant
    @product = variant.product
    @config = PricingConfig.for_product(@product) || PricingConfig.default
  end
  
  def calculate_optimal_price
    base_price = cost_plus_minimum_margin
    
    # Apply strategy based on product config
    strategy_price = send(STRATEGIES[@config.strategy], base_price)
    
    # Apply inventory adjustments
    inventory_adjusted = apply_inventory_adjustment(strategy_price)
    
    # Apply elasticity based optimisation
    elasticity_optimised = apply_elasticity_optimisation(inventory_adjusted)
    
    # Enforce price floors and ceilings
    enforce_bounds(elasticity_optimised)
  end
  
  def cost_plus_minimum_margin
    cost = @variant.cost_price || estimate_cost
    minimum_margin = @config.minimum_margin_percent / 100.0
    
    cost / (1 - minimum_margin)
  end
  
  def competitor_context
    @competitor_context ||= {
      lowest: CompetitorPrice.lowest_for_variant(@variant.id),
      average: CompetitorPrice.market_average_for_variant(@variant.id),
      prices: CompetitorPrice.recent.in_stock.where(variant_id: @variant.id)
    }
  end
  
  private
  
  def apply_undercut_strategy(base_price)
    lowest = competitor_context[:lowest]&.total_price
    return base_price unless lowest
    
    undercut_amount = @config.undercut_amount || 0.01
    undercut_percent = @config.undercut_percent || 0
    
    target = lowest - [undercut_amount, lowest * undercut_percent].max
    [target, base_price].max # Never go below cost plus minimum margin
  end
  
  def apply_match_strategy(base_price)
    average = competitor_context[:average]
    return base_price unless average
    
    [average, base_price].max
  end
  
  def apply_premium_strategy(base_price)
    average = competitor_context[:average]
    return base_price * (1 + @config.premium_percent / 100.0) unless average
    
    premium = average * (1 + @config.premium_percent / 100.0)
    [premium, base_price].max
  end
  
  def apply_dynamic_strategy(base_price)
    inventory_ratio = calculate_inventory_ratio
    
    if inventory_ratio > 2.0
      # Overstocked: be aggressive, undercut
      apply_undercut_strategy(base_price)
    elsif inventory_ratio < 0.5
      # Understocked: hold or go premium
      apply_premium_strategy(base_price)
    else
      # Normal: match market
      apply_match_strategy(base_price)
    end
  end
  
  def apply_inventory_adjustment(price)
    ratio = calculate_inventory_ratio
    
    adjustment = case ratio
                 when 0..0.5 then 1.1   # Low stock: increase 10%
                 when 0.5..1.5 then 1.0 # Normal: no change
                 when 1.5..3.0 then 0.95 # High stock: decrease 5%
                 else 0.90               # Very high: decrease 10%
                 end
    
    price * adjustment
  end
  
  def apply_elasticity_optimisation(price)
    elasticity = @product.price_elasticity || -2.0 # Default assumption
    cost = @variant.cost_price || estimate_cost
    
    # Optimal price formula: P* = (C * e) / (e + 1)
    optimal = (cost * elasticity) / (elasticity + 1)
    
    # Blend current price with optimal (don't swing too dramatically)
    blend_factor = @config.elasticity_blend_factor || 0.3
    price * (1 - blend_factor) + optimal * blend_factor
  end
  
  def enforce_bounds(price)
    floor = @config.price_floor || cost_plus_minimum_margin
    ceiling = @config.price_ceiling || Float::INFINITY
    
    [[price, floor].max, ceiling].min
  end
  
  def calculate_inventory_ratio
    current_stock = @variant.total_on_hand
    avg_weekly_sales = SalesVelocity.weekly_average(@variant.id) || 1
    
    current_stock.to_f / avg_weekly_sales
  end
  
  def estimate_cost
    # If no cost price set, estimate from price and typical margin
    @variant.price * 0.6 # Assume 40% margin as fallback
  end
end

The Solidus price decorator

Integrate dynamic pricing into Solidus:

# app/models/spree/variant_decorator.rb
module Spree
  module VariantDecorator
    def self.prepended(base)
      base.class_eval do
        has_many :competitor_prices, class_name: '::CompetitorPrice'
        has_one :dynamic_price, class_name: '::DynamicPrice'
      end
    end
    
    def price_for_user(user = nil, context = {})
      return price unless dynamic_pricing_enabled?
      
      cached_dynamic_price || calculate_and_cache_dynamic_price
    end
    
    def dynamic_pricing_enabled?
      Spree::Config[:dynamic_pricing_enabled] && 
        product.dynamic_pricing_enabled?
    end
    
    def cached_dynamic_price
      dynamic_price&.current_price if dynamic_price&.fresh?
    end
    
    def calculate_and_cache_dynamic_price
      engine = PricingEngine.new(self)
      optimal = engine.calculate_optimal_price
      
      DynamicPrice.upsert(
        {
          variant_id: id,
          calculated_price: optimal,
          competitor_lowest: engine.competitor_context[:lowest]&.price,
          competitor_average: engine.competitor_context[:average],
          calculated_at: Time.current,
          updated_at: Time.current
        },
        unique_by: :variant_id
      )
      
      optimal
    end
    
    def current_margin
      return nil unless cost_price && price
      ((price - cost_price) / price * 100).round(2)
    end
    
    def current_margin_absolute
      return nil unless cost_price && price
      price - cost_price
    end
  end
end

Spree::Variant.prepend(Spree::VariantDecorator)

Budget allocation engine

Now the automatic ad budget reallocation:

# app/services/budget_allocator.rb
class BudgetAllocator
  def initialize(total_budget:, platform:)
    @total_budget = total_budget
    @platform = platform # :google or :meta
  end
  
  def calculate_allocation
    products = eligible_products
    scores = products.map { |p| [p.id, calculate_product_score(p)] }.to_h
    total_score = scores.values.sum
    
    return {} if total_score.zero?
    
    products.each_with_object({}) do |product, allocation|
      share = scores[product.id] / total_score
      allocation[product.id] = (@total_budget * share).round(2)
    end
  end
  
  def apply_allocation!
    allocation = calculate_allocation
    
    case @platform
    when :google
      apply_google_allocation(allocation)
    when :meta
      apply_meta_allocation(allocation)
    end
    
    log_allocation(allocation)
  end
  
  private
  
  def eligible_products
    Spree::Product
      .joins(:variants)
      .where(spree_variants: { is_master: false })
      .where('spree_variants.cost_price IS NOT NULL')
      .where('spree_variants.total_on_hand > 0')
      .distinct
  end
  
  def calculate_product_score(product)
    margin_score = calculate_margin_score(product)
    conversion_score = calculate_conversion_score(product)
    inventory_score = calculate_inventory_score(product)
    competitive_score = calculate_competitive_score(product)
    
    margin_score * conversion_score * inventory_score * competitive_score
  end
  
  def calculate_margin_score(product)
    avg_margin = product.variants.average('(price - cost_price) / price') || 0
    
    # Normalise to 0.5 to 2.0 range
    # 20% margin = 1.0, 40% = 1.5, 60% = 2.0, 10% = 0.75
    (avg_margin * 2.5 + 0.5).clamp(0.5, 2.0)
  end
  
  def calculate_conversion_score(product)
    rate = ConversionMetrics.rate_for_product(product.id, days: 30) || 0.02
    
    # Normalise: 2% = 1.0, 4% = 1.5, 1% = 0.75
    (rate * 25 + 0.5).clamp(0.5, 2.0)
  end
  
  def calculate_inventory_score(product)
    total_stock = product.variants.sum(:total_on_hand)
    weekly_velocity = SalesVelocity.product_weekly_average(product.id) || 1
    weeks_of_stock = total_stock.to_f / weekly_velocity
    
    case weeks_of_stock
    when 0..1 then 0.5    # Low stock, reduce budget
    when 1..2 then 0.8
    when 2..4 then 1.0    # Normal
    when 4..8 then 1.3    # High stock, increase budget
    else 1.5              # Very high, push hard
    end
  end
  
  def calculate_competitive_score(product)
    # Check if we are price competitive
    competitive_products = 0
    total_variants = 0
    
    product.variants.each do |variant|
      lowest_competitor = CompetitorPrice.lowest_for_variant(variant.id)
      next unless lowest_competitor
      
      total_variants += 1
      competitive_products += 1 if variant.price <= lowest_competitor.total_price * 1.05
    end
    
    return 1.0 if total_variants.zero?
    
    competitiveness = competitive_products.to_f / total_variants
    
    # Scale: 100% competitive = 1.2, 50% = 1.0, 0% = 0.7
    (competitiveness * 0.5 + 0.7).clamp(0.7, 1.2)
  end
  
  def apply_google_allocation(allocation)
    allocation.each do |product_id, budget|
      campaign_id = GoogleAdsCampaignMapping.find_by(product_id: product_id)&.campaign_id
      next unless campaign_id
      
      GoogleAdsApi.update_campaign_budget(
        campaign_id: campaign_id,
        daily_budget: (budget / 30.0).round(2) # Monthly to daily
      )
    end
  end
  
  def apply_meta_allocation(allocation)
    allocation.each do |product_id, budget|
      adset_id = MetaAdsetMapping.find_by(product_id: product_id)&.adset_id
      next unless adset_id
      
      MetaAdsApi.update_adset_budget(
        adset_id: adset_id,
        daily_budget: (budget / 30.0 * 100).round # Meta uses cents
      )
    end
  end
  
  def log_allocation(allocation)
    BudgetAllocationLog.create!(
      platform: @platform,
      total_budget: @total_budget,
      allocation_data: allocation,
      calculated_at: Time.current
    )
  end
end

The API integrations

Google Ads API wrapper:

# app/services/google_ads_api.rb
class GoogleAdsApi
  include Singleton
  
  def self.method_missing(method, *args, **kwargs, &block)
    instance.send(method, *args, **kwargs, &block)
  end
  
  def initialize
    @client = Google::Ads::GoogleAds::GoogleAdsClient.new do |config|
      config.client_id = Rails.application.credentials.google_ads[:client_id]
      config.client_secret = Rails.application.credentials.google_ads[:client_secret]
      config.refresh_token = Rails.application.credentials.google_ads[:refresh_token]
      config.developer_token = Rails.application.credentials.google_ads[:developer_token]
      config.login_customer_id = Rails.application.credentials.google_ads[:manager_id]
    end
    @customer_id = Rails.application.credentials.google_ads[:customer_id]
  end
  
  def update_campaign_budget(campaign_id:, daily_budget:)
    # Get current campaign to find budget resource
    campaign = get_campaign(campaign_id)
    budget_resource = campaign.campaign_budget
    
    operation = @client.operation.update_resource.campaign_budget do |budget|
      budget.resource_name = budget_resource
      budget.amount_micros = (daily_budget * 1_000_000).to_i
    end
    
    @client.service.campaign_budget.mutate_campaign_budgets(
      customer_id: @customer_id,
      operations: [operation]
    )
    
    Rails.logger.info("Updated Google campaign #{campaign_id} budget to #{daily_budget}")
  rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e
    Rails.logger.error("Google Ads API error: #{e.message}")
    Alerting.notify("Google Ads budget update failed", campaign_id: campaign_id, error: e.message)
    raise
  end
  
  def get_campaign(campaign_id)
    query = <<~QUERY
      SELECT campaign.id, campaign.name, campaign.campaign_budget
      FROM campaign
      WHERE campaign.id = #{campaign_id}
    QUERY
    
    response = @client.service.google_ads.search(
      customer_id: @customer_id,
      query: query
    )
    
    response.first&.campaign
  end
  
  def get_campaign_performance(campaign_id, days: 30)
    query = <<~QUERY
      SELECT
        campaign.id,
        metrics.impressions,
        metrics.clicks,
        metrics.conversions,
        metrics.conversions_value,
        metrics.cost_micros
      FROM campaign
      WHERE campaign.id = #{campaign_id}
        AND segments.date DURING LAST_#{days}_DAYS
    QUERY
    
    @client.service.google_ads.search(
      customer_id: @customer_id,
      query: query
    ).map(&:to_h)
  end
end

Meta Ads API wrapper:

# app/services/meta_ads_api.rb
class MetaAdsApi
  include Singleton
  
  BASE_URL = 'https://graph.facebook.com/v18.0'.freeze
  
  def self.method_missing(method, *args, **kwargs, &block)
    instance.send(method, *args, **kwargs, &block)
  end
  
  def initialize
    @access_token = Rails.application.credentials.meta_ads[:access_token]
    @ad_account_id = Rails.application.credentials.meta_ads[:ad_account_id]
  end
  
  def update_adset_budget(adset_id:, daily_budget:)
    response = connection.post("#{adset_id}") do |req|
      req.body = {
        daily_budget: daily_budget,
        access_token: @access_token
      }
    end
    
    result = JSON.parse(response.body)
    
    if result['success']
      Rails.logger.info("Updated Meta adset #{adset_id} budget to #{daily_budget}")
    else
      raise MetaApiError, result['error']['message']
    end
  rescue Faraday::Error, MetaApiError => e
    Rails.logger.error("Meta Ads API error: #{e.message}")
    Alerting.notify("Meta Ads budget update failed", adset_id: adset_id, error: e.message)
    raise
  end
  
  def get_adset_insights(adset_id, days: 30)
    response = connection.get("#{adset_id}/insights") do |req|
      req.params = {
        fields: 'impressions,clicks,conversions,spend,purchase_roas',
        date_preset: "last_#{days}_days",
        access_token: @access_token
      }
    end
    
    JSON.parse(response.body)['data']
  end
  
  def update_product_price_in_catalog(product_id, price, currency: 'GBP')
    response = connection.post("#{product_id}") do |req|
      req.body = {
        price: "#{(price * 100).to_i} #{currency}",
        access_token: @access_token
      }
    end
    
    JSON.parse(response.body)
  end
  
  private
  
  def connection
    @connection ||= Faraday.new(url: BASE_URL) do |f|
      f.request :url_encoded
      f.adapter Faraday.default_adapter
    end
  end
  
  class MetaApiError < StandardError; end
end

The orchestration job

Bring it all together with a scheduled job:

# app/jobs/pricing_and_budget_optimisation_job.rb
class PricingAndBudgetOptimisationJob < ApplicationJob
  queue_as :critical
  
  def perform
    Rails.logger.info("Starting pricing and budget optimisation cycle")
    
    # Step 1: Sync competitor prices
    sync_competitor_prices
    
    # Step 2: Recalculate all dynamic prices
    recalculate_prices
    
    # Step 3: Sync prices to ad platforms
    sync_prices_to_platforms
    
    # Step 4: Reallocate budgets
    reallocate_budgets
    
    # Step 5: Log the cycle
    log_optimisation_cycle
    
    Rails.logger.info("Completed pricing and budget optimisation cycle")
  end
  
  private
  
  def sync_competitor_prices
    CompetitorPriceSyncJob.perform_now
  end
  
  def recalculate_prices
    variants_to_update = Spree::Variant
      .joins(:product)
      .where(spree_products: { dynamic_pricing_enabled: true })
      .where.not(cost_price: nil)
    
    updated_count = 0
    
    variants_to_update.find_each do |variant|
      engine = PricingEngine.new(variant)
      new_price = engine.calculate_optimal_price
      old_price = variant.price
      
      if (new_price - old_price).abs > 0.01
        variant.update!(price: new_price)
        
        PriceChangeLog.create!(
          variant: variant,
          old_price: old_price,
          new_price: new_price,
          reason: 'automated_optimisation',
          competitor_context: engine.competitor_context.to_json
        )
        
        updated_count += 1
      end
    end
    
    Rails.logger.info("Updated #{updated_count} variant prices")
  end
  
  def sync_prices_to_platforms
    # Update Google Merchant Center
    Spree::Variant.where(updated_at: 1.hour.ago..).find_each do |variant|
      GoogleMerchantApi.update_price(
        offer_id: variant.sku,
        price: variant.price,
        currency: 'GBP'
      )
    end
    
    # Update Meta Product Catalog
    Spree::Variant.where(updated_at: 1.hour.ago..).find_each do |variant|
      meta_product_id = MetaProductMapping.find_by(variant_id: variant.id)&.meta_product_id
      next unless meta_product_id
      
      MetaAdsApi.update_product_price_in_catalog(meta_product_id, variant.price)
    end
  end
  
  def reallocate_budgets
    # Get current monthly budget from settings
    google_budget = AdBudgetSetting.find_by(platform: 'google')&.monthly_budget || 10000
    meta_budget = AdBudgetSetting.find_by(platform: 'meta')&.monthly_budget || 5000
    
    BudgetAllocator.new(total_budget: google_budget, platform: :google).apply_allocation!
    BudgetAllocator.new(total_budget: meta_budget, platform: :meta).apply_allocation!
  end
  
  def log_optimisation_cycle
    OptimisationCycleLog.create!(
      completed_at: Time.current,
      prices_updated: PriceChangeLog.where(created_at: 1.hour.ago..).count,
      budget_reallocations: BudgetAllocationLog.where(calculated_at: 1.hour.ago..).count
    )
  end
end

Schedule it in your Sidekiq cron or whenever gem:

# config/schedule.rb (whenever gem)
every 1.hour do
  runner "PricingAndBudgetOptimisationJob.perform_later"
end

# Or sidekiq-cron
Sidekiq::Cron::Job.create(
  name: 'Pricing and Budget Optimisation',
  cron: '0 * * * *', # Every hour
  class: 'PricingAndBudgetOptimisationJob'
)

Measuring success

Once this system is running, you need to track whether it is working. Key metrics:

Gross margin percentage over time. Should increase as pricing becomes more intelligent.

Revenue per session. Should increase as you capture more willingness to pay.

POAS (Profit on Ad Spend). Should increase as budget flows to higher margin products.

Competitive win rate. Percentage of products where your price is at or below market.

Price change frequency. How often prices are adjusting (too static means the system is not reacting; too volatile means it is overreacting).

Build a dashboard that tracks these over time and correlates them with system changes.

The shift in mindset

Let me close with why this matters beyond the technical implementation.

Most marketers think of their job as crafting messages and placing ads. The price is someone else's problem. The inventory is someone else's problem. The margin is finance's problem.

This is why marketing often feels disconnected from business results. You can run brilliant campaigns that drive revenue on products that lose money. You can optimise for conversions while competitors eat your margin with better pricing.

The system I have described here puts marketing in the driver's seat of business optimisation. Price, advertising, inventory, margin are all levers you control. They all talk to each other. They all optimise together.

This is not passive. You are not setting parameters and hoping. You are running an algorithm that continuously finds the profit maximising configuration across all these variables.

This is what modern growth engineering looks like. This is why I build these systems. This is the game.

Want help building a dynamic pricing and algorithmic marketing system for your business? I architect and implement these systems for ecommerce companies, integrating competitor intelligence, margin-aware budget allocation, and real time pricing optimisation. Get in touch to discuss what is possible for your situation.

Related reading

Ready to move from passive marketing to algorithmic profit optimisation? I build dynamic pricing systems, competitor intelligence pipelines, and margin-aware advertising allocation for ecommerce companies. Get in touch to discuss how to turn your marketing into a profit maximisation engine.