> ## Documentation Index
> Fetch the complete documentation index at: https://molelcule.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Ed25519 Request Signing: Molecule API Authentication

> Molecule API requests are signed with Ed25519. The SDK handles signing automatically. This page covers the canonical format and replay protection.

Every request to the Molecule API is authenticated with an Ed25519 digital signature. When you initialise the SDK with a `key_id` and `private_key`, signing happens automatically on every REST call and WebSocket connection — you do not need to construct signatures manually. This page documents the canonical string format and signing flow for teams building custom integrations, verifying signatures in tests, or implementing the protocol in another language.

<Note>
  If you are using the Python SDK, you do not need to implement signing. Pass `key_id` and `private_key` to the `Molecule` constructor and the SDK handles all signing transparently. The information below is intended for custom integrations or independent verification.
</Note>

## How the SDK Signs Requests

When you construct a `Molecule` client with credentials, the SDK creates an internal `Signer` instance that holds a reference to your Ed25519 `SigningKey`. The private key material never leaves your process.

```python theme={"dark"}
import os
from molecule import Molecule

client = Molecule(
    base_url=os.environ["MOLECULE_BASE_URL"],
    key_id=os.environ["MOLECULE_KEY_ID"],        # Identifies your registered public key
    private_key=os.environ["MOLECULE_PRIVATE_KEY"],  # Base64-encoded 32-byte Ed25519 seed
)
```

For every outgoing REST request, the SDK:

1. Builds a canonical string from the request method, path, query parameters, timestamp, and body hash
2. Signs the canonical string bytes with your Ed25519 private key
3. Attaches three headers to the HTTP request

| Header | Content |
| - | - |
| `X-Molecule-Key-Id` | Your key ID, identifying which public key to verify against |
| `X-Molecule-Timestamp` | Current Unix time in milliseconds |
| `X-Molecule-Signature` | Base64-encoded Ed25519 signature of the canonical string |

The server looks up the registered public key for the given `key_id`, verifies the signature against the canonical string it reconstructs from the incoming request, and rejects any request whose timestamp falls outside a 30-second replay window.

## REST Canonical String

The canonical string is a newline-delimited sequence of five fields, encoded as UTF-8 bytes before signing:

```
METHOD\n
PATH\n
SORTED_QUERY\n
TIMESTAMP_MS\n
hex(sha256(BODY))
```

<Accordion title="Field definitions">
  **METHOD** — HTTP method in uppercase: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`.

  **PATH** — The request path including the leading slash, without query string or fragment. For example, `/v1/orders`.

  **SORTED\_QUERY** — All non-null query parameters sorted first by key, then by value, URL-encoded. Boolean values are serialised as `true` or `false`. If there are no query parameters, this field is an empty string (the newline is still present).

  **TIMESTAMP\_MS** — The current Unix time in milliseconds as a decimal string. The value in this field must match `X-Molecule-Timestamp` exactly.

  **hex(sha256(BODY))** — Lowercase hex-encoded SHA-256 digest of the raw request body bytes. For requests with no body (GET, DELETE), use the SHA-256 of the empty string: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.
</Accordion>

The following example shows the canonical string for a `POST /v1/orders` call with a query parameter and a JSON body:

```
POST\n
/v1/orders\n
subaccount_id=sa_1\n
1727270000000\n
ea6bc000e702adb389166c867bba85eec18fbef2c34cfc12250217f45d5a6062
```

To produce this manually in Python:

```python theme={"dark"}
import hashlib
import base64
from nacl.signing import SigningKey

method    = "POST"
path      = "/v1/orders"
query     = "subaccount_id=sa_1"        # pre-sorted, URL-encoded
timestamp = "1727270000000"             # Unix milliseconds as string
body      = b'{"side":"BUY","qty":"10"}'

body_hash = hashlib.sha256(body).hexdigest()
canonical = f"{method}\n{path}\n{query}\n{timestamp}\n{body_hash}".encode()

seed = base64.b64decode("cgxIy78BcAUooPsgrAXQPYDW5h0zLru6Lg35Ga4pEP0=")
signing_key = SigningKey(seed)
signature = base64.b64encode(signing_key.sign(canonical).signature).decode()

headers = {
    "X-Molecule-Key-Id":    "key_test_1",
    "X-Molecule-Timestamp": timestamp,
    "X-Molecule-Signature": signature,
}
```

## WebSocket Canonical String

WebSocket connections use a similar but shorter canonical string. Signing parameters are appended to the WebSocket URL as query parameters rather than sent as headers.

```
WS\n
PATH\n
SORTED_QUERY\n
TIMESTAMP_MS
```

The `SORTED_QUERY` field excludes the signing parameters themselves (`key_id`, `ts`, `sig`, `access_token`) — they are added to the URL after signing. All other query parameters follow the same sort and encoding rules as the REST canonical string. There is no body field for WebSocket connections.

After signing, three parameters are appended to the WebSocket URL:

| Parameter | Content |
| - | - |
| `key_id` | Your key ID |
| `ts` | Unix timestamp in milliseconds |
| `sig` | Base64-encoded Ed25519 signature |

Use `client.ws.orders()` (or any other WebSocket helper) to get a fully signed URL ready to connect:

```python theme={"dark"}
import os
from molecule import Molecule

client = Molecule(
    base_url=os.environ["MOLECULE_BASE_URL"],
    key_id=os.environ["MOLECULE_KEY_ID"],
    private_key=os.environ["MOLECULE_PRIVATE_KEY"],
)

# Returns a signed wss:// URL with key_id, ts, and sig appended
signed_url = client.ws.orders({"subaccount_id": "sa_1"})
print(signed_url)
# wss://api.example.com/v1/ws/orders?subaccount_id=sa_1&key_id=key_test_1&ts=1727270000000&sig=<base64>
```

You can also use `client.iter_ws` to connect and consume messages without managing the WebSocket lifecycle directly:

```python theme={"dark"}
for message in client.iter_ws("/v1/ws/orders", {"subaccount_id": "sa_1"}):
    print(message)
```

The canonical string for this connection (before signing) is:

```
WS\n
/v1/ws/orders\n
subaccount_id=sa_1\n
1727270000000
```

## Replay Protection

The server enforces a 30-second replay window on all signed requests. Any request whose `X-Molecule-Timestamp` (or `ts` query parameter for WebSocket) is more than 30 seconds in the past is rejected with a `401 stale_timestamp` error.

<Steps>
  ### Clock skew

  Keep your system clock synchronised (NTP or equivalent). A clock that drifts more than 30 seconds will cause all requests to fail with `UnauthorizedError`.

  ### Replay rejection

  If the same signature is presented twice within the replay window, the server rejects the second request with a `401 replay` error. The SDK always generates a fresh timestamp for every request, so this only affects custom integrations that cache or retry raw signed requests.

  ### Stale timestamp handling

  ```python theme={"dark"}
  from molecule.errors import UnauthorizedError

  try:
      result = client.orders.list(subaccount_id="sa_1")
  except UnauthorizedError as exc:
      if exc.error == "stale_timestamp":
          # System clock is out of sync — verify NTP configuration
          raise
      if exc.error == "replay":
          # Duplicate request detected — do not resend the same signed payload
          raise
      raise
  ```
</Steps>

## Private Key Formats

The `load_private_key` function in `molecule.auth` accepts the following formats for the 32-byte Ed25519 seed:

| Format | Example |
| - | - |
| Base64 (canonical) | `"cgxIy78BcAUooPsgrAXQPYDW5h0zLru6Lg35Ga4pEP0="` |
| Hex string | `"720c48cbbf017005..."` |
| Raw bytes | `b'\x72\x0c\x48...'` |
| 64-byte seed\|\|pubkey blob | First 32 bytes are used |

Base64 encoding of the 32-byte seed is the canonical format used for environment variables and configuration files.

```python theme={"dark"}
import os
import base64
from nacl.signing import SigningKey

# Generate a new key pair
signing_key = SigningKey.generate()
seed_b64 = base64.b64encode(bytes(signing_key)).decode()
pub_hex = signing_key.verify_key.encode().hex()

print(f"MOLECULE_PRIVATE_KEY={seed_b64}")
print(f"Public key (register this with Molecule): {pub_hex}")
```

<Warning>
  Store your private key in an environment variable or secrets manager — never hardcode it in source code or commit it to version control. The key authorises all order activity on your account.
</Warning>


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