diff --git a/SVSim.Database/Services/Inventory/IInventoryService.cs b/SVSim.Database/Services/Inventory/IInventoryService.cs
new file mode 100644
index 0000000..e1d6ce3
--- /dev/null
+++ b/SVSim.Database/Services/Inventory/IInventoryService.cs
@@ -0,0 +1,28 @@
+using SVSim.Database.Models;
+using SVSim.Database.Services;
+
+namespace SVSim.Database.Services.Inventory;
+
+public interface IInventoryService
+{
+ ///
+ /// Loads the viewer with the canonical inventory graph (Cards.Card, Sleeves, Emblems,
+ /// LeaderSkins, Degrees, MyPageBackgrounds, Items.Item under AsSplitQuery), opens a DB
+ /// transaction, and returns a builder for queueing operations. Throws
+ /// if the viewer does not exist.
+ ///
+ Task BeginAsync(
+ long viewerId,
+ CancellationToken ct = default,
+ Action? configure = null);
+
+ Task> EffectiveOwnedCardsAsync(Viewer viewer, CancellationToken ct = default);
+ Task EffectiveCosmeticsAsync(Viewer viewer, CancellationToken ct = default);
+ long EffectiveBalance(Viewer viewer, SpendCurrency currency);
+}
+
+public sealed class InventoryViewerNotFoundException : Exception
+{
+ public InventoryViewerNotFoundException(long viewerId)
+ : base($"Viewer {viewerId} not found") { }
+}
diff --git a/SVSim.Database/Services/Inventory/IInventoryTransaction.cs b/SVSim.Database/Services/Inventory/IInventoryTransaction.cs
new file mode 100644
index 0000000..1ba2059
--- /dev/null
+++ b/SVSim.Database/Services/Inventory/IInventoryTransaction.cs
@@ -0,0 +1,30 @@
+using SVSim.Database.Enums;
+using SVSim.Database.Models;
+using SVSim.Database.Services;
+
+namespace SVSim.Database.Services.Inventory;
+
+///
+/// Scoped builder returned by . Queue spend +
+/// grant operations; commit to save and assemble the .
+///
+/// Dispose without committing rolls back the underlying DB transaction and detaches any
+/// in-memory mutations. Always wrap in await using.
+///
+///
+public interface IInventoryTransaction : IAsyncDisposable
+{
+ Viewer Viewer { get; }
+ bool IsFreeplay { get; }
+
+ Task TrySpendAsync(SpendCurrency currency, long cost, CancellationToken ct = default);
+ Task TryDebitAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default);
+ Task> GrantAsync(UserGoodsType type, long detailId, int num, CancellationToken ct = default);
+ Task BackfillCardCosmeticsAsync(CancellationToken ct = default);
+
+ long EffectiveBalance(SpendCurrency currency);
+ bool OwnsCard(long cardId);
+ bool OwnsCosmetic(CosmeticType type, int id);
+
+ Task CommitAsync(CancellationToken ct = default);
+}
diff --git a/SVSim.Database/Services/Inventory/InventoryCatalogException.cs b/SVSim.Database/Services/Inventory/InventoryCatalogException.cs
new file mode 100644
index 0000000..d01943f
--- /dev/null
+++ b/SVSim.Database/Services/Inventory/InventoryCatalogException.cs
@@ -0,0 +1,10 @@
+namespace SVSim.Database.Services.Inventory;
+
+///
+/// Thrown when an inventory operation references a catalog id that doesn't exist
+/// (unknown card / item / cosmetic). Programmer error — bubbles to the global error handler.
+///
+public sealed class InventoryCatalogException : Exception
+{
+ public InventoryCatalogException(string message) : base(message) { }
+}
diff --git a/SVSim.Database/Services/Inventory/InventoryCommitResult.cs b/SVSim.Database/Services/Inventory/InventoryCommitResult.cs
new file mode 100644
index 0000000..e058901
--- /dev/null
+++ b/SVSim.Database/Services/Inventory/InventoryCommitResult.cs
@@ -0,0 +1,20 @@
+using SVSim.Database.Services;
+
+namespace SVSim.Database.Services.Inventory;
+
+///
+/// Result of .
+///
+/// — wire-shape entries with currency-collision resolved (one entry per
+/// (type, id); for currencies that were both spent and granted, the last post-state in op order
+/// wins). Use this for response reward_list fields.
+///
+///
+/// — verbatim ordered (type, id, num) sequence the caller queued. No
+/// collapse, no cosmetic-cascade entries. Use this for BP achieved_info and Story
+/// story_reward_list popups.
+///
+///
+public sealed record InventoryCommitResult(
+ IReadOnlyList RewardList,
+ IReadOnlyList Deltas);
diff --git a/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs b/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs
new file mode 100644
index 0000000..b11997c
--- /dev/null
+++ b/SVSim.Database/Services/Inventory/InventoryLoadConfig.cs
@@ -0,0 +1,31 @@
+using System.Linq.Expressions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Query;
+using SVSim.Database.Models;
+
+namespace SVSim.Database.Services.Inventory;
+
+///
+/// Caller-supplied extra .Include chains on top of the canonical viewer-inventory query
+/// in . Use to bring in extra collections needed by
+/// the calling controller (e.g. MissionData, BuildDeckPurchases).
+///
+public sealed class InventoryLoadConfig
+{
+ internal List, IQueryable>> Includes { get; } = new();
+
+ public InventoryLoadConfig WithInclude(
+ Expression> path)
+ {
+ Includes.Add(q => q.Include(path));
+ return this;
+ }
+
+ public InventoryLoadConfig WithInclude(
+ Expression>> collectionPath,
+ Expression> thenPath)
+ {
+ Includes.Add(q => q.Include(collectionPath).ThenInclude(thenPath));
+ return this;
+ }
+}