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:
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:
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:
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:
Rearranging to find the optimal price:
For a product with cost £20 and elasticity -2:
For the same product with elasticity -4 (more price sensitive):
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:
- Crawls competitor sites on a schedule (daily, hourly, or more frequently for high velocity categories)
- Matches competitor products to your catalog (by SKU, UPC, or fuzzy matching on name and attributes)
- Stores historical pricing data to identify patterns and seasonality
- 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:
Or equivalently:
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:
- Product margin: Higher margin products get more budget, all else equal
- Conversion rate: Products that convert better get more budget
- Inventory level: Overstocked products get budget boost, understocked products get budget reduction
- Competitive position: Products where you are price competitive get more budget than products where competitors undercut you
The allocation formula:
Where the score for product is:
Inventory factor might be:
Competitive factor might be:
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:
- A Solidus ecommerce installation
- A competitor crawler that feeds data to a
CompetitorPriceApiclass - 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.