Markets
There are two kinds of market, and they answer different questions.
A local market belongs to a star system and has an order book: people posting what they will buy or sell and at what price. A faction market offers goods on faction terms rather than through player orders.
Reading a local market
Section titled “Reading a local market”const markets = await system.localMarkets.all();const forOre = await system.localMarkets.forCargo(cargoId);forCargo is the one you usually want: markets are per-resource, so asking
“what is the market for this cargo in this system” is the natural question.
Order books have sides
Section titled “Order books have sides”An order book is not a price. It is a set of orders, each with a quantity and a price — and the two sides are separate reads rather than one list you filter:
const buying = market.bids;const selling = market.asks;They are modelled apart because the data available on an order depends on its side: maker state differs between buying and selling. Flattening them would mean a single shape with half its fields undefined at any time.
Reading your observed principal and proceeds
Section titled “Reading your observed principal and proceeds”A maker row stores different assets for each side. Select the loaded market’s rows for a Profile, keep their explicit side, and project the raw values:
const makerAssets = (['bid', 'ask'] as const).flatMap((side) => { const book = side === 'bid' ? market.bids : market.asks; return book.makers .filter((maker) => maker.profile === profileAddress) .map((maker) => ({ side, assets: deriveLocalMarketMakerAssets(side, maker), }));});For a bid, ATLAS is locked principal and cargo is released proceeds. For an ask, cargo is locked principal and ATLAS is released proceeds. These are observed maker-state classifications only: a released value does not prove that cancellation or withdrawal will succeed, does not account for fees or refunds, and is not a freshness guarantee. Refresh the market before making a decision.
Prices are local
Section titled “Prices are local”The same resource can trade at different prices in different systems, and that is a feature of the game rather than stale data. Moving goods between systems where prices differ is a strategy, not an arbitrage bug.
So there is no such thing as the price of a resource. There is a price at a market, at a moment.
Placing an order
Section titled “Placing an order”Joining an order book is a write: you post what you will buy or sell, at your price, from your own state at that market’s starbase. If this would be your first write, read How Atlas Kit changes the game first.
planPlaceLocalMarketOrder takes the market, your state at its starbase, and
the order itself — the side, a price per unit, and a quantity, both as the
same raw bigint amounts you read. This example is illustrative: the saved
replay scene lacks the required history-policy accounts.
import type { PlanAuthorization } from '@aephia/atlas-kit/markets/actions';import { planPlaceLocalMarketOrder } from '@aephia/atlas-kit/markets/actions';
const authorization = { profile: character.profile.address, authority: authoritySigner.address, keyIndex: 0,} satisfies PlanAuthorization;
const held = base.cargo.items.find((item) => item.id === market.cargo.id);if (held !== undefined && held.quantityRaw > 0n) { const plan = await planPlaceLocalMarketOrder(ctx, market, base, { authorization, side: 'ask', priceRaw: 25_000_000n, quantity: 1n, }); console.table(plan.describe());}Selling requires holding what you sell — the snippet checks the starbase storage for the market’s resource first, which is also why placing an order starts from your starbase state rather than from the market alone. If you have no state at that starbase yet, register there first.
The planner proves what it can from current state — the market matches the starbase, the order’s numbers are valid — and the order book itself decides matching when the Plan executes. Remember the caution above: this is the one surface where other players are actively racing you, so plan against fresh reads and expect the book to have moved. Signing and executing works exactly as in Warp & subwarp.
Local Market placement and cancellation resolve the Starbase’s current Democracy history and fee routing. The Plan describes the source of its fee rates in basis points (10,000 = 100%); the executed trade value determines the actual fee. Before signing, Atlas Kit checks that the applicable policy and routing still match. Unrelated shared Treasury balances may change. Missing required routing accounts or pending daily history checkpoints need operator attention; the planner does not create them. Policy can still change between validation and execution. These compatibility routes have offline coverage, not live write qualification. Recreate saved Plans from an older SDK.
Inspecting maximum order debits
Section titled “Inspecting maximum order debits”Use getLocalMarketOrderDebitBounds(plan) before reserving funds for an order.
It reads the Plan without another RPC call and returns either qualified maximum
debits or an explicit unavailable reason. Never interpret unavailable as zero
cost or as permission to skip your application’s budget checks.
import type { Plan } from '@aephia/atlas-kit/planning';import { getLocalMarketOrderDebitBounds } from '@aephia/atlas-kit/markets/actions';
declare const plan: Plan; // A planPlaceLocalMarketOrder result.const bounds = getLocalMarketOrderDebitBounds(plan);if (bounds.status === 'unavailable') { throw new Error(`Cannot reserve this order's costs: ${bounds.reason}`);}console.log({ cargoRaw: bounds.maximumCargoDebitRaw, atlasFloyds: bounds.maximumAtlasDebitRaw, governanceFeeFloyds: bounds.maximumGovernanceFeeRaw, profileLamports: bounds.maximumProfileDebit,});The qualified route currently requires version-zero Market, Character, StarbasePlayer and CurrencyCache accounts, an existing row for the selected cargo, rent-funded Character, Market, StarbasePlayer and CurrencyCache accounts, and the default zero-fee policy with no Democracy history, governance or Treasury account. Other routes remain usable through their existing planner checks, but their debit bounds are unavailable.
An ask reserves the selected cargo quantity; a bid reserves raw price times quantity in ATLAS Floyds. The Profile’s native-token ceiling reserves storage for one new maker row and one order: 114 bytes in the qualified layout. This is conservative even when the maker already exists. Existing account surplus, expected proceeds and rent refunds never reduce this ceiling. The Profile must also retain its own rent reserve. Transaction fees are separate and belong to the transaction fee payer, not these order bounds.
A Profile that cannot cover this conservative ceiling returns
profile-below-storage-ceiling; planning still succeeds because the program
may need less storage for an existing maker or a fully matched order. That is
not a spending guarantee: applications requiring verified reserves must decline
an unavailable result. If a Plan had available bounds when created and the
Profile later loses the required funding, execution rejects before signing.
These are maximum debits, not predicted spending, fills or profit. A resting
bid locks ATLAS and a resting ask locks cargo. A matching taker receives the
opposite asset immediately; a maker’s released assets remain available for
withdrawal. On maker snapshots, escrowedOrReleasedAtlasRaw represents bid escrow or ask
proceeds, while cargoRaw represents ask escrow or bid proceeds.
rentBytesOwnedRaw is program bookkeeping, not a refundable-rent quote.
Qualified ceilings and order intent survive saving and loading the Plan. Execution rechecks their assumptions and Profile funding before signing. A placement must stand alone; confirm it and prepare the next action from fresh state. The chain instruction has no atomic spending-cap field, so a state change between this check and execution remains possible. Simulation order IDs are also provisional: read the confirmed order book instead of assuming a simulated ID was assigned to your order.
Trading with a faction
Section titled “Trading with a faction”planTradeFactionMarket plans one explicit buy or sell from a loaded Fleet and
Faction Market. It takes exactly authorization, direction, offerIndex and
quantityRaw. The index is an integer from 0 through 65535 that identifies an
existing ordered offer; quantity is a bigint from 1 through 2^64−1. No signer
object or derived-account override is accepted.
import type { SageContext } from '@aephia/atlas-kit/client';import type { FleetSnapshot } from '@aephia/atlas-kit/fleets';import type { FactionMarketSnapshot } from '@aephia/atlas-kit/markets';import { planTradeFactionMarket, type PlanAuthorization,} from '@aephia/atlas-kit/markets/actions';
declare const ctx: SageContext;declare const fleet: FleetSnapshot;declare const market: FactionMarketSnapshot;declare const authorization: PlanAuthorization;const plan = await planTradeFactionMarket(ctx, fleet, market, { authorization, direction: 'buy', offerIndex: 0, quantityRaw: 5n,});console.log(plan.describe());| Direction | Cargo | ATLAS |
|---|---|---|
| Buy (Ask/SIDE_SELL) | Faction Treasury → Fleet hold | Character → Treasury vault |
| Sell (Bid/SIDE_BUY) | Fleet hold → Faction Treasury | Treasury vault → Character |
The Fleet must be Docked at an undestroyed local Starbase with current sequence, and the market must be active. The planner freshly validates the canonical Game/Profile/Character/System/Starbase Player/Market/Treasury graph. If the selected offer’s intent changed, refresh and choose it explicitly; an index is not a durable offer identity. Execution freshness binds the whole ordered Market and the relevant accounts, not the entire Game.
Any change to that selected Market—including another offer’s fill or a restock timestamp—can require a fresh Plan. Other markets’ activity can change the shared Treasury stock, ATLAS balance and epoch counters without invalidating a new Plan. A buy retains the selected cargo’s reserve-floor setting (absent means zero); a sell retains the Treasury’s ATLAS payout cap and epoch length. Changing those relevant settings requires preparing a new Plan. The optional Epoch Meter keeps identity/layout checks while its activity counters may change. Both the Treasury and included Epoch Meter retain their observed schema-version bytes; a changed byte requires a fresh Plan. These are SDK freshness checks, not a guarantee of stock, a full fill or active limits in the current deployment; the game applies its rules when the trade executes.
Trade guards require observed schemaVersion identity. Unsupported saved Plan
formats must be recreated through the current planner.
A sell can supply cargo not yet stocked by the Treasury. Creating that reserves row can grow the reserves map and require NormalizeRent. Row absence or removal does not itself invalidate the sell Plan; final rent funding, balances and fill remain chain-authoritative. The SDK does not estimate rent or guarantee funding.
The System anchor is checked during planning and again by the game during execution. Plans allow unrelated System fields to change.
Trades for different markets may be combined when their prerequisites agree. All cargo and direction constraints are retained. Contradictory Treasury rules or shared-account identities are rejected when combining Plans; refresh the markets and prepare compatible Plans, or use separate confirmed transactions.
Do not combine independently prepared trades for the same market. A trade can restock and rewrite the offer list before the next instruction uses its index. New Plans reject that combination, including after saving and loading. Use separate confirmed transactions or a PlanSequence that prepares each trade from fresh snapshots after the previous one confirms.
Validated canonical Standing requires the complete Standing/Config/Epoch Meter cohort. Only true Standing absence permits omission of all three, and only when the selected direction’s minimum standing is at most −1000. Transport, owner, decode and identity failures are errors, not absence. Missing required cohort accounts must be repaired separately. A Plan that depends on absence fails if Standing appears before execution. Standing controls access and selling credits standing; it is not a local price adjustment.
The exact requested quantity is encoded unchanged, but partial fills are possible. Stock, treasury floors, faucet enable/caps, burn, standing/epoch/decay, fees, restocking and final arithmetic remain chain-authoritative. There is no local fill prediction, price advice or minimum-output guarantee. Planning only reads prerequisites and produces an inert Plan: no simulation, signing, submission, provisioning or synchronization. Generated-builder parity is offline construction evidence, not proof of successful deployed trades.
Find and cancel your orders
Section titled “Find and cancel your orders”Local Market orders belong to your Profile. Call
getLocalMarketOrdersByProfile(ctx, marketAddress, profileAddress) from
@aephia/atlas-kit/markets; each order’s makerRef and its maker state’s
profileRef refer to that Profile. The loaded Character relation
character.localMarketOrders.at(marketAddress) uses the same Profile.
Pass selected snapshot orderId values directly in the orderIds array to
planCancelLocalMarketOrders. These IDs are unsigned-u64 bigint values; ask IDs
use the high bit and cannot safely be converted to JavaScript numbers. An empty
array requests filled-escrow withdrawal. Cancellation validates Profile ownership
again against fresh orders; balances, fills and withdrawal availability remain
chain-authoritative.
Gotchas
Section titled “Gotchas”An empty order book is normal. A market with nobody trading in it exists and returns nothing. That is not a missing read.
Prices are raw integers. Like all amounts, they need the cargo definitions
to render meaningfully, and they are bigint.
Faction markets are not order books. Do not expect the same shape. They offer goods rather than matching player orders.
Reference
Section titled “Reference”markets— every export in this entrymarkets/actions— order and trade plannersworld— markets belong to systemscargo— what is being traded