Skip to main content

Encrypt requests for the operator

Use this after you have signed a DerivaDEX request and are ready to submit it to the operator. The examples use Python because the byte-level procedure is easiest to read there, but the same steps apply in any language that can produce the same bytes. You need the signed request JSON exactly as it will be submitted, plus the operator public key from GET /v2/encryption-key. After the payload is encrypted, submit the resulting bytes to POST /v2/request. Each step crosses a different boundary: you fetch the operator key from DerivaDEX, build the encrypted bytes locally, and then send only the encrypted payload to the submission endpoint.

Follow the encryption procedure

  1. Fetch the operator public key from GET /v2/encryption-key.
  2. Serialize the signed request to UTF-8 JSON exactly as you will submit it.
  3. Generate a fresh client secp256k1 private key for this submission attempt, then derive its compressed public key.
  4. Multiply the operator public key by the client private key to produce the shared point.
  5. Run keccak256 over the shared point bytes and keep the first 16 bytes as the AES-GCM key.
  6. Generate a fresh 12-byte nonce, which is a one-time random value for this encryption.
  7. Prefix the UTF-8 JSON bytes with a 4-byte big-endian length.
  8. Encrypt that payload with AES-GCM, which encrypts the bytes and adds an integrity check.
  9. Assemble the final byte payload as ciphertext || tag || nonce || client_public_key_compressed.
  10. Submit those bytes to POST /v2/request.
Use a fresh client private key and a fresh nonce for every submission attempt, including retries.

Worked example

This flow is the same for orders, cancels, profile updates, and withdrawals. What changes is the signed JSON you feed into it. This example starts from the same order fields used in How to Sign DerivaDEX Requests with EIP-712. The signature here comes from that same EIP-712 hash, signed with a fixed sample wallet so the encryption result is reproducible. The JSON shape below matches what TradingJSONEncoder().encode(order_intent.json) produces in ddx-python.

Example: encrypt a signed order request

Each example can be implemented in two ways here:
  • Direct implementation shows the byte-building steps directly.
  • DDX client library shows the shortest path if you already use ddx-python.

Direct implementation

Start with the helper functions that stay the same for every request type:
from coincurve import PublicKey, PrivateKey
from Crypto.Cipher import AES
from Crypto.Hash import keccak
from Crypto.Random import get_random_bytes


def encrypt_request(
    operator_public_key_hex: str,
    signed_request_json: str,
    client_private_key_hex: str | None = None,
    nonce_hex: str | None = None,
) -> str:
    operator_public_key = PublicKey(
        bytes.fromhex(operator_public_key_hex.removeprefix("0x"))
    )

    if client_private_key_hex is None:
        client_private_key = PrivateKey(get_random_bytes(32))
    else:
        client_private_key = PrivateKey(
            bytes.fromhex(client_private_key_hex.removeprefix("0x"))
        )

    client_public_key = client_private_key.public_key.format()
    shared_point = operator_public_key.multiply(client_private_key.secret)

    digest = keccak.new(digest_bits=256)
    digest.update(shared_point.format())
    aes_key = digest.digest()[:16]

    if nonce_hex is None:
        nonce = get_random_bytes(12)
    else:
        nonce = bytes.fromhex(nonce_hex.removeprefix("0x"))

    cipher = AES.new(aes_key, AES.MODE_GCM, nonce=nonce)
    encoded = signed_request_json.encode("utf-8")
    ciphertext, tag = cipher.encrypt_and_digest(
        len(encoded).to_bytes(4, byteorder="big") + encoded
    )

    return "0x" + (ciphertext + tag + nonce + client_public_key).hex()
Use fixed values when you want a reproducible result. These sample values are only for this worked example, not for live submissions:
operator_public_key = "0x03bc06b4271530d20b4ddb03e0069b00b2c0d03baf45d0ab60582448b6d70c2737"
client_private_key = "0xa178baab5a727c5d08c4ed1118179348d8d43e68c0d5e620757f852cdbc79dfd"
nonce = "0x4eaac2e15c60705ec631d9c8"

signed_request_json = (
    '{"t": "Order", "c": {"symbol": "ETHP", "strategy": "main", "side": "Bid", '
    '"orderType": 0, "nonce": '
    '"0x3137373038373530313938323238333436363300000000000000000000000000", '
    '"amount": "0.1", "price": "1800", "stopPrice": "0", '
    '"sessionKeySignature": null, "signature": '
    '"0x59ff6d112050bd06d73048fae8f22e8b76a1b77e68d67fbbf68241e9372776494b0ef8eb99ff225dd08eafb6d3ccc8ed0ec2f214755f482b90699db8233d18da1c"}}'
)

encrypted_request = encrypt_request(
    operator_public_key_hex=operator_public_key,
    signed_request_json=signed_request_json,
    client_private_key_hex=client_private_key,
    nonce_hex=nonce,
)
For this input, the target result is:
"0x7f8083f4c8a4a8dbc1baa5da0baf1b8f33974ffdaa0fffbba2212c90f863d7bae48191addcbdc7f404620a0ac9ad37905c042c12f3316f3d0b832a78fe58dce62334c4dd3d0661dd4f62e80d0ab4609755beeaa4ef48148b5456fb4d02feffa11db5a67613ccf0d0667cdb4d8b0f5fad8b4bae667c72867a62e4c6b701e41886a1f934d4e4a76f165dc500fd15ef9cf13b2a7950c57ebafef3064bfe424ef5f25d981234e491ada032d185d620786f22f45928813129f1b1ee4fddc80029a656ae58e097b9c42cf9820fa20855f74f9c7e01c7354b902c10052a3ad003476fa4d3f5fcba0cdab2928bf54e78f5798c10229dea31c40cc9a9a223c5f61720d68cc5986886fc190cc36e692070c719bc46108bf45a7cdd5352cee30559ed61b7bbc4a58971686a2b0df6f7df07dccb2b14249abed5a37de27d717c432c36dedca4e11838ab74ef80ff71d913ecf32f27f3befae55f80a6d8ab2894b009fa64948a3f256a42d219b26d6729f3285ddc159bf7082e4e36bcc978b3a25da52f2de5018843d52ded82ea1721791cdf08dcdcc570fd5a86a7d6814977c7ab3e268671d95f3eb24eaac2e15c60705ec631d9c80208c7ebdceb4366aff5464cd3b05099481304c9d9b1adbadc2459444b038ca2b3"
In production, leave client_private_key_hex and nonce_hex unset so each submission attempt uses fresh values.

DDX client library

If you already use ddx-python, the library helper applies the same scheme and returns the bytes you can post to POST /v2/request:
from ddx.rest_client.utils.encryption_utils import encrypt_with_nonce

operator_public_key = "0x03bc06b4271530d20b4ddb03e0069b00b2c0d03baf45d0ab60582448b6d70c2737"
signed_request_json = (
    '{"t": "Order", "c": {"symbol": "ETHP", "strategy": "main", "side": "Bid", '
    '"orderType": 0, "nonce": '
    '"0x3137373038373530313938323238333436363300000000000000000000000000", '
    '"amount": "0.1", "price": "1800", "stopPrice": "0", '
    '"sessionKeySignature": null, "signature": '
    '"0x59ff6d112050bd06d73048fae8f22e8b76a1b77e68d67fbbf68241e9372776494b0ef8eb99ff225dd08eafb6d3ccc8ed0ec2f214755f482b90699db8233d18da1c"}}'
)

encrypted_request = encrypt_with_nonce(operator_public_key, signed_request_json)

print("0x" + encrypted_request.hex())
The library generates a fresh client key and a fresh nonce on each call, so the final bytes will differ from run to run.

Check your result before you submit

  1. Confirm that you are encrypting the signed JSON, not an unsigned request object.
  2. Confirm that you are posting the encrypted bytes directly to POST /v2/request, not wrapping them in another JSON object.
  3. Confirm that the operator public key came from the current GET /v2/encryption-key response.
  4. If you retry, rebuild the payload with a fresh client private key and a fresh nonce.

Appendix

Use this byte layout when you build the final payload:
PartWhat it contains
Plaintext4-byte big-endian length || signed_request_json_utf8_bytes
AES keyFirst 16 bytes of keccak256(shared_point_bytes)
Nonce12 random bytes
Final payloadciphertext || tag || nonce || client_public_key_compressed
If you still need to produce the signature that goes inside the JSON payload, use How to Sign DerivaDEX Requests with EIP-712.
Last modified on May 1, 2026