diff --git a/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru b/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru
new file mode 100644
index 0000000..6a48dfb
--- /dev/null
+++ b/bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru
@@ -0,0 +1,81 @@
+meta {
+ name: GET recipe definitions
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/game/world/recipe-definitions
+ body: none
+ auth: none
+}
+
+tests {
+ test("returns 200 JSON with schema v1 and recipes array", function () {
+ expect(res.getStatus()).to.equal(200);
+ expect(res.getHeader("content-type")).to.contain("application/json");
+ const body = res.getBody();
+ expect(body.schemaVersion).to.equal(1);
+ expect(body.recipes).to.be.an("array");
+ expect(body.recipes.length).to.equal(8);
+ });
+
+ test("recipes are ascending by id (ordinal)", function () {
+ const body = res.getBody();
+ const ids = body.recipes.map((x) => x.id);
+ // Match server StringComparer.Ordinal (prototype ids are ASCII snake_case).
+ const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
+ expect(ids).to.eql(sorted);
+ });
+
+ test("frozen prototype eight matches registry id order", function () {
+ const body = res.getBody();
+ const ids = body.recipes.map((x) => x.id);
+ expect(ids).to.eql([
+ "make_armor_quick",
+ "make_contract_token",
+ "make_field_stim_batch",
+ "make_field_stim_mk0",
+ "make_prototype_armor",
+ "make_survey_drone_kit",
+ "refine_scrap_efficient",
+ "refine_scrap_standard",
+ ]);
+ });
+
+ test("frozen prototype eight is present", function () {
+ const body = res.getBody();
+ const ids = new Set(body.recipes.map((x) => x.id));
+ expect(ids.has("make_armor_quick")).to.equal(true);
+ expect(ids.has("make_contract_token")).to.equal(true);
+ expect(ids.has("make_field_stim_batch")).to.equal(true);
+ expect(ids.has("make_field_stim_mk0")).to.equal(true);
+ expect(ids.has("make_prototype_armor")).to.equal(true);
+ expect(ids.has("make_survey_drone_kit")).to.equal(true);
+ expect(ids.has("refine_scrap_efficient")).to.equal(true);
+ expect(ids.has("refine_scrap_standard")).to.equal(true);
+ });
+
+ test("refine_scrap_standard row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.recipes.find((x) => x.id === "refine_scrap_standard");
+ expect(row).to.be.an("object");
+ expect(row.displayName).to.equal("Refine Scrap (Standard)");
+ expect(row.recipeKind).to.equal("process");
+ expect(row.requiredSkillId).to.equal("refine");
+ expect(row.inputs).to.eql([{ itemId: "scrap_metal_bulk", quantity: 5 }]);
+ expect(row.outputs).to.eql([{ itemId: "refined_plate_stock", quantity: 1 }]);
+ });
+
+ test("make_field_stim_mk0 row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.recipes.find((x) => x.id === "make_field_stim_mk0");
+ expect(row).to.be.an("object");
+ expect(row.recipeKind).to.equal("make");
+ expect(row.inputs).to.eql([
+ { itemId: "refined_plate_stock", quantity: 2 },
+ { itemId: "scrap_metal_bulk", quantity: 1 },
+ ]);
+ expect(row.outputs).to.eql([{ itemId: "field_stim_mk0", quantity: 1 }]);
+ });
+}
diff --git a/bruno/neon-sprawl-server/recipe-definitions/folder.bru b/bruno/neon-sprawl-server/recipe-definitions/folder.bru
new file mode 100644
index 0000000..73f8c35
--- /dev/null
+++ b/bruno/neon-sprawl-server/recipe-definitions/folder.bru
@@ -0,0 +1,3 @@
+meta {
+ name: recipe-definitions
+}
diff --git a/docs/manual-qa/NEO-68.md b/docs/manual-qa/NEO-68.md
new file mode 100644
index 0000000..d036b35
--- /dev/null
+++ b/docs/manual-qa/NEO-68.md
@@ -0,0 +1,32 @@
+# NEO-68 — Manual QA checklist
+
+| Field | Value |
+|-------|-------|
+| Key | NEO-68 |
+| Title | E3.M2: GET world recipe-definitions + Bruno |
+| Linear | https://linear.app/neon-sprawl/issue/NEO-68/e3m2-get-world-recipe-definitions-bruno |
+| Plan | `docs/plans/NEO-68-implementation-plan.md` |
+| Branch | `NEO-68-get-world-recipe-definitions-bruno` |
+
+## Preconditions
+
+- Server built and configured with default content paths pointing at repo `content/recipes`, `content/items`, and `content/skills` (local dev / `InMemoryWebApplicationFactory` tests use the same layout).
+
+## Checklist
+
+1. Start **`NeonSprawl.Server`** (e.g. `dotnet run` from `server/NeonSprawl.Server`).
+2. **`GET /game/world/recipe-definitions`** — expect **200** and **`Content-Type`** containing **`application/json`**. Example (default dev URL from `Properties/launchSettings.json` and Bruno `environments/Local.bru`; change the host/port if yours differs):
+
+ ```bash
+ curl -sS -i "http://localhost:5253/game/world/recipe-definitions"
+ ```
+
+3. Parse JSON — expect **`schemaVersion`** === **1**, **`recipes`** array length **8**.
+4. Confirm **`id`** values match the frozen prototype eight in **registry id order** (ordinal):
+
+ **`make_armor_quick`**, **`make_contract_token`**, **`make_field_stim_batch`**, **`make_field_stim_mk0`**, **`make_prototype_armor`**, **`make_survey_drone_kit`**, **`refine_scrap_efficient`**, **`refine_scrap_standard`**
+
+ (same sequence as `RecipeDefinitionsWorldApiTests.FrozenEightInIdOrder`).
+5. Spot-check **`refine_scrap_standard`**: **`displayName`** “Refine Scrap (Standard)”, **`recipeKind`** **`process`**, **`requiredSkillId`** **`refine`**, **`inputs`** `[{ itemId: scrap_metal_bulk, quantity: 5 }]`, **`outputs`** `[{ itemId: refined_plate_stock, quantity: 1 }]`.
+6. Spot-check **`make_field_stim_mk0`**: **`recipeKind`** **`make`**, two **`inputs`** rows (`refined_plate_stock` × 2, `scrap_metal_bulk` × 1), **`outputs`** `[{ itemId: field_stim_mk0, quantity: 1 }]`.
+7. Optional: run **`bruno/neon-sprawl-server/recipe-definitions/Get recipe definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`).
diff --git a/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs
new file mode 100644
index 0000000..6ecb771
--- /dev/null
+++ b/server/NeonSprawl.Server.Tests/Game/Crafting/RecipeDefinitionsWorldApiTests.cs
@@ -0,0 +1,60 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http.Json;
+using NeonSprawl.Server.Game.Crafting;
+using Xunit;
+
+namespace NeonSprawl.Server.Tests.Game.Crafting;
+
+public class RecipeDefinitionsWorldApiTests
+{
+ /// Frozen prototype eight in registry id order (ordinal). Keep in sync with manual QA and Bruno.
+ public static readonly string[] FrozenEightInIdOrder =
+ [
+ "make_armor_quick",
+ "make_contract_token",
+ "make_field_stim_batch",
+ "make_field_stim_mk0",
+ "make_prototype_armor",
+ "make_survey_drone_kit",
+ "refine_scrap_efficient",
+ "refine_scrap_standard",
+ ];
+
+ [Fact]
+ public async Task GetRecipeDefinitions_ShouldReturnSchemaV1_WithFrozenEightInIdOrder()
+ {
+ // Arrange
+ await using var factory = new InMemoryWebApplicationFactory();
+ var client = factory.CreateClient();
+ // Act
+ var response = await client.GetAsync("/game/world/recipe-definitions");
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadFromJsonAsync();
+ Assert.NotNull(body);
+ Assert.Equal(RecipeDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
+ Assert.NotNull(body.Recipes);
+ var ids = body.Recipes.Select(static r => r.Id).ToList();
+ Assert.Equal(FrozenEightInIdOrder, ids);
+
+ var refine = body.Recipes.Single(r => r.Id == "refine_scrap_standard");
+ Assert.Equal("Refine Scrap (Standard)", refine.DisplayName);
+ Assert.Equal("process", refine.RecipeKind);
+ Assert.Equal("refine", refine.RequiredSkillId);
+ Assert.Single(refine.Inputs);
+ Assert.Equal("scrap_metal_bulk", refine.Inputs[0].ItemId);
+ Assert.Equal(5, refine.Inputs[0].Quantity);
+ Assert.Single(refine.Outputs);
+ Assert.Equal("refined_plate_stock", refine.Outputs[0].ItemId);
+ Assert.Equal(1, refine.Outputs[0].Quantity);
+
+ var stim = body.Recipes.Single(r => r.Id == "make_field_stim_mk0");
+ Assert.Equal("make", stim.RecipeKind);
+ Assert.Equal(2, stim.Inputs.Count);
+ Assert.Equal("refined_plate_stock", stim.Inputs[0].ItemId);
+ Assert.Equal(2, stim.Inputs[0].Quantity);
+ Assert.Equal("scrap_metal_bulk", stim.Inputs[1].ItemId);
+ Assert.Equal(1, stim.Inputs[1].Quantity);
+ }
+}
diff --git a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs
new file mode 100644
index 0000000..ebd8c02
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsListDtos.cs
@@ -0,0 +1,48 @@
+using System.Text.Json.Serialization;
+
+namespace NeonSprawl.Server.Game.Crafting;
+
+/// JSON body for GET /game/world/recipe-definitions (NEO-68).
+public sealed class RecipeDefinitionsListResponse
+{
+ public const int CurrentSchemaVersion = 1;
+
+ [JsonPropertyName("schemaVersion")]
+ public int SchemaVersion { get; init; } = CurrentSchemaVersion;
+
+ /// Loaded recipes ordered by stable id (ordinal), matching .
+ [JsonPropertyName("recipes")]
+ public required IReadOnlyList Recipes { get; init; }
+}
+
+/// One row in the read-only recipe definition projection.
+public sealed class RecipeDefinitionJson
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; init; }
+
+ [JsonPropertyName("displayName")]
+ public required string DisplayName { get; init; }
+
+ [JsonPropertyName("recipeKind")]
+ public required string RecipeKind { get; init; }
+
+ [JsonPropertyName("requiredSkillId")]
+ public required string RequiredSkillId { get; init; }
+
+ [JsonPropertyName("inputs")]
+ public required IReadOnlyList Inputs { get; init; }
+
+ [JsonPropertyName("outputs")]
+ public required IReadOnlyList Outputs { get; init; }
+}
+
+/// One input or output row in the read-only recipe definition projection.
+public sealed class RecipeIoJson
+{
+ [JsonPropertyName("itemId")]
+ public required string ItemId { get; init; }
+
+ [JsonPropertyName("quantity")]
+ public required int Quantity { get; init; }
+}
diff --git a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs
new file mode 100644
index 0000000..a6d6a64
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs
@@ -0,0 +1,54 @@
+namespace NeonSprawl.Server.Game.Crafting;
+
+/// Maps GET /game/world/recipe-definitions (NEO-68).
+public static class RecipeDefinitionsWorldApi
+{
+ public static WebApplication MapRecipeDefinitionsWorldApi(this WebApplication app)
+ {
+ app.MapGet(
+ "/game/world/recipe-definitions",
+ (IRecipeDefinitionRegistry registry) =>
+ {
+ var defs = registry.GetDefinitionsInIdOrder();
+ var recipes = new List(defs.Count);
+ foreach (var d in defs)
+ {
+ recipes.Add(
+ new RecipeDefinitionJson
+ {
+ Id = d.Id,
+ DisplayName = d.DisplayName,
+ RecipeKind = d.RecipeKind,
+ RequiredSkillId = d.RequiredSkillId,
+ Inputs = MapIoRows(d.Inputs),
+ Outputs = MapIoRows(d.Outputs),
+ });
+ }
+
+ return Results.Json(
+ new RecipeDefinitionsListResponse
+ {
+ SchemaVersion = RecipeDefinitionsListResponse.CurrentSchemaVersion,
+ Recipes = recipes,
+ });
+ });
+
+ return app;
+ }
+
+ private static List MapIoRows(IReadOnlyList rows)
+ {
+ var list = new List(rows.Count);
+ foreach (var row in rows)
+ {
+ list.Add(
+ new RecipeIoJson
+ {
+ ItemId = row.ItemId,
+ Quantity = row.Quantity,
+ });
+ }
+
+ return list;
+ }
+}
diff --git a/server/NeonSprawl.Server/Program.cs b/server/NeonSprawl.Server/Program.cs
index 2f54b74..7d7e2e7 100644
--- a/server/NeonSprawl.Server/Program.cs
+++ b/server/NeonSprawl.Server/Program.cs
@@ -44,6 +44,7 @@ app.MapInteractionApi();
app.MapInteractablesWorldApi();
app.MapSkillDefinitionsWorldApi();
app.MapItemDefinitionsWorldApi();
+app.MapRecipeDefinitionsWorldApi();
app.MapResourceNodeDefinitionsWorldApi();
app.MapPlayerInventoryApi();
app.MapSkillProgressionSnapshotApi();