diff --git a/SVSim.Database/Services/ICurrencySpendService.cs b/SVSim.Database/Services/ICurrencySpendService.cs
new file mode 100644
index 0000000..6aa27c1
--- /dev/null
+++ b/SVSim.Database/Services/ICurrencySpendService.cs
@@ -0,0 +1,14 @@
+using SVSim.Database.Models;
+
+namespace SVSim.Database.Services;
+
+///
+/// Centralized debit primitive — the symmetric twin of RewardGrantService.ApplyAsync.
+/// Encapsulates the affordability-check + deduction + post-state-total pattern that was inlined
+/// across the shop/pack controllers. Does NOT call SaveChangesAsync; the caller saves.
+/// Freeplay (for Crystal/Rupee/RedEther) makes spends always succeed without deducting.
+///
+public interface ICurrencySpendService
+{
+ Task TrySpendAsync(Viewer viewer, SpendCurrency currency, long cost, CancellationToken ct = default);
+}
diff --git a/SVSim.Database/Services/IViewerEntitlements.cs b/SVSim.Database/Services/IViewerEntitlements.cs
new file mode 100644
index 0000000..1a3ba83
--- /dev/null
+++ b/SVSim.Database/Services/IViewerEntitlements.cs
@@ -0,0 +1,46 @@
+using SVSim.Database.Enums;
+using SVSim.Database.Models;
+
+namespace SVSim.Database.Services;
+
+///
+/// The single read/ownership authority for what a viewer is *treated as* owning. Knows the
+/// Freeplay flag; all freeplay read-side behavior lives here. See
+/// docs/superpowers/specs/2026-05-29-freeplay-mode-design.md.
+///
+public interface IViewerEntitlements
+{
+ /// True when the global Freeplay config section is enabled.
+ bool IsFreeplay { get; }
+
+ ///
+ /// The balance the viewer is treated as having: the configured freeplay amount for
+ /// Crystal/Rupee/RedEther when freeplay is on, otherwise (and always for SpotPoint) the real
+ /// viewer.Currency field.
+ ///
+ long EffectiveBalance(Viewer viewer, SpendCurrency currency);
+
+ bool OwnsCard(Viewer viewer, long cardId);
+
+ /// uses (Skin == leader skin).
+ bool OwnsCosmetic(Viewer viewer, CosmeticType type, int id);
+
+ /// The full owned-card projection for /load/index's user_card_list.
+ Task> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default);
+
+ /// The cosmetic id-lists + leader-skin catalog/owned-set for /load/index.
+ Task EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default);
+}
+
+///
+/// Cosmetic projection bundle for /load/index. The four id-lists are "what the viewer owns"
+/// (all of them in freeplay). Leader skins are always the full catalog with a per-skin owned flag;
+/// is every skin id in freeplay.
+///
+public sealed record EffectiveCosmetics(
+ IReadOnlyList SleeveIds,
+ IReadOnlyList EmblemIds,
+ IReadOnlyList DegreeIds,
+ IReadOnlyList MyPageBackgroundIds,
+ IReadOnlyList AllLeaderSkins,
+ IReadOnlySet OwnedLeaderSkinIds);
diff --git a/SVSim.Database/Services/SpendCurrency.cs b/SVSim.Database/Services/SpendCurrency.cs
new file mode 100644
index 0000000..ee7f246
--- /dev/null
+++ b/SVSim.Database/Services/SpendCurrency.cs
@@ -0,0 +1,16 @@
+namespace SVSim.Database.Services;
+
+/// The scalar wallet currencies the central debit primitive understands.
+public enum SpendCurrency { Crystal, Rupee, RedEther, SpotPoint }
+
+public enum SpendOutcome { Success, Insufficient }
+
+///
+/// Result of a call.
+/// is the balance the client should show after the spend — the real post-deduction balance, or the
+/// freeplay effective balance when the spend was a freeplay no-op.
+///
+public sealed record SpendResult(SpendOutcome Outcome, long PostStateTotal)
+{
+ public bool Success => Outcome == SpendOutcome.Success;
+}