Skip to content
Next — unreleased

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 separate accounts belonging to the configured Game. Read one by its Game-scoped id:

const recipe = await sage.recipes.byId(recipeId);

To discover the catalog without knowing ids, call sage.recipes.list({ policy: 'no-store' }).

The functional equivalent is listRecipes(ctx, options?) from @aephia/atlas-kit/crafting. Both return a frozen readonly array sorted by ascending numeric Recipe id, including active and deactivated Recipes. Ingredients and outputs resolve cargo ids and names; quantityRaw stays an exact bigint, and JSON.stringify(recipes) emits decimal strings. Empty results are frozen arrays too, with discovery metadata available through readMeta.

Discovery uses only the verified Recipe discriminator and configured Game filters, never id probing or an unfiltered scan. A configured indexer takes precedence: it must support the full query, supplies address hints, and every present candidate is validated directly. Duplicates are collapsed, missing stale hints are omitted, and an empty indexer result does not fall back to RPC. Invalid present accounts, unresolved cargo, reported provider/indexer failures and resource-limit violations reject the whole call; the SDK does not truncate the catalog or return a successful prefix.

Each completed listing reconsults discovery; compatible concurrent requests may share in-flight work. A retained context can observe changed Recipes while reusing cargo definitions from its separate registry. policy: 'no-store' prevents discovered Recipe cache writes, not registry retention. Per-call commitment governs Recipe discovery/direct validation; cargo definitions use the context’s commitment. signal cancels only that caller’s wait, including while definitions load. Context disposal cancels its pending callers.

For a daily snapshot, create a fresh context with the desired commitment and no restored registry snapshot store, use a provider (or configured indexer) that can answer the complete query, and dispose the context afterwards. The runnable daily snapshot example uses only public APIs, serializes exact quantities as decimal strings and accepts your configured read-only RPC. Its call shapes compile in the offline gate; Recipe discovery behavior is tested against generated offline fixtures, not a new live catalog capture.

Finder and per-Recipe readMeta record strategy, filters, source, commitment and slots; they do not prove a single atomic chain slot or latest state. Provider interfaces expose no completeness token, so the SDK cannot detect a provider that silently omits accounts. A fresh context avoids retained definitions but does not establish that the provider itself is current or complete.

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.

Start and completion Plans resolve current history and fee routing. Their descriptions show the applicable recipe-fee multiplier (10,000 basis points = 1×), rather than predicting a debit from an unevidenced formula. Completion uses the fee snapshot and governance captured by a version-1 Process, even if the Starbase now has another controller. Version-0 Processes retain their legacy layout and have no captured fee metadata; Atlas Kit does not invent a zero-fee guarantee.

On Game version 2, completion requires an existing reconciled Character XP budget and XP configuration; Hab start and completion also require their existing structure XP ledger. The SDK validates these accounts without creating, funding or migrating them. Unsupported runtimes and missing accounts fail with a local remedy. Relevant policy and runtime state are checked again before signing, while shared balances may change; the transaction can still race later changes. These routes are verified offline, not live-qualified. Recreate older saved start/completion Plans.

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.

The start planner checks the Recipe’s fresh usage counters during planning and again before signing. Each start consumes exactly one use, whatever its quantity, so it needs usageCountRaw below usageLimitRaw; a stored limit of 0 is never usable. Cancelling a process returns its use. Every Recipe on z.ink PTR currently stores 9007199254740991 (2^53 − 1) as its limit. See the usage-limit evidence.

Concurrent Recipe usage by other players does not invalidate a Plan while at least one use remains. Completion and cancellation do not reserve new uses. All three planners still reject changes to the Recipe’s other encoded fields, including fees, status, ingredients, outputs and Hab policy. Recreate previously prepared Plans to use this guard. The check is not a reservation: another transaction can consume remaining uses after the pre-sign read and before yours lands.

A Process started without a Crafting Hab omits the craftingHab property. Completion and cancellation planners accept it without providing a Hab. Completion has passed unsigned PTR simulation. Cancellation is currently offline-tested only: when the no-Hab evidence was recorded, the planner still refused finished Processes, and the only available PTR Process had already finished. The game itself lets a finished Process be cancelled. See the no-Hab evidence.

For a Process that references a Hab, completion requires that same Hab to exist and be active. Cancellation passes the stored Hab without reading it, because the game lets you cancel whatever state the Hab is in, including after it has been deactivated, evicted or deconstructed.

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, without a timestamp: planCancelCraftingProcess(ctx, process, { authorization }) stops the run, and what comes back — inputs, crew — follows the game’s rules, decided at execution rather than promised by the planner. The game has no timing rule for cancellation, so a finished Process can still be cancelled; that returns its inputs instead of producing outputs, so complete it instead if you want the outputs.

Unrelated Starbase activity, such as shield changes, does not invalidate a cancellation Plan. Preparation and execution verify the same canonical System, Game, Starbase generation and active Starbase presence. Changes to the personal Process, inventory, Character or Hab still require a fresh Plan. Recreate saved cancellation Plans prepared before this safeguard was added; caller-added exact state checks remain strict. These checks run before signing, so the game still has the final say if state changes afterward.

The timestamp you pass to completion 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.

The analyzed Food batch spent only its escrow. At SAGE deployment slot 58,713,616, the rent-funded no-Hab batch of ten Food with one crew, 20 Biomass, 10,000 raw ATLAS escrowed and an existing Food output row required no additional ATLAS, Profile lamports or cargo. Other recipes, quantities, crew counts, Hab processes and new output rows were not analyzed. This is not a current spending cap. See the research and limits.

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.