Skip to main content
POST /api/orders/place
X-Api-Key: ps_live_<keyId>_<secret>   # needs `orders:write` scope
Content-Type: application/json
See API keys for format, scope taxonomy, associatedWallet requirement, and rate-limit buckets.

Request body

interface PlaceOrderReq {
  marketId: string;          // app-level symbol, e.g. "NC26-BIN-83479265"
  side: 'buy' | 'sell';      // lowercase
  outcome: string;           // "0" = YES (first), "1" = NO (second)
  price: string;             // decimal USDC, e.g. "0.42"
  quantity: string;          // decimal outcome qty, e.g. "2"
  nonce: string;             // decimal-string of the salt used in the EIP-712 struct
  expiry: number;            // unix seconds (= Order.expiration); use 0 for no on-chain expiry
  maker: string;             // VAULT address (= VaultFactory.vaultOf(signer))
  signature: string;         // 0x-prefixed EIP-712 signature
  clientOrderId?: string;    // idempotency key (optional, scoped per associatedWallet)
  type?: 'limit' | 'market'; // default 'limit'
  timeInForce?: 'gtc' | 'ioc' | 'fok'; // default 'gtc' for limit, 'ioc' for market
}
The request body is whitelisted (forbidNonWhitelisted); any extra field returns 400 validation_failed. maker is the vault address. The signing EOA is recovered from signature and must satisfy vaultFactory.vaultOf(signer) == maker. The backend re-runs that check before storing the order, so passing the EOA in maker instead of the vault returns 400 bad_signature.

Validation rules

FieldRuleFailure
price0 < p < 1 (strict β€” p >= 1 is rejected to block the fee = k Γ— P Γ— (1βˆ’P) sign flip at P > 1). Tick size is 0.01 β€” at most 2 decimal places (integer cents, 99 distinct levels). 0.505 returns invalid_amounts with β€œprice tick size is 0.01”.400 invalid_amounts
quantity> 0, at most 6 decimal places (USDC scale).400 invalid_amounts
type + timeInForceMARKET orders require ioc or fok; MARKET+GTC rejected400 invalid_tif
signatureEIP-712 recover (full on-chain Order struct) must equal an EOA whose vault == maker400 bad_signature
expiry0 (no expiry, recommended) or unix-seconds in the future. There is no minimum TTL.400 expired
outcome"0" or "1". The platform maps this to the on-chain tokenId for the market.400 invalid_outcome / 409 market_outcomes_not_seeded
Balanceavailable >= price Γ— quantity (BUY)200 REJECTED, code: insufficient_funds
Positionvault.erc1155(tokenId) >= quantity (SELL)200 REJECTED, code: insufficient_position
Marketmarket.status === 'OPEN'409 market_not_open

Worked example β€” BUY 2 YES at 0.42

import { Wallet, randomBytes, parseUnits } from 'ethers';

// 1. Resolve vault and outcome tokenId
const vault    = await vaultFactory.vaultOf(wallet.address);
const market   = await fetch(`${BASE}/api/markets/NC26-BIN-83479265`).then(r => r.json());
const tokenId  = market.yesTokenId; // outcome '0'

// 2. Build + sign EIP-712 (see /concepts/trading/eip712-signing)
const salt        = BigInt('0x' + randomBytes(32).toString('hex'));
const priceWei    = parseUnits('0.42', 6);
const qtyWei      = parseUnits('2',    6);
const notionalWei = (priceWei * qtyWei) / 1_000_000n;

const orderStruct = {
  salt, maker: vault, signer: wallet.address,
  taker: '0x0000000000000000000000000000000000000000',
  tokenId: BigInt(tokenId),
  makerAmount: notionalWei,        // BUY β†’ USDC notional
  takerAmount: qtyWei,             // BUY β†’ outcome qty
  expiration: 0n, feeRateBps: 0n,
  side: 0, signatureType: 1,       // BUY, VAULT
};
const signature = await wallet.signTypedData(domain, types, orderStruct);

// 3. POST
const resp = await fetch(`${BASE}/api/orders/place`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.PS_API_KEY! },
  body: JSON.stringify({
    marketId:      'NC26-BIN-83479265',
    side:          'buy',
    outcome:       '0',
    price:         '0.42',
    quantity:      '2',
    nonce:         salt.toString(),
    expiry:        0,
    maker:         vault,
    signature,
    clientOrderId: `mm-bot-${Date.now()}`,
    type:          'limit',
    timeInForce:   'gtc',
  }),
}).then(r => r.json());

Sell orders

Same shape; side: 'sell'. Balance is not locked for SELL orders β€” the outcome token (ERC-1155) must already sit in the vault, not the EOA. Position resolution is keyed by maker (the vault), which is why on-chain splits (USDC β†’ YES + NO) must be initiated by vault.splitPosition(...) rather than by the EOA directly. See Vaults for the split flow.

Next

Time-in-force

GTC vs IOC vs FOK behaviour.

Cancelling orders

Single-order and cancel-all flows.