diff --git a/SVSim.Database/Services/Replay/IReplayHistoryReader.cs b/SVSim.Database/Services/Replay/IReplayHistoryReader.cs
new file mode 100644
index 0000000..bee5f59
--- /dev/null
+++ b/SVSim.Database/Services/Replay/IReplayHistoryReader.cs
@@ -0,0 +1,7 @@
+namespace SVSim.Database.Services.Replay;
+
+public interface IReplayHistoryReader
+{
+ /// Newest-first by CreateTime. Caps at (default 50).
+ Task> GetRecentAsync(long viewerId, int take, CancellationToken ct);
+}
diff --git a/SVSim.Database/Services/Replay/ReplayHistoryEntry.cs b/SVSim.Database/Services/Replay/ReplayHistoryEntry.cs
new file mode 100644
index 0000000..2143df3
--- /dev/null
+++ b/SVSim.Database/Services/Replay/ReplayHistoryEntry.cs
@@ -0,0 +1,27 @@
+namespace SVSim.Database.Services.Replay;
+
+///
+/// Read-side row returned by . The /replay/info
+/// controller maps this to its wire DTO (all-stringified per prod capture).
+///
+public sealed record ReplayHistoryEntry(
+ long BattleId,
+ int BattleType,
+ int DeckFormat,
+ int TwoPickType,
+ int IsLimitTurn,
+ int SelfClassId,
+ int SelfSubClassId,
+ int SelfCharaId,
+ string SelfRotationId,
+ int OpponentClassId,
+ int OpponentSubClassId,
+ int OpponentCharaId,
+ string OpponentName,
+ string OpponentCountryCode,
+ long OpponentEmblemId,
+ long OpponentDegreeId,
+ string OpponentRotationId,
+ bool IsWin,
+ DateTime BattleStartTime,
+ DateTime CreateTime);
diff --git a/SVSim.Database/Services/Replay/ReplayHistoryReader.cs b/SVSim.Database/Services/Replay/ReplayHistoryReader.cs
new file mode 100644
index 0000000..7c042d2
--- /dev/null
+++ b/SVSim.Database/Services/Replay/ReplayHistoryReader.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace SVSim.Database.Services.Replay;
+
+public sealed class ReplayHistoryReader : IReplayHistoryReader
+{
+ private readonly SVSimDbContext _db;
+
+ public ReplayHistoryReader(SVSimDbContext db) => _db = db;
+
+ public async Task> GetRecentAsync(long viewerId, int take, CancellationToken ct)
+ {
+ return await _db.ViewerBattleHistories
+ .AsNoTracking()
+ .Where(h => h.ViewerId == viewerId)
+ .OrderByDescending(h => h.CreateTime)
+ .Take(take)
+ .Select(h => new ReplayHistoryEntry(
+ h.BattleId, h.BattleType, h.DeckFormat, h.TwoPickType, h.IsLimitTurn,
+ h.SelfClassId, h.SelfSubClassId, h.SelfCharaId, h.SelfRotationId,
+ h.OpponentClassId, h.OpponentSubClassId, h.OpponentCharaId,
+ h.OpponentName, h.OpponentCountryCode,
+ h.OpponentEmblemId, h.OpponentDegreeId, h.OpponentRotationId,
+ h.IsWin, h.BattleStartTime, h.CreateTime))
+ .ToListAsync(ct);
+ }
+}
diff --git a/SVSim.EmulatedEntrypoint/Program.cs b/SVSim.EmulatedEntrypoint/Program.cs
index d8de41b..ade7ac4 100644
--- a/SVSim.EmulatedEntrypoint/Program.cs
+++ b/SVSim.EmulatedEntrypoint/Program.cs
@@ -119,6 +119,7 @@ public class Program
builder.Services.AddSingleton();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
// Deck-code mint/resolve uses IMemoryCache for ephemeral (3-min TTL) storage; no DB
// row, no migration. Singleton because the cache + RNG seam are process-wide.