How a card expires and reconciles
Two slow, unglamorous jobs keep a gift card ledger honest over time. The first is expiry: cards that have sat untouched past the policy window need retiring, and the leftover balance booked as breakage, cleanly and on a schedule rather than by hand. The second is reconciliation: at month-end the business needs one trustworthy number for how much it still owes in live cards. This post walks through the nightly expiry sweep and the monthly reconciliation that re-derives every balance from the immutable ledger and proves the books still add up to the cent.
Key takeaways
- A nightly EventBridge schedule sweeps for cards dormant past the policy window and retires them, booking the leftover balance as breakage.
- Expiry is itself an atomic conditional write, so a card being spent at the same instant is never wrongly expired.
- A monthly schedule re-derives every balance from the immutable ledger and checks it against the stored figure.
- The reconciliation proves one identity: opening liability + issued − redeemed − expired = closing liability, to the cent.
- Bedrock writes a short plain-English narrative on top of the finished numbers — it never computes or touches a figure.
Retiring a dormant card
Gift cards do not get spent down to zero and closed off neatly. A good fraction are partly used and then forgotten — the $50 card with $31.60 still on it that nobody ever brings back. Left alone, those balances sit on the books as money owed forever, and the outstanding-liability figure slowly loses meaning. Where local law allows it, the policy sets an expiry window — commonly 24 months with no activity — after which a dormant card is retired and its leftover balance is recognised as breakage: value that was sold but will never be redeemed.
A nightly EventBridge schedule runs the expiry sweep at a quiet hour. It queries for cards whose last-activity timestamp is older than the window and whose status is still active, and retires each one. Crucially, retiring a card is not a blind overwrite — it is an atomic conditional write, the same tool as everywhere else. The sweep says “set this card to expired and book the balance as breakage, only if the last-activity timestamp is still the old one I read.” If the customer happened to spend the card a second before the sweep touched it, the timestamp has moved, the condition fails, and the card is left active — their money is safe. No race between a redemption and the sweep can ever expire a card that was just used.
Each expiry appends a row to the ledger — an expired event with the breakage amount — so the history stays complete and the monthly reconciliation can account for every cent that left the live pool.
Proving the books once a month
The balance on a card row is fast to read, but “fast” is not “trusted.” The trusted number is the one you can rebuild from scratch. Once a month a second EventBridge schedule runs the reconciliation, which re-derives every card’s balance from the immutable ledger — opening row plus every redemption minus, every expiry out — and checks the rebuilt figure against the balance stored on the card row. If a single card disagrees, the report names it rather than papering over it. In normal running they always agree, because every write that moved a balance also appended its ledger row in the same step.
The one identity that must hold
The whole reconciliation comes down to a single equation that has to balance to the cent:
opening liability + issued − redeemed − expired = closing liability
Each term is summed straight from the ledger over the month. Opening is last month’s closing figure. Issued is every opening row — the new money the business took on. Redeemed is every redemption row — money handed back as goods. Expired is every breakage row — money recognised as never coming back. Closing is what the business still owes in live cards. A worked month: opening $8,420.00, issued $3,000.00, redeemed $2,610.40, expired $190.00, and so closing $8,619.60. If the rebuilt closing figure does not equal the sum of the live card balances, the report flags the gap loudly — that mismatch is the single most important alarm in the system, because it means a balance moved without a matching ledger row, and the books can no longer be trusted until it is explained.
Words on top of numbers
By the time the report is assembled, every figure is final and was computed in plain Python over integer cents. The last step hands those finished numbers to a single Bedrock Haiku call that writes a short, readable narrative for the owner: how many cards were issued and redeemed, how much was recognised as breakage, how the outstanding liability moved, and anything the report flagged. The model only ever phrases numbers it is given; it never adds them up, never reconciles, and never touches a balance. Switch Bedrock off and every figure in the report is identical — you simply lose the paragraph of prose. The report and a CSV export are written to S3 (versioned, so each month’s close is preserved) and emailed to the owner through SES.
Design rules for expiry and reconciliation
- Expiry is a scheduled job, and each retirement is an atomic conditional write guarded on last-activity — a just-spent card is never wrongly expired.
- Breakage is recognised explicitly and appended to the ledger, never quietly dropped.
- The reconciliation re-derives every balance from the immutable ledger, not from the live row it is checking.
- One identity must hold to the cent: opening + issued − redeemed − expired = closing. A mismatch is the loudest alarm in the system.
- Bedrock writes the narrative on top of finished numbers — it never computes a figure.