Offer Optimization SDK
What is the least this customer would accept?
A weekend promo is ready at 15% off produce, and finance wants the discount smaller without losing redemptions. Ask each customer's twin what the cheapest version they would still take is — 8% for one, a lower minimum spend for another, nothing at all for a third — and send each of them that, instead of one blanket 15%.
Across a re-priceable campaign this removes 6–15% of the committed discount, holding acceptance fixed. The study behind it is written up in full on the research pages.
Quick start
from eggai import OfferOptimizer optimizer = OfferOptimizer(api_key) terms = optimizer.cheapest_accepted( customer = "c_8813", profile = "shops fortnightly, small baskets, " "coupon-led, buys confectionery", offer = "Fresh Picks Weekend — 15% off " "produce, min spend 500", guidelines = """ discount caps at 12%, min spend above 300. the produce section is fixed. the tagline may be reworded, but never promise free delivery. """, ) terms.text # "8% off produce, min spend 349" terms.tagline # "Weekend Picks" terms.changed # ["discount", "min_spend", "tagline"] terms.objection # "threshold above my usual basket"
import { OfferOptimizer } from '@eggai-sdk/core'; const optimizer = new OfferOptimizer(apiKey); const terms = await optimizer.cheapestAccepted({ customer: 'c_8813', profile: 'shops fortnightly, small baskets, coupon-led, buys confectionery', offer: 'Fresh Picks Weekend — 15% off produce, min spend 500', guidelines: `discount caps at 12%, min spend above 300. the produce section is fixed. the tagline may be reworded, but never promise free delivery.`, }); terms.text; // "8% off produce, min spend 349" terms.tagline; // "Weekend Picks" terms.changed; // ["discount", "min_spend", "tagline"] terms.objection; // "threshold above my usual basket"
How a run works
A run is two models taking turns. Offerwright drafts the terms and the words together, opening at the stingiest version your guidelines allow; the twin answers as this one customer — yes or no, how sure, and which condition it objected to. Each round the proposer moves only the lever the twin objected to. Neither model sees the other's reasoning.
The loop stops the moment the twin accepts, and returns the cheapest version that got there. If
the guidelines run out first, the run returns no offer at all
(accepted == False) rather than one that costs more than the customer is worth.
Objectives
| Objective | The search |
|---|---|
saving (default) | The offer already lands — find the cheapest version this customer still accepts. |
winback | The customer has lapsed — find any offer inside the guidelines they would take at all, cheapest first. |
Batch runs
Pass rows instead of a single customer — one dict per customer, offers per row or
one campaign for the file. Free-text offers are parsed server-side; structured terms
({depth, min_spend, category, validity, subject}) are accepted as-is.
run = optimizer.cheapest_accepted(
rows = [
{"id": "c_8813", "profile": "fortnightly, coupon-led",
"offer": "15% off produce, min 500"},
{"id": "c_2291", "profile": "weekly family shop",
"offer": "15% off produce, min 500"},
],
objective = "saving",
guidelines = "discount caps at 12%",
constraints = {"max_depth": 12, "min_spend_min": 300},
)
run.accepted # the rows worth sending
run["c_8813"].text # lookup by customer id
for terms in run: # or iterate in input order
...
const run = await optimizer.cheapestAccepted({ rows: [ { id: 'c_8813', profile: 'fortnightly, coupon-led', offer: '15% off produce, min 500' }, { id: 'c_2291', profile: 'weekly family shop', offer: '15% off produce, min 500' }, ], objective: 'saving', guidelines: 'discount caps at 12%', constraints: { max_depth: 12, min_spend_min: 300 }, }); run.accepted; // the rows worth sending run.get('c_8813').text; // lookup by customer id for (const terms of run) { ... } // or iterate
max_depth, min_depth,
min_spend_min, min_spend_max) are numeric backstops enforced by the
harness itself, whatever the models do. Use both: guidelines for intent, constraints for the
numbers you cannot breach.The result object
| Field | Meaning |
|---|---|
accepted | True if some version inside your guidelines got a
yes. False → send nothing; every field below except objection is empty. |
text | The cheapest accepted terms, as words — "8% off produce, min spend 349". |
tagline | The offer message that goes with those terms. |
terms | The same, structured: {depth, min_spend, category,
validity, subject}. |
changed | Which levers moved from the original: discount,
min_spend, category, validity, tagline. |
objection | The twin's objection to the offer as written — why the original needed changing at all. |
p | The twin's acceptance probability for the returned terms. |
steps | The full ladder: every proposal, its probability, the twin's words, accepted or not. |
raw | The untouched server row, everything above included. |
Verify it worked
Runs started from the EGGai app split a 5% holdout before scoring — customers who keep the original offer — so the saving is measured against a control, not asserted. The app's verification screen scores both groups when your redemption file comes back. SDK jobs return every row; keep your own control group if you need one.