Start with one bounded decision and one settled business result. That is the whole first job. BanditBuzz is built so the first live choice creates the learner. You do not register options ahead of time, design an experiment, or wait for a warehouse project. You ask for a choice when a person is in front of you, you record what later happened, and a nightly job closes the loop.
This post is the implementer's path. It sticks to the current public contract: POST /decide, POST /events, retries, a client-side deadline, reward windows, and identity that does not require the same identifier on both calls. Scheduled push delivery and richer per-person context come after that base loop is honest.

Setup screen for the two-call loop. The view uses fixed demo data, not a live account.
What you are building
A useful learning loop has four parts you control and a few parts BanditBuzz owns.
You choose:
- A decision name, such as
customer_winback. - A flat list of approved action strings, including
nothingwhen "act or wait" is part of the question. - A reward definition: which event counts as success, and how many whole days after the choice it may arrive.
- How your product maps each returned choice into something real (a template, offer code, or no send).
BanditBuzz chooses among those options with Thompson sampling in its Beta-Bernoulli form: each option keeps a conversion posterior, new options start at Beta(1, 1), and each call samples those posteriors and picks a winner. About 10% of stable person keys land in holdout for that decision-point version, so the scoreboard can report lift against a control instead of against activity. Live lift uses anytime-valid confidence sequences in the style of Maharaj et al.. Attribution, maturation, and posterior recounting run on a batch schedule, usually nightly. Delayed feedback is expected, not an error path; work on delay-adaptive learning such as Howson, Pike-Burke, and Filippi is one reason pending choices stay out of training until the result arrives or the window closes.
What you still own: execution, consent, content, and a local default if /decide is slow or unavailable. BanditBuzz has not published a load-tested latency number, an SLA, a rate limit, or a tested maximum option count. Treat the call as a remote dependency, not as part of checkout or login critical path.
Call 1: ask for a choice
Authenticate with a Bearer API key. The first call for a new decision name creates the decision point, its option posteriors, and the ledger row for that pick.
POST /decide
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"decision": "customer_winback",
"person": {
"email": "sam@x.com",
"time_zone": "America/Chicago"
},
"options": [
"10_percent_offer",
"new_arrivals",
"postcard",
"nothing"
],
"reward": {
"event": "purchase",
"within_days": 7
},
"idempotency_key": "winback-request-123"
}
A successful response looks like:
{
"choice": "10_percent_offer",
"decision_id": "d_8f2a0000-0000-0000-0000-000000000001",
"policy": "explore"
}
policy is one of explore, exploit, or holdout. Use choice as your execution key. Map it in your code to a template ID, journey branch, or no-op. Do not treat decision_id as the join key for rewards; it is a stable reference for optional feedback and for your own logs.
Rules that matter on day one:
- Options are strings in the request. A new string on a later call enters exploration automatically.
nothingis an ordinary option. Include it when "whether to act" is the real question.- Changing
reward.event,within_days, or an optionalvalue_propertycreates a new decision-point version with a fresh policy and a new holdout assignment. Old decisions stay tied to the rule they were made under. - Optional
options_metadatais stored for analysis. The policy does not read it. - Optional
value_propertyon the reward block switches the learner toward value when that property is present on events. Binary conversion remains the cleanest first goal.
Call 2: record the business result
When the person buys, renews, cancels, or otherwise settles the outcome you named in reward.event, send an event. You do not need a prior decide call for /events to accept the write, but for learning you need both sides of the same person to land in the ledger.
POST /events
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"person": {
"email": "sam@x.com"
},
"event": "purchase",
"value": 89.0,
"idempotency_key": "purchase-event-456"
}
The API responds with 202 and an event id:
{
"event_id": "e_31c00000-0000-0000-0000-000000000001"
}
Optional fields: properties (object), occurred_at (ISO timestamp; defaults to receipt time), and value (number). If you declared value_property on the decision point, BanditBuzz can take the numeric value from that property when attributing.
Send a settled result as the reward when the outcome can reverse. BanditBuzz does not unwind a reward for a later return or chargeback. An early "purchase started" signal is a weak teacher for a seven-day purchase goal.
Idempotency on both verbs
A connection can fail after BanditBuzz records a choice or event but before your client sees the response. Without an idempotency key, a blind retry can create a second decision or a second event.
Send idempotency_key on /decide and on /events. Reuse the same key for every retry of that logical request. BanditBuzz returns the first recorded choice or event id instead of inserting another row. Keys are scoped to your account.
Give each distinct logical request its own key. A new visit by the same person is a new decide key. A new purchase is a new event key. Retries of the same attempt reuse the old key.
Idempotency does not replace your local fallback. If the client never receives a response and abandons the call, your product still needs a default action. The key only prevents double-counting when you retry the same attempt.
Local deadline and fallback
Until BanditBuzz publishes latency and capacity numbers, keep /decide off the hot path for login, payment, or any flow that cannot wait. Set a short client deadline. On timeout, network error, or a non-success status, use a local default you have already approved: a fixed template, a random choice among a small safe set, or nothing.
Log that you used the fallback. Do not invent a fake decision_id. A fallback choice is not a BanditBuzz decision, so it should not enter your attribution story as if it were. When the API is healthy again, resume calling /decide for new opportunities.
Retries after a timeout should still carry the original idempotency_key if you are retrying the same attempt. If you already showed the local fallback to the person, do not retry into a second live choice for that same moment; you would create two policies for one experience.
Different identifiers on decide and events
Decide and record do not need the same identifier. You can decide with email and later send a purchase with phone and postal address. Matching uses normalized email, normalized phone, or a later resolved person id that both rows share. Attribution runs in batch. Late identity merges can still credit a decision when the event itself occurred inside the reward window.
What this means in practice:
- Send every identifier you already have at each moment. More identifiers improve match odds.
- Do not invent a session or correlation id just for BanditBuzz. The product does not require one.
- Expect unmatched rows. The product does not yet report identity-match lag or the unmatched share. Plan for some decisions to mature as zeros and some events to sit unmatched until identity catches up.
- Holdout assignment uses a stable person key (normalized email, then phone, then a hash of supplied identifiers). A person who later arrives under a different identifier can move sides for a new version. Keep identifiers as stable as your systems allow.
Reward windows and maturity
The reward block is not decoration. It is the training contract.
For a request-time /decide choice, within_days starts when the choice is recorded. An event must occur inside that window to attribute. A choice becomes a training zero only after the full window closes with no matching event. A choice that earns a reward matures as soon as attribution lands.
Pending choices do not count as failures. That is the delay-aware part of the design: young options are not punished for purchases that have not had time to arrive. Work such as Howson, Pike-Burke, and Filippi supports keeping delayed outcomes pending and updating the learner when results become available; BanditBuzz applies that idea with an explicit whole-day window and a batch update, not with a claimed finite-time regret bound for the production schedule.
When choices overlap for the same person, the latest eligible decision gets the reward. That is a clear product rule, not multi-touch measurement. If two emails can land in the same window, design the decision cadence so last-decision-wins is acceptable, or widen the business question so overlapping actions are rare.
Nightly (or on-demand) jobs attribute rewards, mature closed windows, and recount option posteriors from matured, non-holdout decisions. Learning therefore lags by the declared window plus batch lag. That is expected for a business-result loop. It is a poor fit if you need millisecond feedback from opens and clicks alone.
Put the choice to work
BanditBuzz returns a string. Your stack executes it.
A minimal mapping table is enough:
choice |
Your action |
|---|---|
10_percent_offer |
Trigger offer email template offer_10 |
new_arrivals |
Trigger browse template new_arrivals_v3 |
postcard |
Enqueue print job winback_postcard |
nothing |
Skip outbound for this opportunity |
Keep marketing tools in charge of content, consent, and delivery reports. Keep BanditBuzz in charge of which approved action (including no action) this person gets for this decision point. Do not let a second optimizer assign the same choice in parallel, or you will measure a mixed policy and neither control group will answer a clean question.
Optional: POST /decisions/{decision_id}/feedback stores an engagement signal such as { "event": "click" }. The current attribution and learning jobs do not read that feedback table. Prefer the business event named in reward as the teacher.
Read the scoreboard before you add complexity
Once decisions mature, open the decision's analysis view or call GET /decisions/{name}/analysis. You should see per-option matured counts and reward rates, lift versus holdout with a confidence range, and the explore / exploit / holdout mix over time.

Scoreboard for a win-back decision, including lift versus holdout. The view uses fixed demo data, not a live account.
Read the lift number as whole-policy effect against the automatic holdout, not as a causal effect between two options. Option rates are useful summaries; because the policy changes who receives each option, comparing those rates does not identify a fixed effect. The confidence range follows the anytime-valid approach used on the landing research for Maharaj et al.. Let the range, not the calendar, decide whether you have enough evidence to change the action list.
The free policy stays Thompson sampling over matured results. There is no exploration-rate knob. There are no traffic floors, ceilings, or pinned options. New options start at Beta(1, 1). Action learning keeps full history; the product does not yet auto-respond to seasonality or creative wear-out.
What the caller still owns
Be explicit in the integration design review:
- Client deadline and local default for
/decide. - Idempotency keys for every retried decide and event.
- Mapping from choice strings to real experiences.
- Consent, suppression, and unsubscribe handling in your tools.
- A settled reward event that matches the business goal.
- Monitoring for API errors and fallback rates on your side.
BanditBuzz stores decision rows, event rows, and person identifiers. Connected history and destinations move data according to what you connect. Keys rotate; fine-grained roles, scoped keys, a buyer-visible change audit log, and a full evaluation-log export are not in the product yet.
Limits you should not invent around
Do not assume or document:
- A public p50 or p99 latency, uptime SLA, rate limit, burst limit, or tested option-count limit. Those are listed as open gaps on the product site.
- Instant reward updates. Learning waits for windows and batch jobs.
- Multi-touch credit or reward reversal.
- Cross-decision-point exclusion. The same person can enter two decision points and affect both scoreboards.
- A full decision, propensity, context, and reward export for retrain-elsewhere workflows.
State those limits in your own runbooks so operators do not invent them under pressure.
Next steps after the base loop works
Only after /decide and /events are stable in production should you add more surface area.
Declare push with PUT /decisions/{name} when BanditBuzz should find eligible people, choose among the same options (including required nothing), respect cooldown and eligibility, and deliver actionable picks to a connected destination. Push shares one policy, holdout, and scoreboard with pull. Timing limits, when enabled, choose a local window inside days and hours you allow; the reward clock for a scheduled action starts only after the destination accepts release.
Connect history with a CSV or database source when you want more events than the API path alone will collect. The decide and events shapes stay the same.
Consider the paid contextual tier when replay on your own log shows that per-person context would help. Use a positive lower confidence bound as your decision rule; checkout does not enforce it today. The integration does not change: same verbs, same response shapes. Missing context or a reached monthly person cap falls back to the standard policy.
None of those steps replace the two-call loop. They sit on top of a decision name, an option list, a reward window, and honest event traffic.
A short checklist for the first ship
- Create an API key in the BanditBuzz app.
- Call
/decidewith a real decision name, approved options, a reward block, a person object, and an idempotency key. - Map
choicein your product; keep a local fallback behind a deadline. - Emit
/eventsfor settled results with whatever identifiers you have at conversion time, again with an idempotency key. - Wait for reward windows to mature; read the scoreboard against holdout.
- Change one thing at a time: options, reward definition, or audience behavior. A new reward definition versions the point; a new option does not.
That is the smallest useful learning loop: one live choice, one later business event, retries that do not double-count, a fallback you control, and a scoreboard that measures the policy against a holdout. Add push declaration and richer context only when that loop is already telling the truth.