Multi-axis earning: why a single loyalty point kills the programme before it ships
This is the second post in Hospitality Loyalty Brief. The first explained why you'd run a retention programme. This one explains how to design one that doesn't collapse six months after soft-launch.
The original sin: "one rouble equals one point"
Most hospitality operators copy the retail-and-banking pattern: every spent unit earns one point; accumulate enough — redeem a night. Clear, measurable, easy to pitch.
And catastrophically wrong for a hotel.
Three reasons, all about the physics of the hospitality business:
- A stay is one transaction, but many behaviours. The guest doesn't just pay for a room — they stay three nights, eat in your restaurant, visit your spa, leave a review, refer friends. "One rouble = one point" sees only the final bill.
- ADR varies 5-8× across segments. A studio at €60 and a suite at €380 — inside one programme. A linear scale turns the programme into segregation: a premium guest earns 6× more for the same behaviour, while a mass-market guest accrues points faster than their true LTV contribution.
- Seasonality breaks accrual. A summer-peak resort: a guest in August earns 1 800 points for a week, in February only 400 for the same week. Same behaviour, different "income" inside the programme — feels arbitrary, not fair.
4-axis earning: what fires simultaneously
In TTE.Loyalty every transaction (PMS event) hits four independent axes at once. The guest doesn't pick "how to count" — the system writes to all of them.
Axis 1 · Monetary
Money spent on property. Segmented across 6 revenue categories: room / F&B / spa / ancillary / retail / meetings. Each has its own tier-multiplier — because F&B revenue costs less to deliver than room revenue. That's reality, and accrual should reflect it.
monetary_points = Σ(category_revenue × category_multiplier × tier_bonus)
Example: a Silver member stays 3 nights (room €260, F&B €45, spa €72).
Multipliers: room=1.0, F&B=1.4, spa=1.6. Silver tier-bonus +15%.
points = (260×1.0 + 45×1.4 + 72×1.6) × 1.15 ≈ 511.
Axis 2 · Volume
Number of nights and stays — not amount, but count. This is what builds habit. A guest who stays 12 times for one night is more valuable for retention than a guest with one 12-night trip at the same money-spend: more touches, more upsell windows, clearer preference signal.
volume_points = stays × stay_bonus + nights × night_bonus
Example: those 3 nights = 1 stay = 3 nights × 100 + 1 stay × 800 = 1 100 points.
Axis 3 · Frequency
Not "how much" — "how often". This axis rewards the return pattern: a guest who stays every quarter earns a frequency bonus that grows non-linearly with the streak.
frequency_points = days_since_last_stay < THRESHOLD ? streak_bonus(streak) : 0
Retail doesn't use this axis — because retail visit frequency is measured in days, hotel frequency in quarters. Without a frequency axis a programme can't tell a "loyal" guest from a "one-off big spender".
Axis 4 · Engagement
Actions not tied directly to a purchase: left a review, filled out their preferences profile, brought a friend through referral, shared a photo with your tag, answered a post-stay survey. This is the currency of future revenue — engagement behaviour correlates with repeat-rate at 0.6-0.7 in McKinsey hospitality data.
engagement_points = Σ(action × action_weight × decay_factor)
Example: a 5⭐ review with photo (+1 200), filled profile (+400), referral leading to an actual stay (+5 000). These 6 600 points aren't tied to any single transaction — they're the guest doing work for your hotel, and the programme should recognise that.
Tier qualification: 5 types, not one
Same logic for moving between tiers (Silver → Gold → Platinum). A single spend-threshold kills a programme around month 18: "premium one-off spender" guests stay stuck on Gold without moving, and loyal "frequent low-spenders" never reach Silver.
The alternative: qualify via any of 5 channels:
- Spend-quals — total annual spend
- Stay-quals — number of stays per year
- Night-quals — number of nights per year
- Composite-quals — weighted combination (spend × stays)
- Behavioural-quals — for guests who cross an engagement-actions threshold
A guest advances to the next tier the moment they hit any of the five. The resort operator picks which three or four to enable — and that's just tenant-level config in the admin.
Why all of this fits in one transaction
The core architectural decision in TTE.Loyalty: all four axes and all five quals recompute inside a single DB transaction when the PMS webhook arrives with a new stay.
tenantTransaction(tenantId, async (tx) => {
const monetary = computeMonetaryAxis(stay, tx)
const volume = computeVolumeAxis(member, stay, tx)
const frequency = computeFrequencyAxis(member, stay, tx)
const engagement = computeEngagementAxis(member, tx)
await applyPoints(member, [monetary, volume, frequency, engagement], tx)
await recheckTierQuals(member, tx)
await writeAudit(tx, { actorType: 'system', action: 'earn' })
})
Which means: either every accrual and tier-check went through — or nothing did. No "half-earns" on failure. No guests "accrued in one system, tier in another".
What the operator gets
- The programme reflects real economics. A high-engagement, low-spend guest gets recognition — because they'll do more for retention than a "premium one-off".
- Tier-mix doesn't collapse. The distribution across levels stays diversified, and the programme feels fair across the whole guest spectrum.
- The seasonality problem is solved. The frequency axis rewards off-season returns — the programme itself nudges guests towards the dates you need.
- Audit-trail on everything. Each accrual is its own row in append-only audit_log with a reason. No more "where did these points come from".
Next
The next post will be on tier-design: how to calibrate thresholds so Silver guests don't "get stuck" and the tier distribution stays stable for 18+ months.