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`);
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
| Step | Endpoint | Description |
|---|
| 1. Deposit USDC | GET /v2/clob/account/setup?pubkey= → POST /v2/clob/account/deposit | Before trading |
| 2. Check balance | GET /v2/clob/account/balances?pubkey= | Anytime |
| 3. Browse markets | GET /v2/clob/markets | Anytime |
| 4. Place order | POST /v2/clob/orders (signed order message) | While market is active |
| 5a. List orders | GET /v2/clob/orders?pubkey= | Anytime |
| 5b. Cancel order | POST /v2/clob/orders/:id/cancel (signed cancel) | While order is open |
| 6a. Your fills | GET /v2/clob/fills?pubkey= | Anytime |
| 6b. Market trades | GET /v2/clob/markets/:id/trades | Anytime |
| 7a. Positions | GET /v2/clob/positions?pubkey= | Anytime |
| 7b. P&L summary | GET /v2/clob/positions/summary?pubkey= | Anytime |
| 8. Redeem | POST /v2/clob/account/redeem | After resolution |
| 9. Withdraw | POST /v2/clob/account/withdraw | Anytime |
For a concise version of this guide, see the Quickstart. For full API details on every endpoint, see the API Reference.