Start with the creation boundary
Game.market.createOrder() returns a status code, not the new order ID. In the official engine, an accepted ordinary-resource call queues a global intent. Tick processing later charges the rounded fee and inserts an order whose initial active value is false, amount is zero, and created is the submission tick.
Therefore three facts must remain separate: the reviewed request, the accepted API call, and the exact order object observed later. A matching order found several ticks afterward is not enough; another module or Console command may have created it.
Freeze one reviewed request revision
function buildCreateOrderConfirmation(request) {
return [
'CREATE_MARKET_ORDER',
request.requestId,
request.revision,
request.type,
request.resourceType,
request.roomName,
request.price,
request.totalAmount,
request.creditReserve
].join('|');
}
Memory.marketCreateRequests ??= {};
Memory.marketCreateRequests['sell-U-001'] = {
requestId: 'sell-U-001',
revision: 1,
enabled: true,
type: ORDER_SELL,
resourceType: RESOURCE_UTRIUM,
roomName: 'W1N1',
price: 1,
totalAmount: 10000,
creditReserve: 1000000,
confirmation:
'CREATE_MARKET_ORDER|sell-U-001|1|sell|U|' +
'W1N1|1|10000|1000000'
};
The values are examples, not trading recommendations. Any edit to type, resource, room, price, amount, reserve or revision invalidates the old confirmation. Do not approve a request ID and then let another module mutate its payload.
For ordinary resources, verify Terminal ownership
function evaluateCreateOrderRequest(request) {
if (!request || request.enabled !== true) {
return { ready: false, status: 'request-disabled' };
}
if (
typeof request.requestId !== 'string'
|| request.requestId.length === 0
|| !Number.isInteger(request.revision)
|| request.revision < 1
|| (request.type !== ORDER_BUY
&& request.type !== ORDER_SELL)
|| typeof request.resourceType !== 'string'
|| typeof request.roomName !== 'string'
|| !Number.isFinite(request.price)
|| request.price <= 0
|| !Number.isInteger(request.totalAmount)
|| request.totalAmount <= 0
|| !Number.isFinite(request.creditReserve)
|| request.creditReserve < 0
) {
return { ready: false, status: 'request-invalid' };
}
if (
request.confirmation
!== buildCreateOrderConfirmation(request)
) {
return {
ready: false,
status: 'confirmation-mismatch'
};
}
const room = Game.rooms[request.roomName];
const terminal = room?.terminal ?? null;
if (!terminal || terminal.my !== true) {
return {
ready: false,
status: 'owned-terminal-not-observed'
};
}
return {
ready: true,
status: 'creation-ready',
terminalId: terminal.id
};
}
Order creation is not a Terminal action. The engine checks that an owned Terminal exists in the requested room; it does not require that Terminal to have zero cooldown or pass isActive(). Adding those requirements can incorrectly block a valid creation request.
Reserve the fee at engine storage precision
function calculateCreateOrderFeeCeiling(
price,
totalAmount
) {
const raw = price * totalAmount * 0.05;
return Math.ceil(raw * 1000) / 1000;
}
The public formula is 5% of price multiplied by total amount. The engine stores Credits in thousandths and rounds the internal fee upward. Reserving the milli-Credit ceiling prevents a fractional estimate from leaving the account just below its required reserve.
Use one account-level creation coordinator
function createMarketCreationCoordinator() {
let creationReserved = false;
let creditsReserved = 0;
return {
reserve(input) {
if (creationReserved) {
return {
ready: false,
status: 'creation-slot-reserved'
};
}
if (
input.credits
- creditsReserved
- input.fee
< input.creditReserve
) {
return {
ready: false,
status: 'credit-reserve'
};
}
creationReserved = true;
creditsReserved += input.fee;
return { ready: true, status: 'reserved' };
}
};
}
The API can accept more than one creation intent in a tick, but one-at-a-time is a deliberate attribution policy. Without it, every accepted call sees the same old order-ID set, and several new matching orders can appear together. The same coordinator must also account for other same-tick market fees in production.
Disable first, preserve old IDs, then call once
function submitCreateOrder(request, coordinator) {
const decision = evaluateCreateOrderRequest(request);
if (!decision.ready) return decision;
const fee = calculateCreateOrderFeeCeiling(
request.price,
request.totalAmount
);
const reservation = coordinator.reserve({
credits: Game.market.credits,
fee,
creditReserve: request.creditReserve
});
if (!reservation.ready) return reservation;
const orderIdsBefore = Object.keys(Game.market.orders);
request.enabled = false;
request.lastAttemptAt = Game.time;
const pending = {
requestId: request.requestId,
revision: request.revision,
confirmation: request.confirmation,
submittedAt: Game.time,
type: request.type,
resourceType: request.resourceType,
roomName: request.roomName,
price: request.price,
totalAmount: request.totalAmount,
fee,
orderIdsBefore
};
const result = Game.market.createOrder({
type: pending.type,
resourceType: pending.resourceType,
roomName: pending.roomName,
price: pending.price,
totalAmount: pending.totalAmount
});
request.lastResult = result;
request.status = result === OK
? 'accepted-pending-order-id'
: 'creation-rejected';
if (result === OK) {
Memory.pendingMarketCreations ??= {};
Memory.pendingMarketCreations[request.requestId] =
pending;
}
return { status: request.status, result };
}
Disabling before the call prevents automatic retry. A rejection requires a new reviewed revision. The pending record contains only frozen values; later edits to the request cannot change what the verifier expects.
Verify the ID difference in the next account snapshot
function verifyCreatedOrder(pending) {
if (Game.time < pending.submittedAt + 1) {
return { status: 'waiting-for-next-tick' };
}
if (Game.time > pending.submittedAt + 1) {
return { status: 'verification-window-missed' };
}
const before = new Set(pending.orderIdsBefore);
const candidates = Object.values(Game.market.orders)
.filter(order =>
!before.has(order.id)
&& order.type === pending.type
&& order.resourceType === pending.resourceType
&& order.roomName === pending.roomName
&& Math.abs(order.price - pending.price)
< 0.0005
&& order.totalAmount === pending.totalAmount
&& order.created === pending.submittedAt
);
if (candidates.length === 0) {
return { status: 'accepted-order-not-observed' };
}
if (candidates.length > 1) {
return {
status: 'new-order-identity-ambiguous',
candidateIds: candidates.map(order => order.id)
};
}
const order = candidates[0];
return {
status: 'created-order-observed',
orderId: order.id,
active: order.active,
amount: order.amount,
remainingAmount: order.remainingAmount
};
}
The created tick and old-ID difference exclude later lookalikes. The small price tolerance reflects the public milli-Credit representation; it is not permission to accept a materially different price.
Preserve ambiguous and missed evidence
| Status | Meaning |
|---|---|
creation-slot-reserved | Another attributable creation already owns this tick. |
accepted-pending-order-id | The call returned OK; no new ID is proven yet. |
accepted-order-not-observed | The next snapshot did not contain an exact new order. |
new-order-identity-ambiguous | Several exact new IDs appeared; do not select one arbitrarily. |
verification-window-missed | The bounded attribution window was skipped. |
created-order-observed | One exact new ID and created tick matched. |
Creation is not activation or a completed trade
The inserted order starts inactive in the engine. Later resource, Credit and Terminal conditions determine activation and executable amount. Observe those fields separately. An order ID proves creation, not activation, matching, profit or a future fill.
Production integration boundary
Create the coordinator once per game tick and route every create-order producer through it. Include change-price and extend-order fees in the same Credit reservation ledger. Persist accepted operations by request ID and revision, verify them exactly once on the next tick, and archive ambiguous or missed evidence for human review.