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 arraysignature— the Ed25519 signature as a 64-element byte arraynonce— a unique number (useDate.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 unitstime_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 orderreduce_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: [...] }