QRDXDocs

Exchange transactions

Build, sign and submit orders, swaps, pool and token operations, and read their receipts.

Every operation on the native exchange and native tokens is an exchange transaction: a JSON object signed with the sender's ML-DSA-65 key. A classic 0x key cannot sign one.

{
  "op_type": 5,                      // or "PLACE_ORDER"
  "sender": "0xPQ…",
  "nonce": 4,                        // the sender's next EXCHANGE nonce
  "params": { "pair": "QRDX:0xe13e…", "side": "buy", "order_type": "limit", "price": "0.5", "amount": "10" },
  "gas_limit": 100000,
  "gas_price": "1000000000",         // wei per gas, at least exchange_gasPrice (1 gwei)
  "public_key": "<hex>",             // must derive to sender
  "signature": "<hex>"               // ML-DSA-65 over the signing bytes
}

Exchange operations lists every op_type and its parameters.

1. Get the nonce

RPC
exchange_getNonce
params⌘/Ctrl + Enter to run

Run sends this request from your browser to https://test.qrdx.org/rpc.

The nonce counts committed operations. If you submit several before a block includes them, track the ones in flight yourself and use the next free nonce (QRDX Wallet does this). A transaction that executes uses its nonce even if it fails; one refused at admission does not.

2. Get the bytes to sign

The signature covers, concatenated: op_type (1 byte), sender (UTF-8), nonce (8 bytes big-endian), params (as Python's json.dumps(params, sort_keys=True) renders them, ", " and ": " separators included), gas_limit (8 bytes big-endian) and gas_price (the integer as a decimal string). The transaction hash is BLAKE2b-256 of the same bytes.

Reproducing the params rendering outside Python is error-prone, so ask the node: exchange_getSigningPayload (or POST /exchange_signing_payload) takes the unsigned fields and returns signing_bytes, tx_hash and the normalised tx.

3. Sign and submit

Sign signing_bytes with ML-DSA-65 (raw bytes, no message prefix), add signature and public_key to the returned tx, and send it with exchange_sendTransaction(tx) or POST /submit_exchange_tx ({"tx": {…}}).

This example does everything except submit: it builds an order for a fresh key, fetches the payload, checks the hash itself, signs and verifies.

JS
Build and sign an exchange transaction
output⌘/Ctrl + Enter to run
Edit the code and run it. It talks to testnet from your browser.
const payload = await rpc('exchange_getSigningPayload', {
  op_type: 'SWAP', sender: address, nonce,
  params: { token_in: 'QRDX', token_out: token, amount_in: '1', min_amount_out: minOut },
})
const signature = bytesToHex(ml_dsa65.sign(hexToBytes(payload.signing_bytes), secretKey))
const hash = await rpc('exchange_sendTransaction', { ...payload.tx, signature, public_key: bytesToHex(publicKey) })

Admission checks the signature, that the key derives to sender, the nonce window and the gas price; the node then gossips the transaction, and any validator can include it. It executes only in a block.

4. Read the receipt

exchange_getTransactionReceipt(tx_hash) (or GET /get_exchange_receipt) returns null while the transaction is pending, then:

{ "tx_hash": "…", "block_height": 812, "op": "PERP_ORDER", "sender": "0xPQ…", "nonce": 4,
  "success": true, "error": "", "gas_used": 60000, "fee": "0.00006",
  "data": { "order_id": "3f9a…", "resting": false,
            "fills": [{ "market": "BTC-USD-PERP", "price": "65000", "amount": "0.5", "maker": "0xPQ…" }] } }

data depends on the operation: an order id and fills, a swap's amounts, a new pool or position id, a token address. Nodes keep the latest 50,000 receipts.

Swaps: set a minimum

Quote first with exchange_quoteSwap, then send min_amount_out = quote × (1 − tolerance). If nothing trades in between, the swap gets exactly the quote; if the price moves past your tolerance, it fails and changes nothing.

From a dApp

With QRDX Wallet you don't sign yourself: qrdx_sendExchangeTransaction({ op, params }) shows the user a decoded approval, signs with their key and nonce, and returns { txHash, nonce }. See Wallet integration.

On this page