Before ranking, remove expired, ineligible, unavailable, non-consented or below-margin actions. Apply contact-frequency rules across channels and across offer, price and retention workflows. Recheck fast-changing constraints just before fulfilment. A model score cannot override those rules.
For a campaign across customers i, a simplified allocation problem is:
maximize sum_i delta(x_i, a_i)
subject to sum_i expected_incentive_cost(x_i, a_i) <= campaign_budget
a_i is eligible for customer i
at most one selected action per opportunityWhen costs and values differ, picking each customer's best offer on its own can exceed the budget. Sorting by benefit/cost is a useful heuristic in some settings, not an exact general solution for several actions. If this matters, compare a feasible heuristic with a small integer-programming solution on a representative batch. Expected spending also does not cap realised spending, so use reservations and redemption limits for hard financial commitments.
For synchronous serving, reserve budget atomically before returning an offer. Otherwise concurrent requests can promise the same remaining money. Use a shared contact ledger to stop duplicate sends. Apply eligibility and resource constraints before the final randomised assignment, and log the probabilities of the mechanism you really ran. If a later reservation or delivery fails, keep the original assignment and record the executed action and failure separately. Do not invent a propensity for the replacement or give it the original probability. Intention-to-treat then measures assignment under your real delivery process. A delivered-only comparison needs extra identification. The serving flow puts this into practice.