diff --git a/SVSim.EmulatedEntrypoint/Controllers/DegreeController.cs b/SVSim.EmulatedEntrypoint/Controllers/DegreeController.cs
new file mode 100644
index 00000000..da37bbf1
--- /dev/null
+++ b/SVSim.EmulatedEntrypoint/Controllers/DegreeController.cs
@@ -0,0 +1,41 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using SVSim.Database;
+using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
+using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
+using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Degree;
+using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Degree;
+
+namespace SVSim.EmulatedEntrypoint.Controllers;
+
+///
+/// /degree/* — viewer-scoped degree (title) management. Read, mark-favorite, set-currently-displayed.
+/// Mirrors /sleeve/* and /emblem/*; favorite handler is no-op like the others until a viewer-favorites
+/// schema lands (would also serve /sleeve/favorite + /emblem/favorite via the shared Wizard/FavoriteTask.cs Kind enum).
+///
+[Route("degree")]
+public class DegreeController : SVSimController
+{
+ private readonly SVSimDbContext _db;
+
+ public DegreeController(SVSimDbContext db) => _db = db;
+
+ [HttpPost("degree_list")]
+ public async Task> DegreeList(DegreeListRequest _, CancellationToken ct)
+ {
+ if (!TryGetViewerId(out long viewerId)) return Unauthorized();
+
+ var viewer = await _db.Viewers
+ .Include(v => v.Degrees)
+ .FirstOrDefaultAsync(v => v.Id == viewerId, ct);
+ if (viewer is null) return Unauthorized();
+
+ return new DegreeListResponse
+ {
+ UserDegreeList = viewer.Degrees
+ .Select(d => new DegreeListEntry { DegreeId = d.Id })
+ .OrderBy(e => e.DegreeId)
+ .ToList(),
+ };
+ }
+}
diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Degree/DegreeListRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Degree/DegreeListRequest.cs
new file mode 100644
index 00000000..983ce9b5
--- /dev/null
+++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/Degree/DegreeListRequest.cs
@@ -0,0 +1,12 @@
+using MessagePack;
+using System.Text.Json.Serialization;
+
+namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Degree;
+
+[MessagePackObject]
+public class DegreeListRequest : BaseRequest
+{
+ [JsonPropertyName("degree_id")]
+ [Key("degree_id")]
+ public int DegreeId { get; set; }
+}
diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListEntry.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListEntry.cs
new file mode 100644
index 00000000..21c1a464
--- /dev/null
+++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListEntry.cs
@@ -0,0 +1,12 @@
+using MessagePack;
+using System.Text.Json.Serialization;
+
+namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Degree;
+
+[MessagePackObject]
+public class DegreeListEntry
+{
+ [JsonPropertyName("degree_id")]
+ [Key("degree_id")]
+ public int DegreeId { get; set; }
+}
diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListResponse.cs
new file mode 100644
index 00000000..c10d43d8
--- /dev/null
+++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Degree/DegreeListResponse.cs
@@ -0,0 +1,12 @@
+using MessagePack;
+using System.Text.Json.Serialization;
+
+namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Degree;
+
+[MessagePackObject]
+public class DegreeListResponse
+{
+ [JsonPropertyName("user_degree_list")]
+ [Key("user_degree_list")]
+ public List UserDegreeList { get; set; } = new();
+}
diff --git a/SVSim.UnitTests/Controllers/DegreeControllerListTests.cs b/SVSim.UnitTests/Controllers/DegreeControllerListTests.cs
new file mode 100644
index 00000000..7d4f5d14
--- /dev/null
+++ b/SVSim.UnitTests/Controllers/DegreeControllerListTests.cs
@@ -0,0 +1,49 @@
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using NUnit.Framework;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using SVSim.Database;
+using SVSim.Database.Models;
+using SVSim.UnitTests.Infrastructure;
+
+namespace SVSim.UnitTests.Controllers;
+
+public class DegreeControllerListTests
+{
+ [Test]
+ public async Task DegreeList_returns_owned_ids_wrapped()
+ {
+ using var factory = new SVSimTestFactory();
+ long viewerId = await factory.SeedViewerAsync(tutorialState: 0);
+
+ using (var scope = factory.Services.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ var viewer = await db.Viewers.Include(v => v.Degrees).FirstAsync(v => v.Id == viewerId);
+ var d100 = await db.Set().FirstOrDefaultAsync(d => d.Id == 100)
+ ?? db.Set().Add(new DegreeEntry { Id = 100 }).Entity;
+ var d101 = await db.Set().FirstOrDefaultAsync(d => d.Id == 101)
+ ?? db.Set().Add(new DegreeEntry { Id = 101 }).Entity;
+ viewer.Degrees.Add(d100);
+ viewer.Degrees.Add(d101);
+ await db.SaveChangesAsync();
+ }
+
+ using var client = factory.CreateAuthenticatedClient(viewerId);
+ var requestJson = """{"degree_id":0,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
+ var response = await client.PostAsync("/degree/degree_list",
+ new StringContent(requestJson, Encoding.UTF8, "application/json"));
+
+ Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
+
+ var body = await response.Content.ReadAsStringAsync();
+ using var doc = JsonDocument.Parse(body);
+ var ids = doc.RootElement.GetProperty("user_degree_list").EnumerateArray()
+ .Select(e => e.GetProperty("degree_id").GetInt32())
+ .ToList();
+ Assert.That(ids, Does.Contain(100));
+ Assert.That(ids, Does.Contain(101));
+ }
+}