Skip to main content
This guide walks through every step of trading on the PMX orderbook using the REST API. The CLOB runs off-chain for speed, while settlement happens on-chain by transferring USDC between users’ personal smart wallets.

Prerequisites

  • A Solana wallet with SOL (for tx fees) and USDC (for trading)
  • @solana/web3.js, bs58, and tweetnacl installed
import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";

const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY));
const API = "https://api.pmx.trade";
const pubkey = wallet.publicKey.toBase58();
const headers = { "Content-Type": "application/json" };

async function signAndSend(base64Tx) {
  const tx = VersionedTransaction.deserialize(Buffer.from(base64Tx, "base64"));
  tx.sign([wallet]);
  const sig = await connection.sendRawTransaction(tx.serialize());
  await connection.confirmTransaction(sig, "confirmed");
  return sig;
}

Step 1 — Deposit USDC

Deposit USDC into your personal on-chain smart wallet. Your balance is the actual SPL token balance in your smart wallet and can be withdrawn at any time.
// Check if already set up (smart wallet balance > 0)
const { ready, smartWalletBalance } = await fetch(`${API}/v2/clob/account/setup?pubkey=${pubkey}`).then(r => r.json());

if (!ready) {
  // Deposit 100 USDC (raw units, 6 decimals)
  const { transaction } = await fetch(`${API}/v2/clob/account/deposit`, {
    method: "POST",
    headers,
    body: JSON.stringify({ pubkey, amount: "100000000" }),
  }).then(r => r.json());
  await signAndSend(transaction);
  console.log("USDC deposited to smart wallet");
}
You can deposit additional USDC at any time and withdraw your available balance whenever you want via /account/withdraw. The exchange transfers USDC between personal smart wallets during on-chain settlement.

Step 2 — Check Your Balance

Verify your smart wallet balance, available USDC, and any existing positions:
const { smartWalletBalance, withdrawable, walletBalance, totalCommitted, markets } = await fetch(
  `${API}/v2/clob/account/balances?pubkey=${pubkey}`
).then(r => r.json());

console.log(`Smart Wallet Balance: $${(Number(smartWalletBalance) / 1e6).toFixed(2)}`);
console.log(`Withdrawable: $${(Number(withdrawable) / 1e6).toFixed(2)}`);
console.log(`Wallet Balance: $${(Number(walletBalance) / 1e6).toFixed(2)}`);
console.log(`Committed: $${(Number(totalCommitted) / 1e6).toFixed(2)}`);
All amounts are in raw USDC units (6 decimals). 100000000 = $100. 1000000 = $1. smartWalletBalance is the actual USDC token balance in your personal smart wallet. withdrawable is the amount you can withdraw (smart wallet balance minus committed). walletBalance is the USDC remaining in your wallet.

Step 3 — Browse Markets

Find an active market to trade:
const { markets } = await fetch(`${API}/v2/clob/markets`).then(r => r.json());
const market = markets.find(m => m.status === "active");

console.log(market.title);
console.log(`YES: bid ${market.bestBid.yes} / ask ${market.bestAsk.yes}`);
console.log(`NO:  bid ${market.bestBid.no} / ask ${market.bestAsk.no}`);
Check the full orderbook depth:
const book = await fetch(
  `${API}/v2/clob/markets/${market.id}/orderbook?levels=5`
).then(r => r.json());

console.log("YES bids:", book.yes.bids); // [{ price: 5500, size: "50000000" }, ...]
console.log("YES asks:", book.yes.asks); // [{ price: 5800, size: "30000000" }, ...]

Step 4 — Place Orders

Limit order (rests in book)

Buy 50 YES tokens at $0.55:
const orderMessage = {
  market: market.marketPubkey,
  owner: pubkey,
  side: 0,          // 0 = buy, 1 = sell
  outcome: 0,       // 0 = yes, 1 = no
  priceBps: 5500,   // $0.55
  quantity: "50000000",
  nonce: Date.now().toString(),
  expiry: Math.floor(Date.now() / 1000 + 86400).toString(),
};

// Sign the order message (Borsh-serialized 92-byte format)
const orderSig = signOrderMessage(orderMessage, wallet);

const { orderId, status, fills } = await fetch(`${API}/v2/clob/orders`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    orderMessage,
    signature: orderSig,
    orderType: "limit",
    timeInForce: "gtc",
  }),
}).then(r => r.json());

console.log(`Order ${orderId}: ${status}`);
console.log(`Filled: ${fills.length} trades`);

Market order (fills immediately)

const marketOrder = {
  ...orderMessage,
  priceBps: 9999,  // max price for buy market orders
  nonce: (Date.now() + 1).toString(),
};

const { orderId } = await fetch(`${API}/v2/clob/orders`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    orderMessage: marketOrder,
    signature: signOrderMessage(marketOrder, wallet),
    orderType: "market",
  }),
}).then(r => r.json());

Sell tokens

const sellOrder = {
  ...orderMessage,
  side: 1,          // sell
  priceBps: 6000,   // $0.60
  quantity: "25000000",
  nonce: (Date.now() + 2).toString(),
};

const { orderId: sellOrderId } = await fetch(`${API}/v2/clob/orders`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    orderMessage: sellOrder,
    signature: signOrderMessage(sellOrder, wallet),
    orderType: "limit",
    timeInForce: "gtc",
  }),
}).then(r => r.json());
Order types: limit rests in the book at your price. market fills at best available and cancels the rest.Time-in-force: gtc (default) stays until filled/cancelled. ioc fills what it can and cancels the rest. fok must fill entirely or is rejected.Identity: No auth token needed — the Ed25519 signature on the order message proves ownership. The on-chain program verifies this during settlement.

Step 5 — Manage Orders

List your open orders

const { orders } = await fetch(
  `${API}/v2/clob/orders?pubkey=${pubkey}&status=open`
).then(r => r.json());

for (const o of orders) {
  console.log(`${o.id}${o.side} ${o.outcome} @ ${o.priceBps} bps, remaining: ${o.remainingQuantity}`);
}

Cancel an order

Cancellation requires an Ed25519 signature to prove you own the order:
const orderId = orders[0].id;

// Sign the cancel message
const cancelMessage = `PMX Cancel:${orderId}`;
const cancelSig = nacl.sign.detached(
  new TextEncoder().encode(cancelMessage),
  wallet.secretKey
);
const cancelSignature = Buffer.from(cancelSig).toString("base64");

const { success } = await fetch(`${API}/v2/clob/orders/${orderId}/cancel`, {
  method: "POST",
  headers,
  body: JSON.stringify({ pubkey, signature: cancelSignature }),
}).then(r => r.json());
console.log("Cancelled:", success); // true

Step 6 — View Trade History

Your fills

const { fills } = await fetch(
  `${API}/v2/clob/fills?pubkey=${pubkey}&limit=10`
).then(r => r.json());

for (const f of fills) {
  const side = f.takerSide === "buy" ? "BOUGHT" : "SOLD";
  console.log(`${f.matchedAt}${side} ${f.quantity} ${f.outcome} @ ${f.priceBps} bps (${f.status})`);
}

Public market trades

const { trades } = await fetch(
  `${API}/v2/clob/markets/${market.id}/trades?limit=10`
).then(r => r.json());

for (const t of trades) {
  console.log(`${t.matchedAt}${t.takerSide} ${t.quantity} ${t.outcome} @ ${t.priceBps} bps`);
}

Step 7 — Track Positions & P&L

Check your positions with cost basis, realized and unrealized P&L:
// All positions across markets
const { positions } = await fetch(
  `${API}/v2/clob/positions?pubkey=${pubkey}`
).then(r => r.json());

for (const p of positions) {
  const qty = (Number(p.netQuantity) / 1e6).toFixed(2);
  const avgEntry = (p.avgEntryPrice / 100).toFixed(1);
  const current = p.currentPrice ? (p.currentPrice / 100).toFixed(1) : "—";
  const realized = (Number(p.realizedPnl) / 1e6).toFixed(2);
  const unrealized = p.unrealizedPnl ? (Number(p.unrealizedPnl) / 1e6).toFixed(2) : "—";
  console.log(`${p.outcome.toUpperCase()}${qty} shares @ avg ${avgEntry}¢ (now ${current}¢)`);
  console.log(`  Realized: $${realized} | Unrealized: $${unrealized}`);
}
Get a portfolio-level summary:
const summary = await fetch(
  `${API}/v2/clob/positions/summary?pubkey=${pubkey}`
).then(r => r.json());

const totalPnl = (Number(summary.totalRealizedPnl) + Number(summary.totalUnrealizedPnl)) / 1e6;
console.log(`Total P&L: $${totalPnl.toFixed(2)}`);
console.log(`Active positions: ${summary.activePositions}`);
Positions are updated automatically after each settled fill. Realized P&L is locked in when you sell tokens. Unrealized P&L is computed on-the-fly from the current orderbook mid-price and may fluctuate.

Step 8 — Redeem Winnings

After a market resolves, burn your winning tokens for USDC:
// Check which markets are resolved
const { markets: allMarkets } = await fetch(`${API}/v2/clob/markets`).then(r => r.json());
const resolved = allMarkets.filter(m => m.status === "resolved");

// Check your balances
const { markets: balances } = await fetch(
  `${API}/v2/clob/account/balances?pubkey=${pubkey}`
).then(r => r.json());

for (const m of resolved) {
  const bal = balances.find(b => b.marketId === m.id);
  if (!bal) continue;

  const winningTokens = m.outcome === "yes" ? bal.yesTokens : bal.noTokens;
  if (winningTokens === "0") continue;

  const { transaction } = await fetch(`${API}/v2/clob/account/redeem`, {
    method: "POST",
    headers,
    body: JSON.stringify({ pubkey, marketId: m.id, amount: winningTokens }),
  }).then(r => r.json());

  await signAndSend(transaction);
  console.log(`Redeemed ${winningTokens} tokens from "${m.title}"`);
}
Winning tokens redeem for $1 USDC each (10,000 bps). If you hold 50 YES tokens and YES wins, you receive $50 USDC deposited into your personal smart wallet.

Step 9 — Withdraw USDC

Withdraw your available USDC from your personal smart wallet back to your wallet at any time:
const { withdrawable } = await fetch(
  `${API}/v2/clob/account/balances?pubkey=${pubkey}`
).then(r => r.json());

if (Number(withdrawable) > 0) {
  const { transaction } = await fetch(`${API}/v2/clob/account/withdraw`, {
    method: "POST",
    headers,
    body: JSON.stringify({ pubkey, amount: withdrawable }),
  }).then(r => r.json());

  await signAndSend(transaction);
  console.log(`Withdrawn $${(Number(withdrawable) / 1e6).toFixed(2)} USDC to wallet`);
}
You can only withdraw your withdrawable balance — USDC that is not committed to open orders. Cancel open orders first to free up committed collateral.

Complete Flow Summary

StepEndpointDescription
1. Deposit USDCGET /v2/clob/account/setup?pubkey=POST /v2/clob/account/depositBefore trading
2. Check balanceGET /v2/clob/account/balances?pubkey=Anytime
3. Browse marketsGET /v2/clob/marketsAnytime
4. Place orderPOST /v2/clob/orders (signed order message)While market is active
5a. List ordersGET /v2/clob/orders?pubkey=Anytime
5b. Cancel orderPOST /v2/clob/orders/:id/cancel (signed cancel)While order is open
6a. Your fillsGET /v2/clob/fills?pubkey=Anytime
6b. Market tradesGET /v2/clob/markets/:id/tradesAnytime
7a. PositionsGET /v2/clob/positions?pubkey=Anytime
7b. P&L summaryGET /v2/clob/positions/summary?pubkey=Anytime
8. RedeemPOST /v2/clob/account/redeemAfter resolution
9. WithdrawPOST /v2/clob/account/withdrawAnytime
For a concise version of this guide, see the Quickstart. For full API details on every endpoint, see the API Reference.