LOGISTICS · DIRECT TERMINAL TRANSFER

How to Send Resources Between Terminals Safely

Plan one direct Terminal transfer with a one-time request, validate TERMINAL_MIN_SEND and the 100-character description, separate ordinary-resource and Energy budgets, preserve reserves, disable before send(), and verify outgoing transactions and Store changes afterward.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — send(), destination vision, 100-character description, cooldown, return codes and calcTransactionCost() · Budget boundary: Sending Energy requires amount plus transaction Energy; sending another resource requires that resource plus transaction Energy

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — send(), destination vision, 100-character description, cooldown, return codes and calcTransactionCost()
Budget boundary
Sending Energy requires amount plus transaction Energy; sending another resource requires that resource plus transaction Energy
Execution boundary
OK means the transfer was scheduled; receipt and settlement require outgoing transaction and Store verification
JavaScript syntax
Passed
Offline transfer review
Passed — disabled, invalid arguments, minimum amount, description, inventory, Energy reserve, cooldown and ready states
Screeps Console test
Pending
Live Terminal transfer, power-effect and receiving-room test
Pending
Last verified
July 26, 2026

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

CodeInterpretationReview
OKTransfer scheduledOutgoing transaction and Store later
ERR_NOT_OWNERTerminal is not yoursID and ownership
ERR_NOT_ENOUGH_RESOURCESResource or Energy insufficientDirection-specific formula
ERR_INVALID_ARGSResource, amount, destination, description, or target Terminal invalidRequest snapshot
ERR_TIREDTerminal cooling downterminal.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.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.