Skip to main content
Monaco supports an opt-in RFC 9421 signing protocol alongside legacy X-Monaco-* authentication. Both use the same wallet-authorized Ed25519 session keys. Deploy server support before opting clients in. Existing clients and sessions continue to work; legacy authentication is not retired.

What the signature protects

The signature binds the HTTP method, escaped path, raw query, and exact body bytes to a registered session key. Changing an order’s price or quantity, query parameter ordering, percent encoding, or JSON whitespace invalidates it. Use HTTPS: signatures provide integrity and key possession, not confidentiality. The gateway remains trusted to verify signatures, resolve wallet-to-session-key bindings, and enforce authorization. Downstream services do not independently verify the original HTTP signature. This protocol does not make a compromised gateway unable to fabricate internal commands. Host and scheme are not covered; keys must not be reused across independently trusted deployments. These trust boundaries also apply to legacy request signing. Session expiry, revocation, delegated-agent permissions, and account restrictions continue to use the existing shared authentication checks. Register a new session key through the existing wallet flow for rotation, migrate clients, then revoke the old session. WebSocket and gRPC signing are unchanged.

Signing profile

This is an application profile of RFC 9421 using RFC 9530 Content-Digest. It is not a general-purpose verifier for arbitrary HTTP signature profiles. Every signed request, including GET and empty-body requests, sends:
  • The signature label is monaco; exactly one dictionary member is accepted. The four covered components appear in the order shown. created, keyid, and alg are required; their ordering may vary. Extra components, parameters, signature labels, or digest algorithms are rejected. Structured-field spaces are accepted and signature parameters are serialized canonically. Raw header limits are 1,024 bytes for Signature-Input and 128 bytes each for Signature and Content-Digest.
  • created is an integer Unix timestamp in seconds, within 30 seconds of server time. Legacy timestamps use milliseconds.
  • keyid is the registered 32-byte session public key, encoded as 64 lowercase hexadecimal characters. It is a lookup identifier, not a remote key URL.
  • alg is exactly ed25519.
  • @method is the uppercase request method. @path is the escaped path as sent.
  • @query starts with ? and retains raw ordering, duplicates, and encoding. Its value is ? when the query is absent or empty. Do not decode or sort it.
  • content-digest covers SHA-256 of the exact body bytes, encoded as a structured byte sequence. Hash the empty byte string when there is no body.
Two worked @query values, since divergent canonicalization is the most common cause of an otherwise-correct signature failing:
  • GET /api/v1/orders signs @path as /api/v1/orders and @query as ?.
  • GET /api/v1/orders?status=FILLED%2CPARTIALLY_FILLED&pageSize=20 signs @query as ?status=FILLED%2CPARTIALLY_FILLED&pageSize=20 — exactly the bytes on the wire, neither percent-decoded nor reordered. Sending the same filter unencoded, as ?status=FILLED,PARTIALLY_FILLED&pageSize=20, is a different signature base even though the server reads the same filter, so sign the string you actually send.
The signature base has one line per component followed by the signature parameters. There is no trailing newline:
Sign the UTF-8 bytes of that base with the session private key. Send the same body bytes that were hashed. Monaco rejects missing coverage, invalid digests, unsupported algorithms, ambiguous headers, and mixed RFC/legacy credentials. Presence of Signature or Signature-Input selects standard verification; failure never falls back to legacy authentication. Content-Digest alone is ordinary HTTP integrity metadata, not a signature credential. It does not select RFC authentication or override the body hash verified by legacy authentication.

Freshness, retries, and idempotency

The 30-second clock window limits how long a captured request can be reused. It does not prevent an identical signed request from being accepted again inside that window. Monaco keeps no nonce or distributed replay cache. A fresh signature is also not proof that an operation is new. Use the endpoint’s documented idempotency mechanism where available. Do not blindly retry writes without it. Re-sign a retry if its timestamp has aged out, while retaining the original operation’s idempotency key and payload. Endpoints without idempotency support provide no exactly-once guarantee.

TypeScript migration

After server deployment, opt in when constructing the SDK:
The default remains "legacy". RFC signing covers the final URL and serialized string body. The SDK manages all signature headers and rejects caller overrides or unsupported body types. RFC-signed requests disable redirects, including caller overrides. Public requests keep their existing authentication. No automatic protocol downgrade occurs on errors.

Rust migration

The Rust REST SDK signs generated session-authenticated operations using the final request URL and serialized body. Existing unsigned constructors keep their signatures. Public operations remain unsigned.
Select RequestSigning::Legacy for a server that has not migrated. Authenticated clients disable HTTP redirects. The low-level SessionSigner::sign_at helper is also available for reusable reqwest requests; its caller must preserve the signed bytes and disable redirects. Streaming bodies are rejected rather than silently signed as empty.

Deployment sequence

  1. Deploy the server accepting both protocols and verify signed reads and writes with an existing session key in staging.
  2. Verify stale timestamps, query/body tampering, revoked sessions, and malformed signatures fail. Confirm public routes bypass auth buffering.
  3. Release SDKs and opt individual applications in. Keep old clients on legacy until their migration is verified. Client rollback selects legacy; do not roll the server back while RFC-only clients still target it.
  4. Retire legacy signing only through a separate coordinated contract change after deployed-client usage is known. No retirement date is set.
Signed routes still read and hash their bodies and enforce a 2 MiB bound. Public routes use their normal extractor limits rather than the signed body buffer. This change reduces unnecessary global buffering; it does not remove body hashing from trading requests or promise a trading-latency improvement.