> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Placing orders

> HTTP request schema, validation rules, and worked example for POST /api/orders.

```http theme={null}
POST /api/orders/place
X-Api-Key: ps_live_<keyId>_<secret>   # needs `orders:write` scope
Content-Type: application/json
```

See [API keys](/auth/api-keys) for format, scope taxonomy,
`associatedWallet` requirement, and rate-limit buckets.

## Request body

```typescript theme={null}
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

| Field                  | Rule                                                                                                                                                                                                                                                           | Failure                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `price`                | `0 < 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` + `timeInForce` | MARKET orders require `ioc` or `fok`; MARKET+GTC rejected                                                                                                                                                                                                      | `400 invalid_tif`                                        |
| `signature`            | EIP-712 recover (full on-chain `Order` struct) must equal an EOA whose vault == `maker`                                                                                                                                                                        | `400 bad_signature`                                      |
| `expiry`               | `0` (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` |
| Balance                | `available >= price × quantity` (BUY)                                                                                                                                                                                                                          | `200 REJECTED`, `code: insufficient_funds`               |
| Position               | `vault.erc1155(tokenId) >= quantity` (SELL)                                                                                                                                                                                                                    | `200 REJECTED`, `code: insufficient_position`            |
| Market                 | `market.status === 'OPEN'`                                                                                                                                                                                                                                     | `409 market_not_open`                                    |

## Worked example — BUY 2 YES at 0.42

```typescript theme={null}
import { Wallet, randomBytes, parseUnits } from 'ethers';

// 1. Resolve vault, outcome tokenId, AND the per-market taker fee
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'
const feeBps   = BigInt(market.feeTakerBps);  // CRITICAL — see callout below

// 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);
// CEIL on BUY notional. FLOOR-signed BUY orders are rejected at
// placement (code: order_signed_with_floor_notional) because they
// overflow the matcher's per-fill CEIL math on the closing tail
// fill. See /concepts/trading/eip712-signing#rounding-rule-ceil-on-buy-floor-on-sell-notional
const notionalWei = (priceWei * qtyWei + 999_999n) / 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: feeBps,              // MUST equal market.feeTakerBps
  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());
```

## Response shape

```typescript theme={null}
interface PlaceOrderResp {
  orderId: string;             // server-issued UUID, '' when status=REJECTED
  status: 'PENDING' | 'OPEN' | 'FILLED' | 'CANCELLED' | 'REJECTED' | 'EXPIRED' | 'SETTLEMENT_FAILED';
  filledQty: string;           // cumulative fill at response time (decimal)
  remainingQty: string;        // quantity - filledQty
  trades: TradeFill[];         // synchronous fills produced by IOC/FOK or aggressive LIMIT
  code?: string;               // present when status=REJECTED (insufficient_funds, …)
  message?: string;            // human-readable explanation; pairs with code
  clientOrderId?: string;      // echoed from request when supplied
}
```

The `clientOrderId` field is **echoed verbatim** when the request supplied
one — present on every shape (success, REJECTED, idempotent replay). Use
it to correlate the server-issued `orderId` to your own client-side handle
without a follow-up `GET /api/orders/{id}`. The same value is also carried
on the `events.order_placed` and `events.order_cancelled` frames over
`/ws/user`, so partners that drive their state machine off the WS feed get
the correlation key on both surfaces.

## Batch placement

Submit up to **10 orders in one request** with `POST /api/orders/place-batch`.
Each entry is the exact same body as single [placement](#request-body); the
batch is processed **sequentially in array order** with **independent
per-order outcomes** — one rejected order never aborts the rest, and there
is no all-or-nothing atomicity.

```http theme={null}
POST /api/orders/place-batch
X-Api-Key: ps_live_<keyId>_<secret>   # needs `orders:write` scope
Content-Type: application/json

{ "orders": [ /* 1–10 PlaceOrderReq objects, same shape as single place */ ] }
```

The response is an **array aligned to the request by index**. Each item is a
`PlaceOrderResp` (identical to single place) plus a `success` flag:

```json theme={null}
[
  { "success": true,  "orderId": "0x1f...", "status": "OPEN",     "filledQty": "0", "remainingQty": "2", "trades": [], "clientOrderId": "mm-1" },
  { "success": false, "orderId": "",        "status": "REJECTED", "filledQty": "0", "remainingQty": "0", "trades": [], "code": "insufficient_funds", "message": "..." }
]
```

<Note>
  **Per-wallet limits accumulate across the batch.** KYC deposit-tier,
  responsible-gambling, and partner trade caps are charged against the
  running total of accepted entries, so a batch can never admit more
  exposure than the same orders sent one at a time.
</Note>

<Warning>
  **Business reject vs infrastructure failure.** A rejected *order* (bad
  signature, insufficient funds, …) is a per-entry `success: false` with
  HTTP 200 — the rest of the batch still processes. An *infrastructure*
  failure (matcher / verifier / DB unavailable) fails the **whole request**
  with a 5xx. On a 5xx, retry the whole batch: every forwarded entry carries
  a `clientOrderId` (the server mints one when you omit it), so the replay
  returns the already-committed result via idempotency rather than placing
  duplicates.
</Warning>

Rate limited at **5 req/sec/wallet** (up to 50 orders/sec). KYC deposit-tier
gating applies, same as single place.

## Post-only orders

Set `postOnly: true` to guarantee an order only ever **rests** on the book
and never matches on entry. It's the maker-side guarantee market makers need
to avoid paying taker fees or accidentally crossing their own quotes.

```http theme={null}
POST /api/orders/place
X-Api-Key: ps_live_<keyId>_<secret>   # needs `orders:write` scope
Content-Type: application/json

{ /* ...single-place body... */ "marketId": "UAE-CUP-FINAL-20260425", "type": "limit", "timeInForce": "gtc", "postOnly": true }
```

Only valid for **LIMIT orders with `gtc` time-in-force** (optionally with
`expiry`). Omit it (or send `false`) for today's behavior.

<Warning>
  **Two rejection modes.**

  * **`post_only_invalid_order_type`** (HTTP 400) — `postOnly` was sent with
    a MARKET order or an `ioc` / `fok` time-in-force. The combination is
    invalid; nothing is placed.
  * **`post_only_would_cross`** — the order was marketable at entry: its
    price meets or crosses the best price on the opposite side of the book
    (at-touch equality counts as crossing). It returns
    `{ "status": "REJECTED", "code": "post_only_would_cross" }` with no
    fills — reprice inside the spread and resubmit.
</Warning>

## 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](/concepts/contracts/vaults) for the split flow.

## Next

<CardGroup cols={2}>
  <Card title="Time-in-force" icon="stopwatch" href="/concepts/trading/time-in-force">
    GTC vs IOC vs FOK behaviour.
  </Card>

  <Card title="Cancelling orders" icon="xmark" href="/concepts/trading/cancelling">
    Single-order and cancel-all flows.
  </Card>
</CardGroup>
