A companion gateway is a service that sits in front of your consumer proxy-router (C-Node) and opens, reuses, and closes sessions so the clients behind it never see a session_id. Uplink is one such gateway; the hosted API at api.mor.org is another (different product, same shape). The proxy-router exposes a small set of opt-in features for that companion. Every one of them is inert until you switch it on.
Stock nodes are unaffected. With no GATEWAY_* environment variable, no X-Gateway-Operation header, and no maxStakeWei field, the proxy-router behaves exactly as before: stock stake calculation, no journal, no wallet lock, native session-expiry cleanup, and response bodies unchanged. Only GET /config gains a GatewayCapabilities array.

Capability discovery

GET /config (permission system_config) now returns a GatewayCapabilities array a companion can feature-detect against instead of parsing version strings:

Stake cap on open-by-bid (maxStakeWei)

POST /blockchain/bids/{id}/session accepts an optional maxStakeWei alongside sessionDuration. If the stake the node computes for that duration exceeds the cap, the open is rejected before any approval or open transaction is built — nothing is broadcast.
Sending maxStakeWei also switches that open onto the gateway stake formula (see below). Leave it out and the stock formula applies.

Gateway stake formula

When managed mode is on (GATEWAY_JOURNAL_PATH set) or the request carries maxStakeWei, the node:
  • refreshes total MOR supply and today’s emissions budget immediately before computing the stake, so a quote is not stale by the time approval + open have mined;
  • rounds the stake up with one basis point (0.01%) of headroom, so the contract’s round-down of stipend and duration cannot deliver fewer seconds than requested;
  • rejects sessionDuration below the contract minimum of 300 seconds up front instead of letting the open revert.
Unmanaged opens use the stock formula, ceil(supply × price × (duration + 1) / budget) — one second of headroom and a round-up, described in How much MOR does an open lock?. The two formulas differ by well under a tenth of a percent on any normal session, but they are not byte-identical, so a companion that quotes users a stake figure should either call GET /blockchain/models/:id/session/estimate on a node running in managed mode (the estimate then uses the gateway formula; on an unmanaged node it returns the stock figure), apply the gateway rounding itself (ceil(supply × price × duration × 10001 / (budget × 10000))), or set maxStakeWei a little above its own estimate.

Operation journal (X-Gateway-Operation)

The problem this solves: a gateway sends “open session”, the HTTP connection drops, and the gateway has no idea whether the approval or open transaction was broadcast. Retrying blindly can open a second session and lock stake twice. With the journal enabled, the gateway attaches an idempotency key and the node records every stage durably before acting on it.

Enable

Use

Send X-Gateway-Operation: <32 lowercase hex chars> on:
  • POST /blockchain/bids/{id}/session
  • POST /blockchain/sessions/{id}/close
  • POST /blockchain/stakes/withdraw
Rules:
  • Any journal problem — GATEWAY_JOURNAL_PATH unset, malformed ID, directory not writable, or an ID that already has a journal file — answers 409 {"error":"operation exists or journal unavailable"} and performs no chain action. The duplicate check survives node restarts because it is the file itself. Generate a fresh ID per logical operation; a repeated ID is your signal to read the existing journal, never a retry.
  • Without the header, the same endpoints behave exactly as before and their bodies contain no progress field.

Progress in responses

When an operation was journaled, success and error bodies on those three endpoints include a progress object:

needs_reconcile

If the node recorded a transaction hash and then failed to write the journal (disk gone, permissions), it does not pretend the open failed. stage becomes needs_reconcile, journal_write_error carries the cause, and the hashes stay in the response. The companion must treat this as “chain may have advanced” and reconcile from the hashes — not retry. Conversely, a build failure before any hash of that kind exists resets the stage to not_submitted; a build failure after a hash exists does not, so an ambiguous broadcast can never authorise a second session under the same operation. The journal contains stage, status, session ID, and transaction hashes only — never key material or prompt content.

Wallet lock (managed mode)

While GATEWAY_JOURNAL_PATH is set, OpenSession, CloseSession, and WithdrawUserStakes are serialised through a process-local lock so two concurrent gateway calls cannot race the same nonce. A call that is still waiting when its request context is cancelled returns the context error and never touches the chain.
The lock is per process. Running more than one wallet-bearing proxy-router replica against the same wallet in managed mode is unsupported. Run a single replica, or accept that cross-replica nonce collisions are your problem.

Who closes expired sessions? (GATEWAY_OWN_CLEANUP)

By default the proxy-router runs its own expiry loop that closes sessions past endsAt — and it keeps doing so in managed mode. Setting GATEWAY_JOURNAL_PATH alone does not hand cleanup to the companion; both may close, which is safe (a second close simply fails). Set GATEWAY_OWN_CLEANUP=1 only when the companion actually reconciles and closes expired sessions itself. The node then rehydrates its session list from chain at boot and suspends the native loop.
With GATEWAY_OWN_CLEANUP=1 and no live companion, expired sessions are never auto-closed. Stake stays in the Inference Contract until something calls closeSession. Do not set this in a compose file “just in case”.

Mode summary

maxStakeWei on a request enables the gateway stake formula for that open in any of the three columns.