Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Signing transactions

Every transaction on Parcl V4 is signed with Ed25519. The signing flow: build the transaction JSON, serialize it with a nonce and timestamp, sign the bytes, and submit.

Transaction format

A signed transaction has five fields:

{
  "transaction": { "PlaceOrder": { ... } },
  "signer": [1, 2, 3, ...],
  "signature": [4, 5, 6, ...],
  "nonce": 1712345678901,
  "timestamp": 1712345678901
}
  • transaction — the operation (PlaceOrder, CancelOrder, etc.)
  • signer — your Ed25519 public key as a 32-element byte array
  • signature — the Ed25519 signature as a 64-element byte array
  • nonce — a unique number (use Date.now() — must be unique per tx)
  • timestamp — Unix timestamp in milliseconds

Signing message

The message to sign is the JSON serialization of [transaction, nonce, timestamp]:

const message = JSON.stringify([transaction, nonce, timestamp]);
const signature = ed25519.sign(message, privateKey);

The validator deserializes and re-serializes to verify, so your JSON must match the exact serde format the Rust validator expects. Use the same field order as shown in the transaction types below.

Submitting

POST /v1/tx
Content-Type: application/json
 
{
  "transaction": { ... },
  "signer": [...],
  "signature": [...],
  "nonce": 1712345678901,
  "timestamp": 1712345678901
}

Response:

{
  "status": "success",
  "hash": "abc123...",
  "events": [...]
}

Or on failure:

{
  "status": "failed",
  "error": "insufficient margin"
}

Transaction types

PlaceOrder

{
  "PlaceOrder": {
    "account_id": 12,
    "market_id": 0,
    "side": "Long",
    "order_type": "Market",
    "price": 58000000000,
    "size": 1000000,
    "trigger_price": null,
    "reduce_only": false,
    "post_only": false,
    "time_in_force": "GTC"
  }
}
  • side"Long" or "Short"
  • order_type"Market", "Limit", "StopLimit", "StopMarket"
  • price — 8-decimal scaled. For market orders, use 0 (fills at best available)
  • size — 6-decimal scaled. 1000000 = 1.0 units
  • time_in_force"GTC" (good til cancel), "IOC" (immediate or cancel), "FOK" (fill or kill)
  • trigger_price — for stop orders, the oracle price that triggers the order
  • reduce_only — if true, only reduces existing position (won't open a new one)
  • post_only — if true, rejects if it would fill immediately (maker-only)

Optional attached TP/SL:

{
  "PlaceOrder": {
    ...
    "take_profit": {
      "trigger_price": 60000000000,
      "order_type": "Market",
      "limit_price": null
    },
    "stop_loss": {
      "trigger_price": 55000000000,
      "order_type": "Market",
      "limit_price": null
    }
  }
}

CancelOrder

{
  "CancelOrder": {
    "order_id": 42
  }
}

CancelAllOrders

{
  "CancelAllOrders": {
    "market_id": 0
  }
}

Pass null for market_id to cancel across all markets.

ModifyOrder

{
  "ModifyOrder": {
    "order_id": 42,
    "new_price": 59000000000,
    "new_size": null
  }
}

Fields are optional — only set what you want to change.

AddCollateral

{
  "AddCollateral": {
    "account_id": 12,
    "amount": 5000000000
  }
}

Amount in USDC with 6 decimals. 5000000000 = $5,000.

RemoveCollateral

{
  "RemoveCollateral": {
    "account_id": 12,
    "amount": 1000000000
  }
}

Example: TypeScript signing

import { ed25519 } from "@noble/ed25519";
 
async function signAndSubmit(
  privateKey: Uint8Array,
  transaction: object
) {
  const publicKey = await ed25519.getPublicKeyAsync(privateKey);
  const nonce = Date.now();
  const timestamp = Date.now();
 
  // Build signing message (must match Rust serde format)
  const message = JSON.stringify([transaction, nonce, timestamp]);
  const messageBytes = new TextEncoder().encode(message);
  const signature = await ed25519.signAsync(messageBytes, privateKey);
 
  const body = {
    transaction,
    signer: Array.from(publicKey),
    signature: Array.from(signature),
    nonce,
    timestamp,
  };
 
  const res = await fetch("https://v4-api.dev.parcllabs.com/v1/tx", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
 
  return res.json();
}
 
// Place a market buy on NYC
const result = await signAndSubmit(myPrivateKey, {
  PlaceOrder: {
    account_id: 12,
    market_id: 0,
    side: "Long",
    order_type: "Market",
    price: 0,
    size: 100000,  // 0.1 units
    trigger_price: null,
    reduce_only: false,
    post_only: false,
    time_in_force: "IOC",
  },
});
 
console.log(result);
// { status: "success", hash: "abc123...", events: [...] }