> 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

Every Nexus Exchange interface talks to exactly one **network**, chosen when you construct the client. A network is not a release channel: it is whose money is at stake. **Testnet** carries synthetic play funds, **mainnet** will carry real funds, and **local** is a developer convenience. 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 — so 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 three are the published networks. A deployment you run yourself is a [`Custom` network](#the-custom-network) — the same bundle, described by you rather than resolved for you.

### The three 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 — USDX bridged from Ethereum Mainnet | No     | Will launch with the real-funds Exchange. Not reachable yet; see below. |
| `local`   | Synthetic, against an indexer you run yourself  | Yes    | For local development. Not a public network.                            |

Testnet is the default everywhere, deliberately: defaulting to real funds would be the unsafe direction.

**Mainnet is not reachable yet.** Its public host is not live, so no interface will resolve a mainnet target on your behalf — rather than send your requests somewhere plausible-but-wrong, each one fails closed in the way that fits it: the Python and TypeScript clients raise at construction, the MCP server refuses to start, and the Rust client (and therefore the CLI) builds fine but rejects every request locally, before any bytes leave the process. Naming the target yourself is the one way through: 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 a real-funds deployment is reached 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. Nothing is guessed for you, and that is the point: guessing a real-funds target is the one failure that 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\|local\|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, and none falls back to `local` — an unknown identifier is treated as real funds until proven otherwise, so it must be corrected rather than guessed at. 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 local markets
```

### The `Custom` network

The three networks above are the published ones. A deployment you run yourself — a private stage, a preview environment, an indexer on your own infrastructure — is a **`Custom` network**: a fourth kind of target that you describe, rather than a URL bolted onto one of the three.

It is a network rather than an address because everything a named network bundles still has to be answered for a private deployment, and a URL answers 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 pairing a stream token with an origin that did not mint it is the failure that would follow. 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 — 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 is real-funds-shaped, 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 — you may genuinely not know, and saying so is better than guessing — but 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, and so are the names the built-in networks already answer to — `mainnet`, `testnet`, `local`, and `custom` itself — which are legal under that character set but would address another target's credentials by naming it.

The constraint is not cosmetic. 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, each enforcing 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, the MCP server does not. Choose a label that collides with none of them rather than relying on the refusal to catch it.

Labels in your own configuration are yours to choose; `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 — and if it has neither, it **refuses to sign** rather than reusing a value from another network.

This is the one rule worth understanding before you hit it, because the failure is silent in the other direction: 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 surface |
| 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, still working unchanged but printing 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 usable for a read-only look at a host, and it is deliberately 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 surface hangs off the origin: `/api/exchange` (default) or `/` for a bare indexer. |

Setting any of these **without** `NEXUS_EXCHANGE_NETWORK=custom` is an error rather than 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` — it is deliberately omitted above rather than shown with a placeholder value. There is no sentinel that means *unknown*: any number you write is taken as the EIP-712 domain, so a made-up one buys a signature under the wrong domain instead of the refusal that leaving it out gives you.

An undeclared label is an error, and a declared one is validated when it is selected rather than when the file is read — 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. Three differences, each with a reason:

* **`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**, and is the same knob as the CLI's `direct_base_url` spelled the other way round: the MCP server takes the path and derives the direct base, 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 a custom target whose WebSocket origin is genuinely separate can be described to the CLI, and cannot yet be described 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 surface.
* **WebSocket bases** for public market data and for the authenticated stream, where the interface streams at all — the Python SDK ships no WebSocket client, and 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- and local-only; 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. Its chain id is server-authoritative: 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 — 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 — and for a custom target, by its label rather than its URL, so two stages on the same host keep separate credentials. `--network` therefore selects the target *and* the credential set together. What it cannot do is mint one: 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.** There is no network parameter on key creation: `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, made implicitly — there is nothing to set and nothing to confirm.

Two consequences worth planning around:

* 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",
  "message": "Authentication required"
}
```

That response is **deliberately indistinguishable** 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."

The practical implication: **an authentication failure after changing 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 and local, which adopt such keys as their own and record the network on them. They will **not** be accepted on mainnet when it launches: an unmarked key proves nothing about where it came from, and real funds will not extend trust on an assumption. Mint a fresh key against mainnet rather than expecting an existing one to carry over.

For the full authentication walkthrough — wallet sign-in through 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
* [Interfaces overview](/api-reference/readme.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 axis replaced the earlier `{stable, beta, local}` 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
