diff --git a/src/dotnet/QuantEngine.Application/Services/JsonSeedIngestionService.cs b/src/dotnet/QuantEngine.Application/Services/JsonSeedIngestionService.cs
new file mode 100644
index 00000000..f3c65e94
--- /dev/null
+++ b/src/dotnet/QuantEngine.Application/Services/JsonSeedIngestionService.cs
@@ -0,0 +1,69 @@
+using System.Text.Json;
+using QuantEngine.Core.Interfaces;
+
+namespace QuantEngine.Application.Services;
+
+///
+/// JSON-first seed ingestion path. XLSX conversion remains an external
+/// preparation step; the runtime application only reads canonical JSON and
+/// persists normalized snapshots to PostgreSQL.
+///
+public sealed class JsonSeedIngestionService
+{
+ private readonly GatherTradingDataParser _parser;
+ private readonly ICollectionRepository _repository;
+ private readonly ILogger _logger;
+
+ public JsonSeedIngestionService(
+ GatherTradingDataParser parser,
+ ICollectionRepository repository,
+ ILogger logger)
+ {
+ _parser = parser;
+ _repository = repository;
+ _logger = logger;
+ }
+
+ public async Task IngestAsync(string jsonPath, string runId)
+ {
+ var startedAt = DateTimeOffset.UtcNow;
+ var rows = _parser.ParseGatherTradingData(jsonPath);
+ await _repository.SaveRunAsync(new CollectionRunRecord(
+ runId, "RUNNING", startedAt.ToString("O"), null, rows.Count, 0));
+
+ var errors = 0;
+ foreach (var row in rows)
+ {
+ if (!row.TryGetValue("Ticker", out var tickerValue) || string.IsNullOrWhiteSpace(tickerValue?.ToString()))
+ {
+ errors++;
+ continue;
+ }
+
+ var ticker = tickerValue.ToString()!;
+ await _repository.SaveSnapshotAsync(new CollectionSnapshotRecord(
+ runId,
+ "data_feed",
+ ticker,
+ "json_seed",
+ JsonSerializer.Serialize(row),
+ startedAt.ToString("O")));
+ }
+
+ var finishedAt = DateTimeOffset.UtcNow;
+ var status = rows.Count > 0 && errors == 0 ? "COMPLETED" : "COMPLETED_WITH_ERRORS";
+ await _repository.UpdateRunStatusAsync(runId, status, finishedAt.ToString("O"), rows.Count - errors, errors);
+ _logger.LogInformation("JSON seed ingestion {RunId} completed: {Snapshots} snapshots, {Errors} errors", runId, rows.Count - errors, errors);
+
+ return new CollectionRunResult
+ {
+ RunId = runId,
+ Status = status,
+ StartedAt = startedAt.ToString("O"),
+ FinishedAt = finishedAt.ToString("O"),
+ SuccessCount = rows.Count - errors,
+ ErrorCount = errors,
+ Rows = rows
+ };
+ }
+}
diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs
index ec2b9ce7..77b95202 100644
--- a/src/dotnet/QuantEngine.Web/Program.cs
+++ b/src/dotnet/QuantEngine.Web/Program.cs
@@ -100,6 +100,8 @@ try
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+ builder.Services.AddSingleton();
+ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();