Part 4 of 7 · Referral tracker series ~8 min read

How a reward gets issued

The reward is the whole point, and the one place a mistake costs real money — a double payout, or a reward for a purchase that never happened. This post is about issuing it safely: how a conversion is confirmed, how both sides are credited exactly once even if the webhook fires twice, and how the thank-you gets written without letting a model near the ledger.

Key takeaways

  • The reward is triggered by a conversion webhook — a genuine first purchase — not by a click or a sign-up.
  • The pending referral is flipped to rewarded with a conditional write, so a webhook that fires twice can only pay once.
  • Both sides are credited in the same step: the referrer’s reward and the friend’s, each written to the ledger exactly once.
  • The actual crediting is done deterministically through the store’s API; one Bedrock call only writes the two thank-you notes.
  • Anything the fraud screen holds, or a conversion for an already-rewarded referral, is never paid — it stops or goes to a person.

The one step that spends money

Everything until now has been careful bookkeeping: a code claimed, a click logged, a pending referral opened. None of it cost anything. This step is different, because this is where real value leaves the business — £10 of credit to the referrer, £10 off the friend’s box — and it’s the one place a mistake is expensive in both directions. Pay twice and you’ve handed out money you can’t claw back; pay for a purchase that never really happened and you’ve rewarded a phantom. So the reward step is built around a single obsession: credit both sides exactly once, and only for a conversion that’s genuinely real.

The trigger is the conversion webhook. When the referred friend makes their first real purchase — Mara’s first FreshBox actually ships and is paid for, the barber’s new customer pays for their first cut, the window cleaner’s new sign-up settles their first clean — the store fires a webhook to the same Function URL that handles everything else. “First real purchase” is the store’s call, not the model’s: a paid, non-refunded order, past any cooling-off the business sets, so a sign-up that buys and immediately cancels never trips it.

Exactly once, both sides

Webhooks are famously unreliable in one specific way: they arrive more than once. A network hiccup, a retry the store thinks failed, a checkout that double-fires — any of these can deliver the same conversion two or three times. If the reward step simply paid out each time it was called, a flaky network would become free money. So the very first thing it does is claim the payout with a conditional write: flip this referral from pending to rewarded, but only if it’s currently pending. The first webhook wins the flip and proceeds to pay; any later copy finds the referral already rewarded, the conditional write fails, and it stops without paying a second time. Exactly-once isn’t a hope here, it’s a property of that one atomic write.

Only after winning the flip does the step credit the two sides, and it treats them as one unit of work. The referrer’s reward and the friend’s reward are each written to a rewards ledger keyed so that a given referral can only ever record one credit per side. The ledger is the source of truth for “has this been paid?” — separate from the store’s own view — so even if fulfilment through the store has to be retried, the ledger keeps the count honest. Both sides, together, once.

Issuing a reward: a conversion webhook, an exactly-once flip, both sides credited, and one Bedrock thank-you On the left, an external box “Store fires conversion webhook”, a genuine first purchase, sends an arrow into a box “Rewarder Lambda” inside a dotted AWS account container. The rewarder first performs a conditional write on the DynamoDB referrals table: flip this referral from pending to rewarded only if it is still pending. If the flip fails because the referral is already rewarded or was held by the fraud screen, an arrow goes to a box “Stop, no double-pay”. If the flip succeeds, the flow continues to a box “Screen check” that consults the fraud rules; a fail routes to “Hold for a person”. On a pass, two parallel arrows go to a box “Credit both sides” that writes one entry per side to a DynamoDB rewards ledger and calls the store API to fulfil the referrer credit and the friend discount. From there an arrow goes to a Bedrock box labelled Claude Haiku 4.5, which writes two short thank-you notes, and then to a box “Send thank-you” via SNS SMS or SES email to each side. A note reads: the flip makes the payout exactly once; the model writes the thank-you but never touches the ledger. Conversion first purchase, webhook AWS account Rewarder Lambda flip pending → rewarded DynamoDB referrals conditional flip Stop, no double-pay already Screen check fraud rules Hold for a person fail Credit both sides ledger + store API, one per side pass DynamoDB rewards ledger, once/side Bedrock two thank-you notes phrase Send thank-you SNS / SES, each side The flip makes the payout exactly once; the model writes the thank-you but never touches the ledger.
Fig 4. Issuing a reward. A conversion webhook flips the referral from pending to rewarded with a conditional write — a duplicate can’t pay twice. On a clean screen, both sides are credited once through the ledger and the store, and one Bedrock call writes the two thank-you notes that go out by SMS or email.

The model writes the thanks, not the cheque

Once both sides are credited, the last thing the step does is say thank you, and this is the only place a model runs at conversion time. A single Bedrock Haiku 4.5 call is handed the facts it may use — the two first names, the business voice, and the reward each side actually received — and writes two short notes: one to the referrer (“Mara just made her first order — your £10 is on your next box, thanks for the recommendation”) and one to the friend (“welcome to FreshBox, your £10 has been applied”). Each goes out by whatever channel suits — SMS through SNS where a mobile is the contact, email through SES otherwise. The reward amounts in those notes come from the ledger the code already wrote, not from the model; Bedrock is told them so the wording is accurate, but it doesn’t get to decide them.

That line matters more than it looks. The model never touches the ledger, never calls the store’s API, never sees a total it could round up. It writes words about a payout that has already, deterministically, happened. If the Bedrock call is slow or the draft is off, a fixed thank-you template goes out instead — the credit is already applied, so a plain note is no loss. The warmth is nice to have; the accuracy is non-negotiable, and keeping them on opposite sides of that fence is what makes it safe to let a model near a system that moves money at all.

The payouts that don’t happen

Plenty of conversions arrive that shouldn’t pay, and the reward step is where they’re quietly turned away. A duplicate webhook loses the conditional flip and stops. A conversion for a referral the fraud screen has already held — a suspected self-referral, a code caught spraying — never gets past the screen check, and lands in front of a person instead of the ledger (that’s Part 5). A conversion for a friend who was never attributed has no pending referral to flip at all, so there’s simply nothing to pay. And a refund that arrives after a reward doesn’t silently reverse the credit; it’s flagged for a person, because clawing money back from a customer’s account is exactly the sort of decision that should never be automatic. In every one of these cases the default is the same: when in doubt, don’t pay, and tell a human why.

Design rules that shaped the reward

  • Only a conversion pays. A genuine, paid, non-refunded first purchase is the trigger — never a click or a sign-up.
  • The flip is the guarantee. One conditional write from pending to rewarded makes the payout exactly once, however many times the webhook fires.
  • Both sides, one unit. The referrer’s and the friend’s credits are written together, one entry per side, to a ledger that is the source of truth.
  • Code credits, the model thanks. The store API and ledger do the crediting; Bedrock only writes the thank-you notes.
  • The model never sees the ledger. Amounts come from the record; the model is told them for wording and decides nothing.
  • When in doubt, don’t pay. Held, unattributed, duplicate, or post-refund conversions stop or go to a person — never an automatic payout.
All posts