Skip to main content
Every order placed on the PMX orderbook must be signed by the owner’s Solana wallet. This signature is verified both off-chain (by the matching engine) and on-chain (during settlement). The signing scheme uses Ed25519 with a human-readable hex payload.

Message Structure

The order message is a 92-byte Borsh-serialized struct:
FieldOffsetSizeTypeDescription
market032bytesMarket on-chain address (base58-decoded)
owner3232bytesOwner wallet address (base58-decoded)
side641u80 = Buy, 1 = Sell
outcome651u80 = Yes, 1 = No
priceBps662u16 LEPrice in basis points (1–10,000)
quantity688u64 LEToken quantity in raw units
nonce768u64 LEUnique nonce (prevents replay)
expiry848i64 LEUnix timestamp (seconds) when order expires
Total: 92 bytes

Signed Payload

The actual bytes signed are not the raw 92 bytes. Instead, the payload is a human-readable ASCII string:
PMX Order:\n<hex_of_92_bytes>
This format prevents Privy and other embedded wallets from blocking signMessage when the raw bytes resemble a Solana transaction.

JavaScript Implementation

import nacl from "tweetnacl";
import bs58 from "bs58";

function buildOrderMessage(msg) {
  const buf = Buffer.alloc(92);
  let offset = 0;

  // market (32 bytes)
  buf.set(bs58.decode(msg.market), offset);
  offset += 32;

  // owner (32 bytes)
  buf.set(bs58.decode(msg.owner), offset);
  offset += 32;

  // side (1 byte)
  buf.writeUInt8(msg.side, offset);
  offset += 1;

  // outcome (1 byte)
  buf.writeUInt8(msg.outcome, offset);
  offset += 1;

  // priceBps (2 bytes, little-endian)
  buf.writeUInt16LE(msg.priceBps, offset);
  offset += 2;

  // quantity (8 bytes, little-endian)
  buf.writeBigUInt64LE(BigInt(msg.quantity), offset);
  offset += 8;

  // nonce (8 bytes, little-endian)
  buf.writeBigUInt64LE(BigInt(msg.nonce), offset);
  offset += 8;

  // expiry (8 bytes, little-endian, signed)
  buf.writeBigInt64LE(BigInt(msg.expiry), offset);

  return buf;
}

function signOrderMessage(msg, keypair) {
  const orderBytes = buildOrderMessage(msg);
  const textPayload = "PMX Order:\n" + orderBytes.toString("hex");
  const messageBytes = Buffer.from(textPayload, "utf-8");

  const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
  return Buffer.from(signature).toString("base64");
}

Browser Wallet Signing

When using a browser wallet (Phantom, Solflare, Privy), use the wallet’s signMessage method:
function signWithWallet(msg, wallet) {
  const orderBytes = buildOrderMessage(msg);
  const textPayload = "PMX Order:\n" + orderBytes.toString("hex");
  const messageBytes = new TextEncoder().encode(textPayload);

  // wallet.signMessage returns the signature bytes
  const signatureBytes = await wallet.signMessage(messageBytes);
  return Buffer.from(signatureBytes).toString("base64");
}

Nonce Requirements

The nonce field must be unique per wallet. Re-using a nonce that has already been accepted will result in a NONCE_ALREADY_USED error. Common strategies:
StrategyExample
Timestamp-basedDate.now().toString()
Counter-basedIncrementing integer per wallet
Randomcrypto.randomUUID() converted to a BigInt
Used nonces are tracked permanently to prevent replay attacks.

Expiry

  • Must be in the future at the time of submission
  • Maximum 30 days from now
  • After expiry, the engine automatically cancels the order and emits an order:cancelled event
  • Use Unix timestamp in seconds (not milliseconds)
// Expire in 24 hours
const expiry = Math.floor(Date.now() / 1000) + 86400;

Cancel Signing

To cancel an order, you must sign a cancel message proving you own it. The format is:
PMX Cancel:<orderId>
For example:
PMX Cancel:b2c3d4e5-1234-5678-abcd-ef0123456789

JavaScript Implementation

function signCancelMessage(orderId, keypair) {
  const message = `PMX Cancel:${orderId}`;
  const messageBytes = new TextEncoder().encode(message);
  const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
  return Buffer.from(signature).toString("base64");
}

Browser Wallet Cancel Signing

async function signCancelWithWallet(orderId, wallet) {
  const message = `PMX Cancel:${orderId}`;
  const messageBytes = new TextEncoder().encode(message);
  const signatureBytes = await wallet.signMessage(messageBytes);
  return Buffer.from(signatureBytes).toString("base64");
}
Submit the cancel via POST /v2/clob/orders/{orderId}/cancel with { pubkey, signature } in the body. See Cancel Order for details.