Crafting
Crafting has three moving parts, and separating them makes the API obvious:
- a recipe is the rule: these inputs become that output, taking this long;
- a hab is the place: a plot at a starbase where crafting can happen;
- a process is the run: one recipe executing in one hab, right now.
This guide covers recipes and processes. The hab as a facility — its buildings, modifiers, capacity, and rent — has its own guide.
Recipes
Section titled “Recipes”Recipes are static definitions from the Game account, so they are the same for everyone and cheap to read:
const recipe = await sage.recipes.byId(recipeId);Habs and processes
Section titled “Habs and processes”Both hang off whoever owns them, which is usually a character or their state at a starbase:
const habs = await sage.craftingHabs.byCharacter(characterAddress);const running = await sage.craftingProcesses.byCharacter(characterAddress);A process is a lifecycle, not a boolean. It has a start, a duration derived from its recipe, and an end — which is why crafting occupies a hab for a period rather than completing instantly.
Why processes need discovery
Section titled “Why processes need discovery”Crafting processes have no derivable address. You cannot compute where one lives from the character or the recipe; it is assigned when the process is created.
That is why the reads above are byCharacter, byProfile, and
byStarbasePlayer rather than a single get(address) — each encodes a
verified way of finding processes, rather than pretending you can calculate
where they are. See the bindings escape hatch for the
broader pattern of accounts without derivable addresses.
Running a process
Section titled “Running a process”The lifecycle above is also what you write against: starting a process, collecting a finished one, and cancelling one that should stop are each one planned transaction. If this would be your first write, read How Atlas Kit changes the game first.
Starting commits everything up front: the recipe, how many runs, the inputs from your starbase storage, and the crew who will work it. Because a process has no derivable address, starting one also names the fresh address the new process will live at — you generate it, and its key signs once at creation alongside your wallet:
import { generateKeyPairSigner } from '@solana/kit';import type { PlanAuthorization } from '@aephia/atlas-kit/crafting/actions';import { planStartCraftingProcess } from '@aephia/atlas-kit/crafting/actions';import { executePlan } from '@aephia/atlas-kit/planning';
const authorization = { profile: character.profile.address, authority: authoritySigner.address, keyIndex: 0,} satisfies PlanAuthorization;
const processKey = await generateKeyPairSigner();const recipe = await sage.recipes.byId(recipeId);const plan = await planStartCraftingProcess(ctx, recipe, base, { authorization, craftingProcess: processKey.address, quantity: 1n, numCrew: 2,});
const result = await executePlan(ctx, plan, { feePayer: authoritySigner, signers: [processKey],});Keep processKey until the Plan has executed: the fresh key signs the
creation alongside your wallet, so it goes into signers — execution fails
without it. Afterwards the key has no further role; the process is found
again by discovery, never by re-deriving that address.
Completing collects a process that has reached the end of its duration. Derive the lifecycle state first — the planner refuses a process that is not actually complete at the timestamp you give it:
import { deriveCraftingProcessState } from '@aephia/atlas-kit/crafting';import type { PlanAuthorization } from '@aephia/atlas-kit/crafting/actions';import { planCompleteCraftingProcess } from '@aephia/atlas-kit/crafting/actions';
const authorization = { profile: character.profile.address, authority: authoritySigner.address, keyIndex: 0,} satisfies PlanAuthorization;
const processes = await sage.craftingProcesses.byCharacter(characterAddress);for (const process of processes) { if (deriveCraftingProcessState(process, nowUnixSeconds).kind !== 'complete') continue; const plan = await planCompleteCraftingProcess(ctx, process, { authorization, atUnixSeconds: nowUnixSeconds, }); console.log(plan.summary); break;}Cancelling is the same shape as completing, for a process that has not
finished: planCancelCraftingProcess stops the run, and what comes back —
inputs, crew — follows the game’s rules, decided at execution rather than
promised by the planner.
The timestamp you pass is used only for the local lifecycle check; the game remains the authority on timing, capacity, and returns when the Plan executes. Signing and executing works exactly as in Warp & subwarp.
Gotchas
Section titled “Gotchas”A finished process still exists. Completion is a state, not a deletion. Read the lifecycle rather than assuming presence means “in progress”.
Completing is not automatic. A process that reaches its duration sits complete until someone collects it — running a process covers planning that collection.
Hab capacity is finite. A hab holds a limited number of concurrent processes, so “can this character craft” is not answerable from the recipe alone — crafting habs covers reading the free slots.
Recipe inputs are raw amounts. Like all quantities, they are bigint and
need the cargo definitions to render as names and decimals.
Reference
Section titled “Reference”crafting— every export in this entrycrafting/actions— the lifecycle planners- Crafting habs — the facility itself
starbases— where habs livecargo— the inputs and outputs