Developers

Trading

Open and close leveraged positions, place and cancel limit orders, and set take-profit triggers.

Open a position

POST /polymarket/positions/open
{
  "userAddress": "0x…",
  "tokenId":     "7239…",         // YES or NO token (decimal string)
  "conditionId": "0xabcd…",       // bytes32 hex from /polymarket/markets
  "outcome":     "YES",           // "YES" | "NO" — UPPERCASE
  "usdcAmount":  "100.50",        // user margin in dollars (decimal string)
  "leverage":    "3",             // optional, "1" if omitted; "1"–"100"
  "slug":        "btc-2026",      // optional; auto-registers an unknown market

  // Auth — pick one:
  "sessionToken": "…",            // skips signature
  // — or —
  "signature": "0x…",             // OpenPositionIntent
  "nonce":     12,
  "deadline":  1700000000
}

outcome controls which side is bought. With outcome: "NO" plus the YES tokenId, the backend buys the NO complement of that market. Most integrators should set tokenId to whichever side they want and outcome to match — match outcome to the outcome field of the market row for tokenId (and complementOutcome for complementTokenId).

leverage = 1 opens without a loan and never liquidates. leverage > 1 borrows (leverage − 1) × usdcAmount from the lending pool and posts shares as isolated collateral; the position carries a liquidationPrice that the engine monitors continuously.

The current binding leverage cap can be lower than market.maxLeverage because of orderbook depth or bid/ask spread. Either pre-check via /polymarket/order-preview or be prepared for HTTP 400 "Maximum leverage is Nx" and retry with a smaller value. The backend clamps to whatever cap is binding right now; it never silently rounds down.

Response

{
  "positionId": 789,
  "tokenId": "7239…",
  "outcome": "YES",
  "sharesAmount": "0", // populated on transition to OPEN
  "entryPrice": "0",
  "liquidationPrice": "0",
  "status": "PENDING",
}

Lifecycle

PENDING → OPENING → OPEN (happy path) PENDING → OPENING → FAILED (CLOB rejected — funds auto-recovered, loan auto-repaid)

Poll status (the owner's trading session, as on GET …/take-profit below):

GET /polymarket/positions/{positionId}/status?userAddress=0x…
x-trading-session: <sessionToken>
→ {
    "id": 789, "status": "OPEN",
    "entryPrice": "0.62", "sharesAmount": "162.10", "liquidationPrice": "0.45",
    "currentPrice": "0.60", "pnlUsd": "-3.24",
    "distanceToLiquidation": "0.150000", "pctToLiquidation": "60.0"
  }

Typical end-to-end latency (PENDING → OPEN): single-digit seconds. On a FAILED terminal state, read the position once with GET /polymarket/positions/{id}?userAddress=0x… (same header) — errorMessage describes the CLOB rejection. Both reads answer 404 for a position the session's wallet does not own.

Close a position

POST /polymarket/positions/{positionId}/close
{
  "userAddress": "0x…",
  "sessionToken": "…",           // OR signature/nonce/deadline
  "signature": "0x…",            // ClosePositionIntent over { userAddress, positionId, nonce, deadline }
  "nonce": 7,
  "deadline": 1700000000
}
→ { "positionId": 789, "exitPrice": "0.61", "sharesAmount": "0", "status": "CLOSING" }

The backend market-sells the entire position, auto-repays any loans (including positions that merged loans together), and credits remaining equity to the Deposit Wallet. The position transitions OPEN → CLOSING → CLOSED. There is no liquidation penalty on a voluntary close, though a close-side trading fee applies (see Fees).

When the market is resolved, close is automatic via on-chain CTF redemption (no orderbook needed). The backend handles winning legs at $1 and losing legs at $0 without integrator intervention; the position transitions to CLOSED once the redemption batch lands.

If the position is being liquidated, the close call errors with 409 — wait for the executor to finish.

Limit orders

The order-first API places resting limit orders that fund a position when they fill. The order's nominal margin × leverage is locked the moment the order is placed (loan issued, USDC locked in the Deposit Wallet); cancelling unwinds the loan.

POST /polymarket/orders
{
  "userAddress":  "0x…",
  "tokenId":      "7239…",
  "outcome":      "YES",        // "YES" | "NO"
  "leverage":     3,            // number (not string)
  "marginUsdc":   100.50,       // number, dollars
  "limitPrice":   0.60,         // number, [0, 1)
  "slug":         "btc-2026",   // optional
  "conditionId":  "0xabcd…",    // optional but recommended
  "sessionToken": "…"           // REQUIRED for placement (no signed variant yet)
}
→ {
  "order": {
    "id": 999, "userAddress": "0x…", "tokenId": "7239…",
    "side": "LONG", "clobSide": "BUY",
    "leverage": "3", "margin": "100.50",
    "limitPrice": "0.60",
    "sharesRequested": "500.00", "sharesFilled": "0",
    "avgFillPrice": null, "status": "OPEN",
    "loanId": 456, "positionId": null,
    "createdAt": "…", "updatedAt": "…"
  }
}

Order lifecycle: OPEN → PARTIALLY_FILLED → FILLED (or CANCELLED). Once filled (fully or partially), a position row appears with the order's loanId attached.

Cancel a limit order

POST /polymarket/orders/{orderId}/cancel
{
  "userAddress": "0x…",
  "sessionToken": "…"             // OR signature/nonce/deadline (CancelOrderIntent)
}
→ { "success": true }

List own orders

GET /polymarket/orders/{userAddress}[?statuses=OPEN,PARTIALLY_FILLED]
GET /polymarket/orders/{orderId}/status?userAddress=0x…

Take-profit

POST   /polymarket/positions/{id}/take-profit   { userAddress, sessionToken, takeProfitPrice: "0.85" }
GET    /polymarket/positions/{id}/take-profit?userAddress=…   (sessionToken in header or query)
DELETE /polymarket/positions/{id}/take-profit   { userAddress, sessionToken }

takeProfitPrice is the YES-equivalent exit price (i.e. always the price of the side held). The backend monitors and auto-closes when the price crosses. Requires a trading session — no signed variant.