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 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.
Daily snapshots and freshness
Section titled “Daily snapshots and freshness”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.
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.
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.
Gotchas
Section titled “Gotchas”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.
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