# Env: proxy-router

Source: https://nodedocs.mor.org/reference/env-proxy-router

## Agent Instructions

- Non-browser fetches of page URLs on this site return clean Markdown (not the JS UI). Prefer `https://nodedocs.mor.org/llms-full.txt` for the full corpus, or `https://nodedocs.mor.org/llms.txt` for the index.
- Per-page Markdown is also at `<page-url>.md` (homepage: `https://nodedocs.mor.org/index.md`).
- Docs search MCP: `https://nodedocs.mor.org/mcp` (discovery: `https://nodedocs.mor.org/.well-known/mcp`).
- Never invent contract addresses, chain IDs, token addresses, or live bid/model counts. Cite Networks and tokens; link active.mor.org for live data.
- Never claim Morpheus runs inference — independent providers do. Opening a session escrows MOR; it does not spend it.

The proxy-router is configured through `.env` (or container env vars). This page documents every variable. The canonical `.env.example` lives in the repo at [`proxy-router/.env.example`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/proxy-router/.env.example) and the full annotated dump at [`docs/proxy-router.all.env`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/docs/proxy-router.all.env).

> Defaults below are from the source of truth (`proxy-router/internal/config/config.go`). When in doubt, the source wins.

## Conflicting / overlapping variables — read this first

Several pairs of variables look like they do "the same thing." They don't. The proxy-router resolves each pair with a specific precedence rule, and getting it wrong is a common cause of "I changed the env but nothing happened."

> For every `*_CONTENT` ↔ `*_PATH` pair below, the **`*_CONTENT`** variable is a **one-shot file seeder**, not a runtime override. It only takes effect when the corresponding file **does not exist**. After the first successful start the file exists, and changing the `*_CONTENT` env on subsequent runs has **no effect** until you delete the file at `*_PATH`.

### `COOKIE_FILE_PATH` ↔ `COOKIE_CONTENT`

| Situation | What the proxy-router does |
|-----------|---------------------------|
| File at `COOKIE_FILE_PATH` already exists | Reads the file. **`COOKIE_CONTENT` is silently ignored.** |
| File does not exist + `COOKIE_CONTENT` is set | Writes `COOKIE_CONTENT` to the file (`username:password\n`), then reads it. |
| File does not exist + `COOKIE_CONTENT` is unset | Generates a random 32-char password for `admin`, writes it to the file. |

Source: [`proxy-router/internal/system/auth.go`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/proxy-router/internal/system/auth.go) (`EnsureConfigFilesExist`).

**Practical implications**

- For TEE / Akash / ephemeral container deployments, set `COOKIE_CONTENT` once on first start; the file gets seeded inside the volume and persists.
- To rotate credentials on a self-hosted node, use the **`POST /auth/users` endpoint** (or edit the cookie file in place) — changing `COOKIE_CONTENT` after first start does nothing.

### `MODELS_CONFIG_PATH` ↔ `MODELS_CONFIG_CONTENT`

| Situation | What the proxy-router does |
|-----------|---------------------------|
| File at `MODELS_CONFIG_PATH` already exists | Reads the file. **`MODELS_CONFIG_CONTENT` is silently ignored.** |
| File does not exist + `MODELS_CONFIG_CONTENT` is set | Writes `MODELS_CONFIG_CONTENT` to the file, then reads it. |
| File does not exist + `MODELS_CONFIG_CONTENT` is unset | Logs a warning and runs with an empty model registry. |

Source: [`proxy-router/internal/config/models_config.go`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/proxy-router/internal/config/models_config.go) (`ModelConfigLoader.Init`).

**Practical implications**

- Use `MODELS_CONFIG_CONTENT` (single-line JSON) in TEE/Akash deployments where you can't mount a volume.
- To update models on a self-hosted node, edit the JSON file directly and restart — don't bump `MODELS_CONFIG_CONTENT`. See [models-config.json](/reference/models-config).

### `AGENT_CONFIG_PATH` ↔ `AGENT_CONFIG_CONTENT`

Same `*_CONTENT`-only-seeds-the-file rule as the two above. Source: [`proxy-router/internal/config/agents_config.go`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/proxy-router/internal/config/agents_config.go).

### `AUTH_CONFIG_FILE_PATH` (no inline twin)

Path to `proxy.conf`, which holds `rpcauth=` and `rpcwhitelist=` lines. **There is no `AUTH_CONFIG_CONTENT` env** — the file is built at runtime by the proxy-router itself when you add users via [`POST /auth/users`](/reference/api-auth#add-or-update-a-user). For TEE / ephemeral deployments, `proxy.conf` is created on first start (with the admin entry derived from the cookie file) and managed via the API afterwards.

### `WALLET_PRIVATE_KEY` ↔ system keychain

| Situation | What the proxy-router does |
|-----------|---------------------------|
| `WALLET_PRIVATE_KEY` is set | Uses it. The keychain is **not** read. |
| `WALLET_PRIVATE_KEY` is unset and the OS supports a keychain (macOS) | Reads / writes the key from the system keychain. |
| Neither | Refuses to start signing transactions; read-only operations still work. |

`APP_RESET_KEYCHAIN=true` wipes the macOS keychain entry on start (use with care).

### `WEB_PUBLIC_URL` fallback

If `WEB_PUBLIC_URL` is unset, the proxy-router synthesizes `http://localhost:<port-from-WEB_ADDRESS>`. For provider deployments where consumers must reach you over the public internet, **always set this explicitly** to your public URL (e.g. `https://provider.example.com`). The TLS-pinning step in TEE attestation reads this URL.

### `ETH_NODE_USE_SUBSCRIPTIONS` ↔ `ETH_NODE_POLLING_INTERVAL`

Not a precedence pair, but easy to misconfigure:

- `ETH_NODE_USE_SUBSCRIPTIONS=true` → expects a **WSS** `ETH_NODE_ADDRESS` (e.g. `wss://...`). Polling interval is irrelevant.
- `ETH_NODE_USE_SUBSCRIPTIONS=false` (default) → expects an **HTTPS** `ETH_NODE_ADDRESS` and uses `ETH_NODE_POLLING_INTERVAL` (default `10` seconds).

Mismatch (e.g. `subscriptions=true` with HTTPS endpoint) makes the node fail to receive blockchain events even though startup may look healthy.

### `PROXY_STORE_CHAT_CONTEXT` is TEE-frozen

In the **`-tee` image** this is hard-coded `false` at build time and **cannot be overridden at runtime**. Setting it in the env has no effect inside a TEE deployment. See [TEE reference](/providers/full/tee-reference).

Storage and forwarding are independent. `PROXY_STORE_CHAT_CONTEXT` persists chats for `/v1/chats`. `PROXY_FORWARD_CHAT_CONTEXT` (default `false`) prepends that stored history onto the next prompt — opt-in for clients that send only the latest turn. OpenAI-compatible clients that already send the full `messages[]` should leave forwarding off.

---

## Application

| Variable | Default | Notes |
|----------|---------|-------|
| `APP_RESET_KEYCHAIN` | `false` | Set `true` to reset the macOS keychain entry on start |
| `AUTH_CONFIG_FILE_PATH` | `./proxy.conf` | Path to the auth config file (no inline twin) |
| `COOKIE_FILE_PATH` | `./.cookie` | Path to admin cookie file |
| `COOKIE_CONTENT` | (unset) | **Seeds the cookie file on first start only.** See precedence above. |

## Blockchain

| Variable | Default | Notes |
|----------|---------|-------|
| `ETH_NODE_CHAIN_ID` | `8453` (mainnet) / `84532` (testnet) | BASE chain ID |
| `ETH_NODE_ADDRESS` | (unset, falls back to built-in public RPC round-robin) | RPC endpoint URL (HTTPS or WSS — must match `ETH_NODE_USE_SUBSCRIPTIONS`). **Set explicitly for any real deployment** — the public round-robin fallback is rate-limited and unreliable; missed events break the node silently. |
| `ETH_NODE_LEGACY_TX` | `false` | Set `true` to disable EIP-1559 |
| `BLOCKSCOUT_API_URL` | `https://base.blockscout.com/api/v2` | Block explorer API |
| `EXPLORER_RETRY_DELAY` | `5` | Delay between explorer retries (seconds) |
| `EXPLORER_MAX_RETRIES` | `5` | Max explorer retries |
| `ETH_NODE_USE_SUBSCRIPTIONS` | `false` | `true` to use WSS subscriptions; `false` for HTTP polling |
| `ETH_NODE_POLLING_INTERVAL` | `10` | Polling interval (seconds, only when `ETH_NODE_USE_SUBSCRIPTIONS=false`) |
| `ETH_NODE_MAX_RECONNECTS` | `30` | Max reconnect attempts |

## Environment

| Variable | Default | Notes |
|----------|---------|-------|
| `ENVIRONMENT` | `development` | Use `production` in deployments |

## Marketplace

| Variable | Default | Notes |
|----------|---------|-------|
| `DIAMOND_CONTRACT_ADDRESS` | mainnet `0x6aBE1d282f72B474E54527D93b979A4f64d3030a`; testnet `0xA328196f2438DADA5ab729E39388D86896c27c85` | Diamond marketplace contract |
| `MOR_TOKEN_ADDRESS` | mainnet `0x7431aDa8a591C955a994a21710752EF9b882b8e3`; testnet `0x5C80Ddd187054E1E4aBBfFCD750498e81d34FfA3` | MOR ERC-20 token |
| `WALLET_PRIVATE_KEY` | (unset) | Wallet private key. Falls back to system keychain when unset. See precedence above. |

## Logging

| Variable | Default | Notes |
|----------|---------|-------|
| `LOG_COLOR` | `false` | Colored console output |
| `LOG_FOLDER_PATH` | (unset) | If set, also write logs to files in this directory |
| `LOG_IS_PROD` | `false` | Production log format |
| `LOG_JSON` | `false` | JSON-formatted log lines |
| `LOG_LEVEL_APP` | `warn` | App-level log level: `debug` `info` `warn` `error` `dpanic` `panic` `fatal` |
| `LOG_LEVEL_TCP` | `warn` | TCP layer log level |
| `LOG_LEVEL_ETH_RPC` | `warn` | Ethereum RPC log level |
| `LOG_LEVEL_STORAGE` | `warn` | Badger storage log level |

For TEE images logging is **frozen** to production / JSON / minimal at build time — these vars have no effect inside a `-tee` image.

## Network ports & URLs

| Variable | Default | Notes |
|----------|---------|-------|
| `WEB_ADDRESS` | `0.0.0.0:8082` | Bind for the HTTP/Swagger API |
| `WEB_PUBLIC_URL` | `http://localhost:<WEB_ADDRESS port>` | Public-facing URL of the API. **Set explicitly** for any provider that consumers must reach. |
| `PROXY_ADDRESS` | `0.0.0.0:3333` | Bind for the consumer-to-provider TCP proxy |

## Proxy state & configs

| Variable | Default | Notes |
|----------|---------|-------|
| `MAX_CACHED_DESTS` | `5` | Max cached provider destinations |
| `PROXY_STORAGE_PATH` | `./data/badger/` | Local Badger / runtime state |
| `PROXY_STORE_CHAT_CONTEXT` | `true` | Persist chat context. **Frozen `false` in `-tee` images.** |
| `PROXY_FORWARD_CHAT_CONTEXT` | `false` | Prepend stored history to prompts. Opt-in for clients that send only the latest turn (the bundled Desktop UI sets it to `true`); leave off for OpenAI-compatible clients that already send the full transcript. |
| `MODELS_CONFIG_PATH` | `./models-config.json` | See [models-config](/reference/models-config). See precedence above. |
| `MODELS_CONFIG_CONTENT` | (unset) | **Seeds the models config file on first start only.** See precedence above. |
| `AGENT_CONFIG_PATH` | (built-in default) | Local agent registry path |
| `AGENT_CONFIG_CONTENT` | (unset) | **Seeds the agent config file on first start only.** Same rules as the models pair. |
| `RATING_CONFIG_PATH` | `./rating-config.json` | See [rating-config](/reference/rating-config) |

## Timeouts & retries

| Variable | Default | Notes |
|----------|---------|-------|
| `LLM_TIMEOUT` | (depends on release) | P-Node → backend LLM timeout (streaming + non-streaming) |
| `CNODE_PNODE_TIMEOUT` | (depends on release) | Per-attempt timeout for C-Node waiting for P-Node first response |
| `CNODE_PNODE_MAX_RETRIES` | (depends on release) | Max retries for C-Node → P-Node read timeout (chat / embeddings) |
| `CNODE_PNODE_AUDIO_MAX_RETRIES` | (depends on release) | Max retries for audio transcription / speech routes |

For exact defaults check the source of truth ([`config.go`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/proxy-router/internal/config/config.go)) — these can change between releases as we tune the network.

## Model health self-checks (provider)

On provider nodes the proxy-router periodically probes every model from `models-config.json` that has an **active bid from this provider**: LLM models get a small dynamically generated arithmetic prompt (correctness is graded), embedding models get an embeddings request. Untagged / unknown-type models are probed as LLM unless their tags name image/video/audio/music; a failed guessed probe reports `skipped` (with `errorKind`), not `unhealthy`. STT/TTS and media-tagged models are skipped without a probe. The report covers the whole ecosystem of bids and configured models — active bids whose model is missing from `models-config.json` are flagged as `no_model_configured` (capacity being sold with no backend to serve it). The cached per-model results (on-chain `modelId` and registered model name, `bidId`, status, `errorKind` plus upstream `httpStatus` on failure, `lastHealthy` epoch, latency, prompt correctness — never the private `models-config.json` `modelName`, `apiUrl`, or `apiKey`) are exposed in the `models` field of `GET /healthcheck`, in the `network.ping` (pong) response on the `:3333` MOR RPC port, and in `POST /proxy/provider/ping`, so consumers can see a provider's model health before opening a session. The [active.mor.org](/ecosystem/active-status) sweep copies these stamps into ALL / ACTIVE / GATEWAY. Full operational guide: [Model health self-report](/providers/full/model-health).

| Variable | Default | Notes |
|----------|---------|-------|
| `MODEL_HEALTH_CHECK_DISABLED` | `false` | Set `true` to disable the loop (the `models` field disappears from `/healthcheck`) |
| `MODEL_HEALTH_CHECK_INTERVAL` | `1h` | How often models are probed; results are cached between runs |
| `MODEL_HEALTH_CHECK_TIMEOUT` | `60s` | Per-model probe timeout |
| `MODEL_HEALTH_CHECK_PROBE_DELAY` | `2s` | Pause between per-model probes so many-model providers don't burst their upstream into a rate limit |
| `MODEL_HEALTH_MAX_CONSECUTIVE_ERRORS` | `3` | Consecutive real-session prompt failures after which the model is reported `unhealthy` (`errorKind: session_errors`) immediately, without waiting for the next scheduled probe |

## Session health policy (consumer)

On consumer nodes, `SESSION_HEALTH_POLICY` controls how strictly the pre-open healthcheck pings gate provider selection when opening a session **by model ID** (the failover loop). Opening by a specific bid ID is unaffected.

| Variable | Default | Notes |
|----------|---------|-------|
| `SESSION_HEALTH_POLICY` | `permissive` | One of `permissive`, `preferred`, `strict` — see below |

| Policy | Behavior |
|--------|----------|
| `permissive` | Default, same as previous releases: skip only providers that self-report the bid's model as `unhealthy`, `tee_unverified`, or `no_model_configured`. Providers with an unknown or missing report are still tried. |
| `preferred` | When at least one provider reports the model `healthy` or `degraded`, only those providers are tried and unknown/missing reports are skipped. When none does, the open falls back to `permissive` so the model is not black-holed. |
| `strict` | Only providers that report the model `healthy` are tried. `degraded` and unknown reports are excluded. |

**Single-bid caveat** (`healthPolicySingleBid: permissive`, fixed): when the hard filters (omitted provider, own bids) leave a single candidate provider for the model, `preferred`/`strict` are forced to `permissive` for that open so the sole provider can still be tried — single-provider models must remain usable.

## TEE (Phase 1 + Phase 2 attestation)

These are only consulted when at least one model in the marketplace is `tee`-tagged. Otherwise they are unused.

| Variable | Default | Notes |
|----------|---------|-------|
| `TEE_PORTAL_URL` | `https://secretai.scrtlabs.com/api` | SecretAI Portal used to verify raw TDX CPU quotes |
| `TEE_IMAGE_REPO` | `ghcr.io/morpheusais/morpheus-lumerin-node-tee` | GHCR repo where the consumer-side P-Node attestation fetches the signed golden manifest via cosign |
| `ARTIFACT_REGISTRY_URL` | `https://cdn.jsdelivr.net/npm/secretvm-verify@0.13.0/data/tdx.csv` | SecretVM TDX artifact registry CSV (MRTD + RTMR0-2 lookup). Served from the published [secretvm-verify](https://www.npmjs.com/package/secretvm-verify) package after the GitHub repo was removed. |
| `SEV_ARTIFACT_REGISTRY_URL` | `https://cdn.jsdelivr.net/npm/secretvm-verify@0.13.0/data/sev.json` | SecretVM SEV-SNP artifact registry JSON |
| `ARTIFACT_REGISTRY_REFRESH_INTERVAL` | `1h` | How often the proxy-router re-downloads the artifact registry |

See [TEE overview](/concepts/tee-overview) and [TEE reference](/providers/full/tee-reference).

## IPFS (optional)

The proxy-router talks to a local Kubo API only when you set a real **multiaddr**. Unset `IPFS_MULTADDR` means IPFS is **disabled** — that is the headless default since v7.11. There is **no** `localhost:5001` default (that string is not a multiaddr and used to log ERROR on every boot). The bundled desktop already sets `/ip4/127.0.0.1/tcp/5001` when it runs Kubo.

| Variable | Default | Notes |
|----------|---------|-------|
| `IPFS_MULTADDR` | *(empty — disabled)* | Kubo API multiaddr, e.g. `/ip4/127.0.0.1/tcp/5001`. Invalid values log a warning and disable IPFS. |
| `IPFS_DISABLED` | `false` | Set `true` to force-disable even when `IPFS_MULTADDR` is set. |

## System tuning (optional sysctl tweaks)

These map to OS-level adjustments and only apply when `SYS_ENABLE=true`.

| Variable | Default | Notes |
|----------|---------|-------|
| `SYS_ENABLE` | `false` | Set `true` to apply the values below at startup |
| `SYS_LOCAL_PORT_RANGE` | `1024 65535` | |
| `SYS_NET_DEV_MAX_BACKLOG` | `100000` | |
| `SYS_RLIMIT_HARD` | `524288` | |
| `SYS_RLIMIT_SOFT` | `524288` | |
| `SYS_SOMAXCONN` | `100000` (Linux) / `2048` (Darwin/macOS) | |
| `SYS_TCP_MAX_SYN_BACKLOG` | `100000` | |

## Mainnet vs testnet quick switch

Comment / uncomment the appropriate block in `.env`:

```bash
# MAINNET
ETH_NODE_CHAIN_ID=8453
DIAMOND_CONTRACT_ADDRESS=0x6aBE1d282f72B474E54527D93b979A4f64d3030a
MOR_TOKEN_ADDRESS=0x7431aDa8a591C955a994a21710752EF9b882b8e3
BLOCKSCOUT_API_URL=https://base.blockscout.com/api/v2

# TESTNET (Base Sepolia)
# ETH_NODE_CHAIN_ID=84532
# DIAMOND_CONTRACT_ADDRESS=0xA328196f2438DADA5ab729E39388D86896c27c85
# MOR_TOKEN_ADDRESS=0x5C80Ddd187054E1E4aBBfFCD750498e81d34FfA3
# BLOCKSCOUT_API_URL=https://base-sepolia.blockscout.com/api/v2
```

## Full annotated dump

For the complete annotated reference (every variable, every example value), see [`docs/proxy-router.all.env`](https://github.com/MorpheusAIs/Morpheus-Lumerin-Node/blob/main/docs/proxy-router.all.env) in the repo.
