> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/api-reference/guides/networks.md).

# Networks

How every Nexus Exchange interface selects a network, whether testnet, mainnet, or a custom target you describe yourself.

Every Nexus Exchange interface talks to exactly one **network**, chosen when you construct the client. A network is not a release channel. It decides whose money is at stake. **Testnet** carries synthetic play funds and **mainnet** will carry real funds. Selecting one bundles everything that differs between them: the REST and WebSocket targets, whether a faucet exists, and the signing domain your requests are scoped to. You pick a network rather than assembling a URL. For the current base URLs, see [APIs & Rates](https://docs.nexus.xyz/exchange/apis-and-rates).

Those two are the published networks. A deployment you run yourself is a [`Custom` network](#the-custom-network). It carries the same bundle, but you describe it instead of the client resolving it.

### The published networks

| Network   | Funds                                             | Faucet | Availability                                                            |
| --------- | ------------------------------------------------- | ------ | ----------------------------------------------------------------------- |
| `testnet` | Synthetic USDX with no real-world value           | Yes    | Live today. The default in every interface.                             |
| `mainnet` | Real funds, as USDX bridged from Ethereum Mainnet | No     | Will launch with the real-funds Exchange. Not reachable yet; see below. |

Testnet is the default everywhere on purpose, because defaulting to real funds would be unsafe.

**Mainnet is not reachable yet.** Its public host is not live, so no interface resolves a mainnet target on your behalf. Rather than send your requests somewhere plausible but wrong, each one fails closed. The Python and TypeScript clients raise at construction, and the MCP server refuses to start. The Rust client (and therefore the CLI) builds fine but rejects every request locally, before any bytes leave the process. The only way through is to name the target yourself. Python, TypeScript, and the MCP server accept `mainnet` when it comes with an explicit base URL (see [Pointing at a host without describing it](#pointing-at-a-host-without-describing-it)), which is how you reach a real-funds deployment before the durable host is live. In Rust the override is a separate constructor that carries no network at all, so it targets a URL rather than mainnet. No interface guesses a real-funds target for you, because that failure cannot be rehearsed. When mainnet launches, this page and each interface's release notes will say so.

### Selecting a network

| Interface  | Select a network                                          | Default   |
| ---------- | --------------------------------------------------------- | --------- |
| Python     | `Client(network=Network.TESTNET)`                         | `testnet` |
| TypeScript | `new Client({ network: Network.Testnet })`                | `testnet` |
| Rust       | `Config::new(Network::Testnet)`                           | `testnet` |
| CLI        | `--network <mainnet\|testnet\|LABEL>`, or `NEXUS_NETWORK` | `testnet` |
| MCP server | `NEXUS_EXCHANGE_NETWORK`                                  | `testnet` |

The CLI's `--network` also accepts the label of a custom target declared in its config file, and the MCP server accepts `NEXUS_EXCHANGE_NETWORK=custom` alongside a described bundle. See [Describing a custom target](#describing-a-custom-target).

An unrecognized network name is always an error. No interface falls back to a default. The interfaces treat an unknown identifier as real funds until proven otherwise, so you must correct it. Retired release-channel names are rejected with a pointer to their replacement. `stable` named a host that serves testnet, so `testnet` is its direct equivalent.

### Example

```python
from nexus_exchange import Client, Network

client = Client(network=Network.TESTNET, api_key=..., api_secret=...)
```

```typescript
import { Client, Network } from "@nexus-xyz/exchange-ts";

const client = new Client({ network: Network.Testnet, apiKey: "…", apiSecret: "…" });
```

```rust
use nexus_exchange::{Config, Network};

let config = Config::new(Network::Testnet);
```

```bash
nexus --network testnet markets
```

### The `Custom` network

The two networks above are the published ones. A deployment you run yourself, such as a private stage, a preview environment, or an indexer on your own infrastructure, is a **`Custom` network**. It is a third kind of target that you describe, not a URL attached to one of the two.

It is a network rather than an address because a private deployment still needs everything a named network bundles, and a URL supplies none of it. `Custom` carries the same bundle, supplied by you:

| Part of the bundle        | Who supplies it                                                                                                                                                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **REST base**             | You. Required.                                                                                                                                                                                                                                          |
| **Direct `/api/v1` base** | You, where the deployment splits it from the REST base. Defaults to the REST base, which is where a single-host deployment serves it.                                                                                                                   |
| **WebSocket origin**      | You, in the interfaces that let a deployment split it out. The Rust SDK and CLI never derive one, because deriving it would pair a stream token with an origin that did not mint it. See [the field-set note](#the-two-field-sets-differ-deliberately). |
| **Funds**                 | You. Required, no default. See below.                                                                                                                                                                                                                   |
| **Faucet**                | You. Assumed **absent** until declared, so a funding call cannot route at a faucet that is not there.                                                                                                                                                   |
| **Signing domain**        | Read from this deployment's `GET /metadata`, or supplied by you. Never guessed.                                                                                                                                                                         |
| **Label**                 | You. Required, because it is the credential namespace.                                                                                                                                                                                                  |

#### Funds is a required tri-state

`funds` is `real`, `play`, or `unknown`, and it has **no default**. Neither boolean answer is safe to assume. A staging deployment of the real-funds Exchange behaves like real funds, and a preview environment with production data behind it is not play money just because it is not the published host. Only the operator knows.

`unknown` **fails closed**. It is a legitimate answer, since you may not know, and saying so is better than guessing. The interfaces then refuse the operations guarded on real funds rather than letting them through. If a command or tool that moves value declines on a custom target, an undeclared `funds` is the first thing to check.

#### The label is required, and it is a credential namespace

Every `Custom` target carries a caller-chosen label, constrained to `[A-Za-z0-9._-]` with a maximum of 64 characters. `.` and `..` are rejected outright. So are the names the built-in networks already answer to (`mainnet`, `testnet`, `custom` itself, and the reserved `local`), which are legal under that character set but would address another target's credentials by naming it.

The label is the key your stored credentials are namespaced under, so it reaches the filesystem in the clients that persist configuration. An unvalidated label containing `/` or `..` is a path traversal, and the credential it would address belongs to a different target. The character set and the length cap hold across every interface, and each enforces them itself rather than inheriting them. The MCP server is not built on the Rust SDK and carries its own copy of the rule. The reserved names are the one part that is not yet uniform. The Rust SDK and the CLI refuse a built-in network's name, and the MCP server does not. Choose a label that collides with none of them rather than relying on the refusal to catch it.

You choose the labels in your own configuration. `dev`, `preview` and `example` are all fine.

#### The signing domain is never guessed

A `Custom` target does not inherit a signing domain from anywhere. The client reads the EIP-712 chain id from the deployment's own `GET /metadata`, or takes one you supply. If it has neither, it **refuses to sign** rather than reusing a value from another network.

Understand this rule before you hit it, because the opposite failure is silent. A signature made under the wrong domain can be *valid on a different network*. Refusing is the only safe answer.

### Pointing at a host without describing it

Every interface still accepts a bare base URL:

| Interface  | Bare base URL                                               |
| ---------- | ----------------------------------------------------------- |
| Python     | `base_url=`, plus `direct_base_url=` for the direct base    |
| TypeScript | `baseUrl`                                                   |
| Rust       | `Config::with_base_url(…)`, plus `.with_direct_base_url(…)` |
| CLI        | `--base-url <URL>`, or `NEXUS_BASE_URL`                     |
| MCP server | `NEXUS_EXCHANGE_API_URL`                                    |

This is the shortcut, not the documented path. In the MCP server it is formally deprecated. It still works unchanged but prints a notice that points at the bundle. A bare URL builds a `Custom` target with **undeclared funds** and **no signing domain**, so the real-funds-guarded operations refuse and the client will not sign. That is enough for a read-only look at a host, and on purpose not enough to trade against one.

To do more than read, describe the target instead of naming an address.

### Describing a custom target

Two interfaces take a full bundle from configuration rather than from constructor arguments.

**MCP server.** Set `NEXUS_EXCHANGE_NETWORK=custom`, then:

| Variable                       | Meaning                                                                                            |
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
| `NEXUS_EXCHANGE_API_URL`       | REST base. Required.                                                                               |
| `NEXUS_EXCHANGE_NETWORK_LABEL` | The label. Required.                                                                               |
| `NEXUS_EXCHANGE_FUNDS`         | `real`, `play` or `unknown`. Required.                                                             |
| `NEXUS_EXCHANGE_FAUCET`        | Whether a faucet exists here. Absent means no.                                                     |
| `NEXUS_EXCHANGE_GATEWAY_PATH`  | Where the gateway path sits under the origin: `/api/exchange` (default) or `/` for a bare indexer. |

Setting any of these **without** `NEXUS_EXCHANGE_NETWORK=custom` is an error, not a silent no-op. Half a bundle applied quietly would leave you believing you had configured a safety property that is not in effect.

**CLI.** Declare the target under `custom_networks` in its config file (`$XDG_CONFIG_HOME/nexus/config.json`, falling back to `~/.config/nexus/config.json`) and select it by label:

```json
{
  "custom_networks": {
    "dev": {
      "base_url": "https://exchange.example.com/api/exchange",
      "direct_base_url": "https://api.example.com",
      "funds": "play",
      "ws_url": "wss://stream.example.com",
      "faucet": true
    }
  }
}
```

```bash
nexus --network dev markets
```

A `chain_id` belongs in that entry too, once you have read it from this deployment's own `GET /metadata`. The example omits it on purpose rather than show a placeholder value. There is no sentinel that means *unknown*. The CLI takes any number you write as the EIP-712 domain, so a made-up one produces a signature under the wrong domain, where leaving it out produces a refusal.

An undeclared label is an error. The CLI validates a declared label when you select it, not when it reads the file, so a mistake in a stage you are not using does not break every other command.

#### The two field sets differ, deliberately

Neither list is a subset of the other. There are three differences:

* **`chain_id` is CLI-only.** The MCP server never produces an EIP-712 signature (it authenticates with HMAC), so it has no use for a signing domain, and carrying one would be a second place for it to go stale.
* **`gateway_path` is MCP-only.** It is the same setting as the CLI's `direct_base_url`, approached from the other side. The MCP server takes the path and derives the direct base, and the CLI takes the direct base and needs no path.
* **`ws_url` is CLI-only.** The MCP server derives its WebSocket origin from the gateway base, which the API contract supports because the stream paths carry no separate host. The CLI accepts an explicit one because a deployment may put its stream on a different host. So you can describe a custom target with a separate WebSocket origin to the CLI, but not yet to the MCP server.

### What a network carries

Selecting a network resolves all of the following together, which is why it is one choice rather than several:

* **REST base** for the trading and account endpoints.
* **WebSocket bases** for public market data and for the authenticated stream, where the interface streams at all. The Python SDK ships no WebSocket client. The Rust SDK refuses to connect on testnet until the published stream host is live, rather than pair a stream token with an origin that did not mint it, so supply an explicit WebSocket URL there.
* **Whether a faucet exists.** Among the published networks, synthetic funding is testnet-only, and mainnet has none. A custom target declares its own, and is assumed to have none until it does.
* **Whether balances are real money.** A single flag to branch on before anything irreversible, rather than pattern-matching a hostname.
* **The EIP-712 signing domain**, which scopes agent registration to this network. The server is the authority for its chain id, so read it from `GET /metadata` for the network you are connected to. A client that cannot obtain one refuses to sign rather than reusing a value from elsewhere.

### Credentials and network isolation

Session tokens, HMAC API keys, and agent registrations are minted **per network** and are invalid on every other. A key configured for one network will not authenticate against another, so a credential exposed on testnet cannot sign for real funds.

A client is bound to its network for its whole lifetime, and there is no setter. Switching networks means constructing a new client with that network's own credentials, and never carrying a signature, nonce, or agent registration across.

The CLI stores credentials **per network**, keyed by network name. For a custom target it keys them by label rather than URL, so two stages on the same host keep separate credentials. `--network` therefore selects the target *and* the credential set together. It cannot mint a credential. Switching to a network you have not set up yet leaves you with no stored key for it, so run `nexus setup` for that network, or pass its credentials alongside `--network` on the command line.

#### What binds a key to a network

**The host you mint it against.** Key creation has no network parameter. `POST /keys` records the network of the instance that served the request, and the key is valid only there from then on. So the base URL you point at when you create a key *is* the binding decision. There is nothing to set and nothing to confirm.

Plan around two consequences:

* Creating a key while pointed at one network and then trading against another cannot work, however the client is configured afterwards.
* You need a separate key per network you intend to use, minted separately against each.

#### What a wrong-network key looks like

It looks exactly like a key that does not exist: **HTTP 401 Unauthorized** with this body:

```json
{
  "code": "unauthorized"
}
```

That response is **indistinguishable, on purpose,** from a bad signature, an unknown key id, or a missing header. A rejected key must not be identifiable as "real, but registered elsewhere", because that would let someone confirm a guessed key id is live on another network. The API therefore tells you nothing beyond "this request is not authenticated."

**An authentication failure after you change which network you target is far more likely to be the network than a revoked key.** Check which host minted the key before assuming it was deleted or the secret was lost. Nothing in the response will point you there.

#### Keys minted before per-network binding

Keys created before network binding existed carry no network of their own. They keep working on testnet, which adopts such keys as its own and records the network on them. Mainnet will **not** accept them when it launches, because an unmarked key proves nothing about where it came from. Mint a fresh key against mainnet rather than expecting an existing one to carry over.

For the full authentication walkthrough, from wallet sign-in to a signed request, see the [Quickstart](https://docs.nexus.xyz/exchange/trading/quickstart).

### Related

* [APIs & Rates](https://docs.nexus.xyz/exchange/apis-and-rates): current base URLs and rate limits
* [SDKs & Tools](/api-reference/guides/sdks.md)
* [Portfolio & Account State](/api-reference/guides/portfolio.md)
* [Quickstart](https://docs.nexus.xyz/exchange/trading/quickstart)

> **Status:** development preview on testnet. Mainnet has not launched and is not reachable yet. The network selector replaced the earlier `{stable, beta}` release-channel selector. Pin to a released version in production and check each interface's release notes before upgrading. Testnet credentials and balances have no real-world value.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.nexus.xyz/api-reference/guides/networks.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
