> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PrivateLink

> Reach Monaco's gRPC trading API and WebSocket feeds from your own AWS VPC over AWS PrivateLink: what changes, how it works, and how to set it up.

AWS PrivateLink gives a colocated market maker a private connection from its own AWS VPC to Monaco's **gRPC trading API** and **WebSocket feeds**. Traffic stays on the AWS network and never crosses the public internet or the Cloudflare edge, and order entry gets **4× the public order budgets**.

<Note>
  PrivateLink is available on **staging (testnet)** today, in `us-east-1`. It is offered on request for a fixed monthly cost. Reach out to the Monaco team to get started.
</Note>

## What changes

| | Public | PrivateLink |
| - | - | - |
| gRPC trading | `staging.apimonaco.xyz:443` | `pl-staging.apimonaco.xyz:443` |
| WebSocket feeds | `wss://ws-staging.apimonaco.xyz/ws` | `wss://pl-staging.apimonaco.xyz:8443/ws` |
| REST | `https://staging.apimonaco.xyz/api` | Not available, use the public host |
| Order budgets | 1× | **4×** (2,000 creation items/s, 4,000 risk-reduction items/s) |
| Per-address budgets: [authentication](/developers/rate-limits#authentication) and [public application reads](/developers/rate-limits#public-application-reads) | 60 requests/min (burst 120) per client address, each | Not metered, since PrivateLink carries no client address |
| Network path | Internet → Cloudflare → Monaco | Your VPC → interface endpoint → Monaco, same Availability Zone |
| Edge flood blocks | Apply | Never apply, since the edge is not on the path |

What stays the same:

* **Authentication.** The rules are the public hosts' rules. Trading and account RPCs and the WebSocket `Authenticate` handshake are signed with your ed25519 session key; market data, public WebSocket channels, and health checks need no signature. PrivateLink is a network path, not an authentication boundary, so it relaxes none of this. See [Wallets & Auth](/developers/accounts/wallets).
* **The API surface.** Same gRPC services and methods, same WebSocket channels, events, and message limits.
* **Every other budget.** Apart from the order and per-address budgets above, reads, movements, settings writes, WebSocket limits, and resting-order caps match the public path. See [Rate Limits](/developers/rate-limits).

## How it works

```
  Your AWS account                          Monaco
 ┌──────────────────────────┐            ┌──────────────────────────────────────┐
 │ trading hosts            │            │ endpoint service                     │
 │      │                   │  AWS       │   (allowlisted accounts only,        │
 │      ▼                   │  Private   │    each connection accepted by hand) │
 │ interface endpoint ──────┼──Link─────►│      │                               │
 │ (subnet in Monaco's AZ)  │            │      ▼                               │
 └──────────────────────────┘            │ network load balancer (TLS)          │
                                         │   :443  ──► gRPC trading API         │
                                         │   :8443 ──► WebSocket feeds          │
                                         └──────────────────────────────────────┘
```

* **Admission.** Only AWS accounts Monaco has allowlisted can create an endpoint to the service, and Monaco accepts each endpoint connection by hand. Past that, each call is authenticated exactly as on the public path.
* **The ingress is decided by the listener, not by you.** A connection that arrives over PrivateLink is metered against the PrivateLink order budgets. You send nothing different, and nothing you send can change it. The `x-ratelimit-*` budget headers report the PrivateLink figures. PrivateLink and public order budgets are separate buckets, and the family budget applies to each at 4×.
* **TLS.** TLS terminates at Monaco's load balancer with the public `*.apimonaco.xyz` certificate, so standard WebPKI roots verify it, provided your client verifies against `pl-staging.apimonaco.xyz`. The `:443` listener offers HTTP/2 only (ALPN `h2`), which every standard gRPC client negotiates. The `:8443` listener accepts the HTTP/1.1 upgrade WebSocket needs. Plaintext is not accepted on either port.
* **One Availability Zone.** The service runs in a single Availability Zone, the same one as the Monaco API services behind it, and your endpoint must sit in that zone too, so traffic never crosses an AZ boundary. An outage of that AZ takes down the public hosts as well, so they are no fallback for it. They are a fallback for any failure confined to the PrivateLink path: on your side, such as your endpoint, its security group, or your DNS resolution, and on Monaco's side, such as the endpoint service, its load balancer, or a listener, while the API services behind it stay up.
* **One region.** An interface endpoint can only reach a service in its own region. Staging is in `us-east-1`.
* **Deploys.** When Monaco rolls API tasks, connections on a draining task are reset after 30 seconds. Reconnect with backoff and resync, exactly as on the public path. See [Reconnect and resync](/developers/market-maker-runbook#4-reconnect-and-resync).

## Set up

You need an AWS account with a VPC in `us-east-1`. For private DNS, the VPC must have both `enableDnsSupport` and `enableDnsHostnames` turned on.

<Steps>
  <Step title="Request access">
    Send the Monaco team the AWS principal that will create the endpoint: your account root (`arn:aws:iam::<account-id>:root`) or a specific IAM role ARN.

    Once Monaco has allowlisted it, you receive:

    * the **endpoint service name**, `com.amazonaws.vpce.us-east-1.vpce-svc-…`
    * the **Availability Zone ID** the service runs in, for example `use1-az4`
  </Step>

  <Step title="Pick a subnet in that AZ ID">
    Match on the AZ **ID**, never the AZ name. AZ names such as `us-east-1a` map to different physical zones in different AWS accounts.

    ```bash theme={null}
    aws ec2 describe-subnets --region us-east-1 \
      --filters Name=vpc-id,Values=<your-vpc-id> \
      --query 'Subnets[?AvailabilityZoneId==`<az-id-from-monaco>`].SubnetId'
    ```

    If none comes back, create a subnet in that AZ ID first.
  </Step>

  <Step title="Create a security group for the endpoint">
    Allow inbound TCP `443` (gRPC) and `8443` (WebSocket feeds) from your trading hosts.
  </Step>

  <Step title="Create the interface endpoint">
    ```bash theme={null}
    aws ec2 create-vpc-endpoint --region us-east-1 \
      --vpc-endpoint-type Interface \
      --vpc-id <your-vpc-id> \
      --service-name <service-name-from-monaco> \
      --subnet-ids <subnet-id> \
      --security-group-ids <endpoint-security-group-id> \
      --no-private-dns-enabled
    ```

    The endpoint starts in `pendingAcceptance`. Private DNS is enabled in a later step, once the connection is accepted.
  </Step>

  <Step title="Send Monaco the endpoint ID">
    Send the `vpce-…` ID from the previous step. Once Monaco accepts the connection, the endpoint state becomes `available`:

    ```bash theme={null}
    aws ec2 describe-vpc-endpoints --region us-east-1 \
      --vpc-endpoint-ids <vpce-id> --query 'VpcEndpoints[].State'
    ```
  </Step>

  <Step title="Enable private DNS">
    ```bash theme={null}
    aws ec2 modify-vpc-endpoint --region us-east-1 \
      --vpc-endpoint-id <vpce-id> --private-dns-enabled
    ```

    Inside your VPC, and only there, `pl-staging.apimonaco.xyz` now resolves to your endpoint's private addresses. The hostname has no public DNS record, so outside a VPC with private DNS on it does not resolve at all.

    The change takes several minutes to reach your VPC's resolver: in Monaco's own test it took about seven minutes before the hostname resolved. Connections through a newly accepted endpoint can also time out for the first few minutes. Wait and retry before troubleshooting.
  </Step>

  <Step title="Verify">
    From a trading host:

    ```bash theme={null}
    dig +short pl-staging.apimonaco.xyz      # private addresses in your VPC's range
    grpc_health_probe -tls -addr=pl-staging.apimonaco.xyz:443
    ```

    [`grpc_health_probe`](https://github.com/grpc-ecosystem/grpc-health-probe) calls the standard `grpc.health.v1.Health/Check` service and should print `status: SERVING`.
  </Step>
</Steps>

## Connect

### gRPC

Point your channel at `https://pl-staging.apimonaco.xyz` and turn on HTTP/2 keepalive. The load balancer drops a connection that stays idle for **350 seconds**, and the server sends no keepalive of its own, so a quiet channel can be dropped without warning and fail its next call. A ping every 30 seconds keeps it open. With the [Rust gRPC SDK](/sdk/rust-grpc):

```rust theme={null}
use std::time::Duration;
use tonic::transport::{Channel, ClientTlsConfig};

let channel = Channel::from_static("https://pl-staging.apimonaco.xyz")
    .tls_config(ClientTlsConfig::new().with_webpki_roots())?
    .http2_keep_alive_interval(Duration::from_secs(30))
    .keep_alive_timeout(Duration::from_secs(10))
    .keep_alive_while_idle(true)
    .connect()
    .await?;
```

Everything else, including request signing and idempotency keys, is unchanged from [Authenticate requests](/sdk/rust-grpc#authenticate-requests).

### WebSocket feeds

Connect to `wss://pl-staging.apimonaco.xyz:8443/ws`. The server sends a heartbeat every 25 seconds, so the idle timeout never applies to a healthy connection. With the TypeScript SDK, override only the WebSocket URL. REST calls keep using the public host:

```typescript theme={null}
const sdk = new MonacoSDK({
  walletClient,
  network: "staging",
  seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
  wsUrl: "wss://pl-staging.apimonaco.xyz:8443/ws", // feeds over PrivateLink
});
```

<Note>
  The TypeScript SDK places orders over REST, which is not offered over PrivateLink. To get the PrivateLink order budgets, send orders over gRPC.
</Note>

### Without private DNS

This fallback is for gRPC. WebSocket feeds need private DNS, or a WebSocket client that sets the TLS server name separately from the URL. The TypeScript SDK's client can't, so with it, connecting to the endpoint DNS name always fails certificate verification.

If you can't enable private DNS, connect your gRPC channel to the endpoint's own DNS name, `vpce-….vpce-svc-….us-east-1.vpce.amazonaws.com` (shown under `DnsEntries` in `describe-vpc-endpoints`), and set the TLS server name to `pl-staging.apimonaco.xyz`. The certificate does not cover the endpoint name, so verification fails without that override. Don't turn off certificate or hostname verification to get past the mismatch; set the server name instead.

```rust theme={null}
let channel = Channel::from_static("https://<endpoint-dns-name>")
    .tls_config(
        ClientTlsConfig::new()
            .with_webpki_roots()
            .domain_name("pl-staging.apimonaco.xyz"),
    )?
    .http2_keep_alive_interval(Duration::from_secs(30))
    .keep_alive_while_idle(true)
    .connect()
    .await?;
```

## Troubleshooting

| Symptom | Cause | Fix |
| - | - | - |
| `create-vpc-endpoint` reports the service does not exist or you are not authorized | Your principal is not allowlisted, or you are in the wrong region | Confirm the principal you sent Monaco, and use `--region us-east-1` |
| `create-vpc-endpoint` reports the service is not available in your subnet's AZ | The subnet is in a different AZ ID | Pick a subnet by AZ ID (step 2) |
| Endpoint stays `pendingAcceptance` | Monaco has not accepted the connection yet | Send the `vpce-…` ID to the Monaco team |
| `modify-vpc-endpoint --private-dns-enabled` is rejected | VPC DNS attributes are off | Turn on `enableDnsSupport` and `enableDnsHostnames` on the VPC |
| `pl-staging.apimonaco.xyz` does not resolve (`NXDOMAIN`) | Private DNS is off, was enabled only minutes ago, or the host uses a resolver outside the VPC | Enable private DNS, wait several minutes, and resolve through the VPC resolver, or connect [without private DNS](#without-private-dns) |
| Calls time out right after the connection is accepted | The new endpoint is still settling | Wait a few minutes and retry |
| TLS handshake fails with a name mismatch | Connecting to the endpoint DNS name without a server-name override | Set the TLS server name to `pl-staging.apimonaco.xyz` |
| Connection times out | The endpoint security group does not admit your hosts | Allow inbound `443` and `8443` from your trading hosts |
| WebSocket upgrade fails | Connecting on `:443`, which is HTTP/2-only gRPC | Use `:8443` for WebSocket feeds |
| First gRPC call after a quiet period fails | The 350-second idle timeout closed the connection | Turn on HTTP/2 keepalive ([gRPC](#grpc)) |
| `UNAVAILABLE` or a reset during a Monaco deploy | A draining API task was reset | Reconnect with backoff |

## Offboarding

Tell the Monaco team. Monaco removes your principal from the allowlist and rejects the endpoint connection. Then delete your endpoint:

```bash theme={null}
aws ec2 delete-vpc-endpoints --region us-east-1 --vpc-endpoint-ids <vpce-id>
```

## Related

<CardGroup cols={2}>
  <Card title="Rate Limits" icon="gauge" href="/developers/rate-limits">
    Public and PrivateLink order budgets, budget headers, and backoff.
  </Card>

  <Card title="Market-Maker Runbook" icon="book" href="/developers/market-maker-runbook">
    Quote lifecycle, state tracking, reconnect, and emergency stop.
  </Card>

  <Card title="Rust gRPC SDK" icon="rust" href="/sdk/rust-grpc">
    Connect, sign requests, and place orders over gRPC.
  </Card>

  <Card title="WebSocket Reference" icon="satellite-dish" href="/reference/websockets">
    Channels, events, heartbeats, and close codes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.