Quick answer
Use terminal.send() only from one explicit request. Validate the resource, integer amount, TERMINAL_MIN_SEND, destination room name, description length, source Terminal ownership and cooldown, resource inventory, transaction Energy, and post-send Energy reserve. Disable the request before the API call, save the snapshot and return code, then verify Game.market.outgoingTransactions and Store changes on a later tick.
The destination does not require vision
The official API does not require Game.rooms[destination] to exist. Requiring destination vision would incorrectly block valid transfers.
function validateDestinationRoom(roomName) {
return typeof roomName === 'string'
&& /^[WE]\d+[NS]\d+$/.test(roomName);
}
A syntactically valid name does not prove the room has a Terminal. The API return code remains the final same-tick check.
Validate minimum amount and description
function validateSendRequest(request) {
if (!request || request.enabled !== true) {
return { valid: false, reason: 'disabled' };
}
if (
typeof request.resourceType !== 'string'
|| !Number.isInteger(request.amount)
|| request.amount < TERMINAL_MIN_SEND
|| !validateDestinationRoom(request.destination)
) {
return { valid: false, reason: 'arguments-invalid' };
}
const description = request.description == null
? ''
: String(request.description);
if (description.length > 100) {
return {
valid: false,
reason: 'description-too-long'
};
}
return {
valid: true,
reason: 'valid',
description
};
}
TERMINAL_MIN_SEND is the official minimum amount constant. Do not duplicate its numeric value in application code.
Use the correct Energy formula
sending a non-Energy resource
required resource = amount
required Energy = transaction cost
sending Energy
required Energy = amount + transaction cost
function calculateSendBudget(input) {
const sendingEnergy =
input.resourceType === RESOURCE_ENERGY;
const requiredResource = sendingEnergy
? 0
: input.amount;
const requiredEnergy = sendingEnergy
? input.amount + input.transactionEnergy
: input.transactionEnergy;
return {
sendingEnergy,
requiredResource,
requiredEnergy
};
}
Checking only transaction Energy while sending Energy can under-budget the source Terminal by the full transfer amount.
Build a testable transfer plan
function evaluateTerminalSend(input) {
const validation = validateSendRequest(input.request);
if (!validation.valid) {
return { ready: false, reason: validation.reason };
}
if (!input.terminalReady) {
return { ready: false, reason: 'terminal-not-ready' };
}
if (!Number.isFinite(input.transactionEnergy)) {
return { ready: false, reason: 'energy-cost-invalid' };
}
const budget = calculateSendBudget({
resourceType: input.request.resourceType,
amount: input.request.amount,
transactionEnergy: input.transactionEnergy
});
const resourceAvailable = Number.isFinite(
input.resourceAvailable
)
? input.resourceAvailable
: 0;
const energyAvailable = Number.isFinite(
input.energyAvailable
)
? input.energyAvailable
: 0;
const reserve = Number.isFinite(
input.request.energyReserve
)
? input.request.energyReserve
: 0;
if (resourceAvailable < budget.requiredResource) {
return {
ready: false,
reason: 'resource-insufficient',
...budget
};
}
if (
energyAvailable - budget.requiredEnergy
< reserve
) {
return {
ready: false,
reason: 'energy-reserve',
...budget
};
}
return {
ready: true,
reason: 'ready',
description: validation.description,
...budget,
energyAfter:
energyAvailable - budget.requiredEnergy
};
}
Complete one-time send example
State impact: this code may schedule one real Terminal transfer. It writes a request snapshot and never retries automatically.
module.exports.loop = function () {
const request = Memory.terminalSendRequest;
const validation = validateSendRequest(request);
if (!validation.valid) {
return;
}
const terminal = typeof request.terminalId === 'string'
? Game.getObjectById(request.terminalId)
: null;
const terminalReady = Boolean(
terminal
&& terminal.structureType === STRUCTURE_TERMINAL
&& terminal.my === true
&& terminal.isActive() === true
&& terminal.cooldown === 0
);
if (!terminalReady) {
request.lastStatus = 'terminal-not-ready';
request.lastCheckedAt = Game.time;
return;
}
const transactionEnergy =
Game.market.calcTransactionCost(
request.amount,
terminal.room.name,
request.destination
);
const resourceAvailable =
request.resourceType === RESOURCE_ENERGY
? 0
: terminal.store.getUsedCapacity(
request.resourceType
);
const energyAvailable =
terminal.store.getUsedCapacity(
RESOURCE_ENERGY
);
const plan = evaluateTerminalSend({
request,
terminalReady,
transactionEnergy,
resourceAvailable,
energyAvailable
});
request.lastStatus = plan.reason;
request.lastCheckedAt = Game.time;
if (!plan.ready) {
request.preview = {
resourceAvailable,
energyAvailable,
transactionEnergy,
requiredResource:
plan.requiredResource ?? null,
requiredEnergy:
plan.requiredEnergy ?? null
};
return;
}
request.enabled = false;
request.status = 'submitted';
request.submittedAt = Game.time;
request.snapshot = {
terminalId: terminal.id,
sourceRoom: terminal.room.name,
destination: request.destination,
resourceType: request.resourceType,
amount: request.amount,
description: plan.description,
transactionEnergy,
requiredEnergy: plan.requiredEnergy,
energyBefore: energyAvailable,
resourceBefore:
request.resourceType === RESOURCE_ENERGY
? energyAvailable
: resourceAvailable
};
const result = terminal.send(
request.resourceType,
request.amount,
request.destination,
plan.description
);
request.result = result;
request.resultAt = Game.time;
request.status = result === OK
? 'accepted-pending-settlement'
: 'failed-review-required';
console.log(JSON.stringify({
type: 'terminal-send-result',
terminalId: terminal.id,
sourceRoom: terminal.room.name,
destination: request.destination,
resourceType: request.resourceType,
amount: request.amount,
transactionEnergy,
requiredEnergy: plan.requiredEnergy,
result
}));
};
Disable before calling send
A failed send can result from inventory, cooldown, target Terminal, or configuration changes. Automatic retry can repeatedly export resources after a request is no longer intended. Re-enable only after reviewing the snapshot and current state.
Treat the calculated cost as an estimate
Game.market.calcTransactionCost() provides the ordinary transfer estimate. PWR_OPERATE_TERMINAL can reduce transfer Energy and cooldown. This guide uses the estimate as a conservative preflight and verifies actual Store and transaction records later instead of claiming an effect-adjusted exact cost before execution.
Verify the outgoing transaction later
function findMatchingOutgoingTransaction(snapshot) {
return Game.market.outgoingTransactions.find(tx =>
tx.time >= snapshot.submittedAt
&& tx.from === snapshot.sourceRoom
&& tx.to === snapshot.destination
&& tx.resourceType === snapshot.resourceType
&& tx.amount === snapshot.amount
&& tx.description === snapshot.description
) || null;
}
Compare the transaction record, source Terminal Store, Energy, destination, description, amount, and saved snapshot. OK alone means “scheduled,” not “received and allocated.”
The receiving room still needs logistics
Arrival in the target Terminal does not automatically:
- move resources to Storage;
- feed Labs or a Factory;
- update your task queue;
- send an acknowledgement;
- maintain a target inventory;
- return excess stock.
Source transfer and destination inventory management are separate workflows.
Handle return codes
| Code | Interpretation | Review |
|---|---|---|
OK | Transfer scheduled | Outgoing transaction and Store later |
ERR_NOT_OWNER | Terminal is not yours | ID and ownership |
ERR_NOT_ENOUGH_RESOURCES | Resource or Energy insufficient | Direction-specific formula |
ERR_INVALID_ARGS | Resource, amount, destination, description, or target Terminal invalid | Request snapshot |
ERR_TIRED | Terminal cooling down | terminal.cooldown |
The current send() API table does not list ERR_FULL. Do not copy return codes from unrelated Store methods.
Debugging checklist
- Require one explicit request.
- Validate resource, integer amount, and destination.
- Use
TERMINAL_MIN_SEND. - Limit description to 100 characters.
- Do not require destination vision.
- Check source Terminal ownership, activity, and cooldown.
- Calculate transaction Energy.
- For Energy, require amount plus transaction Energy.
- Preserve an Energy reserve.
- Disable before calling.
- Save the snapshot and return code.
- Verify outgoing transactions and Store later.
Scope and next steps
This guide does not implement multi-room replenishment queues, destination storage logic, exact power-effect prediction, retries, acknowledgements, target inventory control, market orders, or cross-shard resources.
Frequently asked questions
Can I send to an invisible room?
Yes. Destination vision is not required.
Why is Energy special?
The Terminal pays both the Energy being sent and the Energy transaction cost.
Does OK mean resources are already allocated?
No. The transfer was scheduled; destination logistics remain separate.
Should a failed request retry every tick?
No. Require a new explicit review.