Skip to content
Next — unreleased

Crafting

View as Markdown

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 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);

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.

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.

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.

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.