> ## 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.

# Rust SDK

> Monaco's REST and gRPC Rust clients

Monaco ships two Rust clients on crates.io. Both reach `https://apimonaco.xyz`; public market data needs no authentication, and trading, account, deposit and withdrawal calls need a Monaco client ID and a wallet-authorized session key.

| Client | Crate | Transport | Use when |
| - | - | - | - |
| [REST](#rest-client) | [`monaco-sdk`](https://crates.io/crates/monaco-sdk) | HTTP/JSON | You want simple request/response calls and OpenAPI-generated types. |
| [gRPC](#grpc-client) | [`monaco-grpc-sdk`](https://crates.io/crates/monaco-grpc-sdk) | gRPC over HTTP/2 | You want protobuf-native service clients and the lowest-latency path. |

## REST client

`monaco-sdk` is generated from Monaco's OpenAPI spec and covers the REST API surface.

### Add Dependencies

```toml theme={null}
[dependencies]
monaco-sdk    = "1.0"
alloy         = { version = "1", features = ["signers", "signer-local"] }
reqwest       = { version = "0.13", features = ["json"] }
tokio         = { version = "1", features = ["full"] }
ed25519-dalek = "2"
sha2          = "0.10"
rand          = "0.8"
serde_json    = "1"
uuid          = { version = "1", features = ["v4"] }
```

### Market Data

No authentication is required to read trading pairs.

```rust theme={null}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = monaco_sdk::Client::new("https://apimonaco.xyz");

    let pairs = client
        .list_trading_pairs(None, Some(true), None, None, None, None)
        .await?
        .into_inner();

    let pairs = pairs.trading_pairs.unwrap_or_default();
    for pair in pairs.iter().take(5) {
        println!("{}", pair.symbol.as_deref().unwrap_or("?"));
    }

    let sei_pair = pairs
        .iter()
        .find(|pair| pair.symbol.as_deref() == Some("SEI/USDC"))
        .expect("SEI/USDC pair not found");

    let pair_id = sei_pair.id.unwrap();
```

### Authenticate

Monaco authenticates with a wallet-authorized ed25519 **session key**. Generate a keypair, have the wallet sign a challenge that commits to it, then sign every authenticated request with the session key.

```rust theme={null}
    use alloy::hex;
    use alloy::signers::local::PrivateKeySigner;
    use ed25519_dalek::{Signer as _, SigningKey};
    use rand::rngs::OsRng;
    use serde_json::json;

    let signer: PrivateKeySigner = "0xYOUR_PRIVATE_KEY".parse()?;
    let address = format!("{:?}", signer.address());
    let base = "https://apimonaco.xyz";
    let api_prefix = "/api/v1";
    // Monaco's API edge rejects requests without a User-Agent, and reqwest sends none by default.
    let http = reqwest::Client::builder().user_agent("my-app/1.0").build()?;

    let session_key = SigningKey::generate(&mut OsRng);
    let session_public_key = hex::encode(session_key.verifying_key().to_bytes());

    let challenge: serde_json::Value = http
        .post(format!("{base}{api_prefix}/auth/challenge"))
        .json(&json!({
            "address": address,
            "clientId": "YOUR_CLIENT_ID",
            "sessionPublicKey": session_public_key,
        }))
        .send().await?.json().await?;

    let message = challenge["message"].as_str().unwrap();
    let nonce = challenge["nonce"].as_str().unwrap();

    let sig = alloy::signers::Signer::sign_message(&signer, message.as_bytes()).await?;
    let sig_hex = format!("0x{}", hex::encode(sig.as_bytes()));

    http.post(format!("{base}{api_prefix}/auth/verify"))
        .json(&json!({
            "address": address,
            "clientId": "YOUR_CLIENT_ID",
            "nonce": nonce,
            "signature": sig_hex,
            "sessionPublicKey": session_public_key,
        }))
        .send().await?.error_for_status()?;
```

### Sign Authenticated Requests

On native targets, the SDK can sign session-authenticated requests automatically:

```rust theme={null}
use monaco_sdk::{Client, RequestSigning, SessionSigner};

let client = Client::new_authenticated(
    base,
    SessionSigner::new(session_key.to_bytes(), RequestSigning::Rfc9421),
);
```

`Client::new` and `Client::new_authenticated` send `User-Agent: monaco-sdk-rust/<version>`. `Client::new_with_client` uses the `reqwest::Client` you pass unchanged, so set a User-Agent on it (`reqwest::Client::builder().user_agent(...)`): Monaco's API edge rejects requests without one with `403`.

Use your registered session signing key as `session_key`. RFC 9421 signing is opt-in and requires a server that supports it. Use `RequestSigning::Legacy` for existing servers. The authenticated client disables redirects; public operations remain unsigned. See [HTTP message signatures](https://docs.0xmonaco.com/reference/http-message-signatures.md) for the signing profile and migration steps.

The manual example below uses legacy signing. Legacy authenticated REST requests carry `X-Monaco-PublicKey`, `X-Monaco-Timestamp`, and `X-Monaco-Signature`. The signature covers:

```text theme={null}
METHOD
path?query
timestamp_ms
SHA256_hex(body)
```

With an empty query, sign the path alone, with no `?`.

```rust theme={null}
    use sha2::{Digest, Sha256};
    use std::time::{SystemTime, UNIX_EPOCH};

    async fn signed_post(
        http: &reqwest::Client,
        base: &str,
        api_prefix: &str,
        key: &SigningKey,
        public_key: &str,
        path: &str,
        body: &serde_json::Value,
    ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
        let request_path = format!("{api_prefix}{path}");
        let body_bytes = serde_json::to_vec(body)?;
        let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis().to_string();
        let body_hash = hex::encode(Sha256::digest(&body_bytes));
        let signing_string = format!("POST\n{request_path}\n{ts}\n{body_hash}");
        let signature = hex::encode(key.sign(signing_string.as_bytes()).to_bytes());

        let resp = http
            .post(format!("{base}{request_path}"))
            .header("X-Monaco-PublicKey", public_key)
            .header("X-Monaco-Timestamp", ts)
            .header("X-Monaco-Signature", signature)
            .header("Content-Type", "application/json")
            .body(body_bytes)
            .send().await?
            .error_for_status()?;

        Ok(resp.json().await?)
    }
```

### Place a Limit Order

The raw generated client keeps `idempotency_key` optional and does not generate it. Set it yourself (`idempotencyKey` in JSON) before serialization and signing. Retain the prepared payload and key; persist them before sending if recovery must survive a process restart.

```rust theme={null}
    let submission_key = uuid::Uuid::new_v4().to_string();
    let create = json!({
        "idempotencyKey": submission_key,
        "tradingPairId": pair_id,
        "orderType": "LIMIT",
        "side": "BUY",
        "price": "0.0100",
        "quantity": "0.1",
        "timeInForce": "GTC",
    });
    // Save `create` before sending. A recovery attempt reuses this payload.
    let order = signed_post(&http, base, api_prefix, &session_key, &session_public_key, "/orders", &create).await?;

    let order_id = order["orderId"].as_str().unwrap().to_string();
    println!("order {order_id}: {}", order["status"].as_str().unwrap_or("?"));
```

For a lost response, reuse `create` unchanged and sign again with a fresh timestamp. Use a distinct key for each batch-create item, generated before signing the batch. The key protects the original submission for 15 minutes after acceptance; retries do not extend that window. Do not retry an unresolved submission after expiry. This example does not retry automatically.

A `GTC` order rests on the book until filled or cancelled. See [Order Management](https://docs.0xmonaco.com/developers/trade.md) for other time-in-force options.

### Cancel an Order

```rust theme={null}
    let cancel = signed_post(&http, base, api_prefix, &session_key, &session_public_key, "/orders/cancel", &json!({
        "orderId": order_id,
    })).await?;

    println!("cancel: {}", cancel["status"].as_str().unwrap_or("?"));

    Ok(())
}
```

## gRPC client

`monaco-grpc-sdk` is generated from Monaco's protobuf module on the Buf Schema Registry.

### Add Dependencies

```toml theme={null}
[dependencies]
monaco-grpc-sdk = "1.0"
tonic = { version = "0.14", features = ["transport", "tls-ring", "tls-webpki-roots"] }
tokio = { version = "1", features = ["full"] }

# Request signing for authenticated calls (see "Authenticate requests").
ed25519-dalek = "2"
prost = "0.14"
sha2 = "0.10"
hex = "0.4"
uuid = { version = "1", features = ["v4"] }
```

#### Optional `serde` feature

Turn on the `serde` feature to derive `serde::Serialize` and `serde::Deserialize` on every generated message and enum, in the same JSON shape the REST API (`/api/v1/...`) sends and accepts:

```toml theme={null}
monaco-grpc-sdk = { version = "1.0", features = ["serde"] }
```

* Field names are camelCase, and the REST snake\_case aliases are accepted on input.
* The uint64 lease and quote fences (`generation`, `quoteRevision`, `expectedGeneration`) are decimal strings.
* An unset optional field is omitted where REST omits it and serialized as `null` elsewhere (an unset `Position.opened_at` serializes as `"openedAt": null`). A key the server omits reads as unset.
* The request types that REST refuses unknown fields on refuse them here too.

The feature is off by default and changes nothing on the gRPC wire.

### Connect and List Pairs

The gRPC service is exposed on the same host as the REST API. Connect to the host on port `443`; the load balancer routes gRPC method paths such as `/monaco.api.*` to Monaco's gRPC API.

```rust theme={null}
use monaco_grpc_sdk::monaco::api::market::{
    market_service_client::MarketServiceClient,
    ListTradingPairsRequest,
};
use tonic::transport::{Channel, ClientTlsConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let channel = Channel::from_static("https://apimonaco.xyz")
        .tls_config(ClientTlsConfig::new().with_webpki_roots())?
        .connect()
        .await?;

    let mut market = MarketServiceClient::new(channel);

    let response = market
        .list_trading_pairs(ListTradingPairsRequest {
            page: Some(1),
            page_size: Some(20),
            market_type: None,
            base_token: None,
            quote_token: None,
            is_active: Some(true),
        })
        .await?
        .into_inner();

    for pair in response.trading_pairs {
        println!("{} {}", pair.symbol, pair.id);
    }

    Ok(())
}
```

### Authenticate requests

Market data is public. Trading and account RPCs are **tokenless**: sign every
request with the ed25519 **session key** you registered through
`AuthService.Challenge`/`Verify`, and attach the signature as metadata. The
server verifies it against your stored public key.

The signature covers the canonical string `POST\n<method-path>\n<timestamp-ms>\nSHA256(request bytes)`,
where the request bytes are the **prost-encoded message** — the same bytes tonic
sends — so hash `msg.encode_to_vec()` and bind the full leading-slash method
path. Add `ed25519-dalek`, `prost`, `sha2`, and `hex` alongside the SDK.

The raw generated client keeps `idempotency_key` optional and does not generate it. Create and retain the key before protobuf encoding or signing. Persist the prepared message and key before sending if recovery must survive a process restart.

```rust theme={null}
use ed25519_dalek::{Signer, SigningKey};
use monaco_grpc_sdk::monaco::api::orders::{
    orders_service_client::OrdersServiceClient, CreateOrderRequest,
};
use prost::Message;
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use tonic::transport::{Channel, ClientTlsConfig};
use tonic::Request;

/// Wrap a request with the three `x-monaco-*` signing metadata entries.
fn signed_request<T: Message>(msg: T, method_path: &str, key: &SigningKey) -> Request<T> {
    let hash = hex::encode(Sha256::digest(msg.encode_to_vec()));
    // Real current time in milliseconds; a stale or far-future value is rejected.
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock before epoch")
        .as_millis()
        .to_string();
    let signing_string = format!("POST\n{method_path}\n{timestamp}\n{hash}");
    let signature = hex::encode(key.sign(signing_string.as_bytes()).to_bytes());
    let public_key = hex::encode(key.verifying_key().to_bytes());

    let mut request = Request::new(msg);
    let md = request.metadata_mut();
    md.insert("x-monaco-publickey", public_key.parse().expect("ascii metadata"));
    md.insert("x-monaco-timestamp", timestamp.parse().expect("ascii metadata"));
    md.insert("x-monaco-signature", signature.parse().expect("ascii metadata"));
    request
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let channel = Channel::from_static("https://apimonaco.xyz")
        .tls_config(ClientTlsConfig::new().with_webpki_roots())?
        .connect()
        .await?;

    // ed25519 session key registered via AuthService.Challenge/Verify (32-byte
    // hex seed). The server verifies against its public key — there is no token.
    let seed: [u8; 32] = hex::decode(std::env::var("MONACO_SESSION_SEED")?)?
        .try_into()
        .expect("MONACO_SESSION_SEED must be 32-byte hex");
    let key = SigningKey::from_bytes(&seed);

    let mut orders = OrdersServiceClient::new(channel);
    const CREATE_ORDER: &str = "/monaco.api.orders.OrdersService/CreateOrder";

    let submission_key = uuid::Uuid::new_v4().to_string();
    let create = CreateOrderRequest {
        idempotency_key: Some(submission_key),
        trading_pair_id: "TRADING_PAIR_UUID".into(), // from MarketService.ListTradingPairs
        order_type: "LIMIT".into(),
        side: "BUY".into(),
        quantity: "0.01".into(),
        price: Some("60000".into()),
        trading_mode: Some("SPOT".into()),
        time_in_force: Some("GTC".into()),
        ..Default::default()
    };
    // Save `create` before sending; retain it for recovery.
    let response = orders
        .create_order(signed_request(create.clone(), CREATE_ORDER, &key))
        .await?
        .into_inner();
    println!("created order: {response:?}");

    Ok(())
}
```

For a lost create response, re-sign `create.clone()` with a fresh timestamp; keep the same payload and key. Give every batch-create item a distinct key before encoding and signing the batch. Protection lasts 15 minutes after acceptance and retries do not extend it. Do not retry an unresolved submission after expiry. This example does not retry automatically.

Each call re-signs with a fresh timestamp and body hash, so signatures are never
reused. A `UNAUTHENTICATED` status on a previously working call means the session
expired or was revoked — re-run challenge/verify and retry; do not loop against a
persistent `UNAUTHENTICATED`. The [authenticated gRPC quickstart](https://docs.0xmonaco.com/reference/grpc#authenticate-requests)
covers the metadata, error handling, and [golden signing vectors](https://docs.0xmonaco.com/reference/grpc#golden-signing-vectors)
you can check a Rust signer against.

### Release Channels

| Version pattern | Source | Purpose |
| - | - | - |
| `X.Y.Z` | every SDK release on `main` | Latest stable |
| `X.Y.Z-prerelease.<sha>` | a `release/*` branch | Release candidate, opt in via exact pin |
| `X.Y.Z-develop.<sha>` | every SDK-affecting merge to `develop` | Dev snapshot, opt in via exact pin |

Release candidates and dev snapshots use a SemVer pre-release suffix, so a default `cargo add monaco-grpc-sdk` ignores them. Pin explicitly to opt in:

```toml theme={null}
monaco-grpc-sdk = "=1.0.74-develop.367860f"
```

See the [gRPC Protos reference](https://docs.0xmonaco.com/grpc/protos) for the complete service list and message schemas.


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