# Markets
Documentation: next (source: develop).

Install the preview: `pnpm add @aephia/atlas-kit@next`. These docs follow develop and may include changes not yet published to npm.

> Inspect local and faction markets, then plan local orders or directional Faction Market trades.

Markdown source of https://develop.atlas-kit-docs.pages.dev/guides/markets/ — see https://develop.atlas-kit-docs.pages.dev/ai/ for the full machine-readable surface.

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.

:::game[Every starbase has its own market]
Each starbase has its own market. Players choose what they are willing to pay
or accept there, and trades happen when a buyer and seller agree. That means
the game does not set one price for the whole galaxy. A resource can be cheap
where it is plentiful and expensive where it is scarce, especially because
moving goods between starbases takes time and fuel.
:::

## Reading a local market

```ts
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.

> **Runnable example — Read the markets of a system.** List the local markets of a system and inspect one order book. Run it in the browser at https://develop.atlas-kit-docs.pages.dev/guides/markets/.

## 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:

```ts
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

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:

```ts
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

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

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](/start-here/how-it-acts/) 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.

```ts
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](/guides/starbases/#registering-at-a-starbase) 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](/guides/moving-a-fleet/#sign-and-execute-once).

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

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.

```ts illustrative
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.

:::caution[Market data ages faster than most reads]
Most SAGE state changes on a timescale of minutes. Market orders do not — they
are the one place where another player is actively racing you.

If you are showing prices, use a short `maxAge`, and treat anything you read as
a snapshot rather than a current quote. See [setting up your
RPC](/start-here/rpc/) for how freshness is configured.
:::

## 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.

```ts illustrative
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

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

**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

- [`markets`](/reference/markets/) — every export in this entry
- [`markets/actions`](/reference/markets/actions/) — order and trade planners
- [`world`](/guides/world/) — markets belong to systems
- [`cargo`](/guides/cargo/) — what is being traded
