Skip to content

LP integration — Concepts

Programmatic integration guide for liquidity providers and market-making teams building against the Parti API: deposit → trade → withdraw → run an LP bot.

This guide is sourced directly from the deployed code. Field names, byte layouts, and error strings are exact. Anything currently gated/disabled is marked GATED.

Overview

Parti is a prediction-market CLOB. Funds are custodied as USDG in an on-chain vault on Robinhood Chain (an EVM chain, chain id 4663); you withdraw on-chain, and your account balance is tracked in micro-USDC (1 USDC = 1,000,000 micro). You identify yourself by a Solana Ed25519 public key (base58) — the same key your Privy embedded wallet exposes.

There are two services an LP talks to:

Service Purpose Routes
The Parti API Auth/session, orders, withdraw, LP bot, markets, balances, positions, WebSocket /v1/builders/*, /v1/orders/*, /v1/withdraw-signed, /v1/lp-bot/*, /v1/markets/*, /v1/balance/*, /v1/positions/*, /v1/ws
Deposit service Per-user deposit address derivation + deposit progress /v1/deposit/robinhood-address/{user}, /v1/deposit/progress/{user}

Base URLs

CORS is gated on Origin, not Host.

The Parti API: https://oracle-api.parti.com. WebSocket = the API base with httpws + /v1/ws.

Deposit service: https://oracle-deposit.parti.com.

Availability

Some endpoints may be limited during rollout and return 403 with a specific error code when unavailable:

Surface When unavailable
Deposits / withdrawals / markets always open
TradingPOST /v1/orders 403 trading_disabled when trading is not currently open
LP bot — all of /v1/lp-bot/* 403 lp_disabled when LP is not currently open

Tolerate trading_disabled / lp_disabled until each surface is open.

Markets & read endpoints

All read endpoints below are on the Parti API and are edge-cached (see Cache-Control / X-Cache: HIT|MISS). Markets/book/trades are public.

Market model

See Resolution — every outcome is its own binary YES/NO with its own market_id; place orders against outcomes[i].market_id, never the bare event_id of a multi-outcome event.

GET /v1/markets

Filterable + paginated event list:

Param Default Notes
status active active | halted | resolved
category case-insensitive exact match on the category name (see values below)
search substring match on question
ends_before / ends_after unix-seconds bound on expiry
neg_risk true | false — filter to negative-risk (mutually-exclusive multi-outcome) sets, e.g. range-bucket ladders + sports moneylines
order volume volume | expiry | created_at
ascending false (true for expiry) sort direction
limit 500 max 500
offset 0 pagination

A non-allowlisted order is rejected with 400 invalid_order (the param is honoured-or-errored, never silently ignored).

Categories. Markets are categorised by the underlying crypto symbol — BTC, ETH, DOGE, SOL, XRP, BNB — plus Sports, Politics, and Culture. There is no crypto category; pass an individual symbol (?category=BTC) instead.

Response: { "markets": Market[], "total": <filtered grand total>, "limit", "offset" }. total is the post-filter grand total across all pages (not the page size); paginate with offset until offset + markets.length >= total.

Market columns: event_id, question, status, category, slug, image, description, tags, expiry, end_date, fee_bps, volume, neg_risk, outcomes, resolution_metadata, resolution_source.

Results are edge-cached for 30s (Cache-Control: public, s-maxage=30).

GET /v1/markets/{id}

Single market by event_id or slug. 404 not_found if no match.

GET /v1/markets/{id}/book

Order-book snapshot (1s edge cache; live updates via WS):

{ "market_id": "…", "bids": [...], "asks": [...], "ts": 1750700123456 }

Falls back to an empty book (bids:[], asks:[]) if the live book is unavailable — it never 5xxs.

GET /v1/markets/{id}/trades?limit={n}

Recent trades, newest first. limit default 50, max 200. 3s edge cache. Response: { "trades": Trade[], "total" }. Each Trade: id, price, size, side, time (unix seconds), market_id, maker, taker, fee, builder_fee.

GET /v1/markets/{id}/candles

Time-bucketed YES-probability OHLC derived natively from the market's fills, as a single YES series (a NO-side fill at price p is folded in as 10000 − p), so the chart is always YES-probability regardless of which side the taker hit.

Param Default Notes
interval 1h one of 1m, 5m, 1h, 1d — anything else is 400 invalid_interval
from to − 500·interval unix-seconds window start (aligned down to a bucket)
to now (current open bucket) unix-seconds window end (aligned up to a bucket)

The span is hard-capped at 1500 buckets (oldest end trimmed). Edge-cached 5s/15s/60s/300s for 1m/5m/1h/1d.

Response:

{ "market_id": "…", "interval": "1h",
  "candles": [ { "t": <unix_seconds>, "o": <0..1>, "h": <0..1>,
                 "l": <0..1>, "c": <0..1>, "v": <contracts>, "n": <trade_count> } ] }

o/h/l/c are YES probability in 0..1; v is summed contracts and n the trade count for the bucket. No trades → candles: [].

Account reads (public)

The pubkey is in the URL, so no session cookie is required (the data is not secret — anyone can read anyone's, like an on-chain explorer).

Endpoint Returns
GET /v1/balance/{user} { user, available, locked, total } micro-USDC (coerce with Number())
GET /v1/position/{market_id}/{user} zero-shaped when never traded: { user, market_id, outcome, size, yes, no, cost_basis, … } (always 200)
GET /v1/positions/all/{user} { positions: [...] }outcome (YES/NO), size, yes, no, cost_basis, avg_price, mark_price, cash_pnl, slug, question, redeemable, end_date, status
GET /v1/orders/all/{user} { orders: [...], total } — live/resting orders
GET /v1/trades/all/{user}?limit={n} { trades: [...], total, limit, offset } — every fill (maker or taker), newest first. limit default 50, max 200
GET /v1/deposits/all/{user}?limit={n} { deposits: [...], total, limit, offset } — on-chain deposit credits, newest first. Each row: tx_signature, amount_micro, seen_at, credited_at, status (pending until credited). limit default 50, max 200
GET /v1/withdrawals/all/{user}?limit={n} { withdrawals: [...], total, limit, offset } — withdrawal history, newest first. Each row: recipient, chain, amount_micro, status, tx_signature, created_at, confirmed_at. limit default 50, max 200

All four per-user lists share the { <resource>: [...], total, limit, offset } envelope — total is the grand total over the whole set (not the page).

LP rewards reads (public)

Endpoint Returns
GET /v1/rewards/config { daily_budget_micro_usdc, maker_rebate_bps, rewards_band_spread_bps_max, per_wallet_cap_pct }
GET /v1/rewards/me?user={pubkey} per-user accrual — see LP bot → How rewards accrue
GET /v1/rewards/wallet/{wallet} identical payload, wallet as path param
GET /v1/rewards/leaderboard?days={1-90}&limit={1-200} { days, rows: [{ rank, maker, micro_usdc, score, days_active }] }
  • POST /v1/orders — submit an order. Returns 403 trading_disabled when trading is not currently open; rate-limited.
  • POST /v1/orders/cancel{ order_id, signature, nonce }. Signature/nonce accepted-but-unverified by the API today (forward-compat).
  • POST /v1/orders/cancel-all{ signature, nonce }{ cancelled: <n> }.

WebSocket

GET /v1/ws — live order-book + trade pushes. market_id is the tradable outcome id. After the upgrade, send subscribe frames:

{ "type": "subscribe", "channel": "book:<market_id>" }
{ "type": "subscribe", "channel": "trades:<market_id>" }

Server acks with { "type": "subscribed", "channel": "…" }. { "type": "unsubscribe", "channel": "…" } to stop; { "type": "ping" }{ "type": "pong", "ts": <unix_ms> }. Bad frames return { "type": "error", "code", "channel", "detail" } (e.g. invalid_channel, bad_json).

book_update streams the live order book, pushed on change:

{ "type": "book_update", "market_id": "…",
  "snapshot": { "market_id": "…",
    "bids": [{ "price": <bps>, "size": <contracts> }],
    "asks": [{ "price": <bps>, "size": <contracts> }],
    "ts": <unix_ms> } }

trade — each fill on the trades channel:

{ "type": "trade", "market_id": "…",
  "trade": { "id": <int>, "price": <bps>, "size": <contracts>,
    "side": "buy" | "sell", "time": <unix_seconds>,
    "market_id": "…", "maker": "<pubkey>", "taker": "<pubkey>", "fee": <micro> } }

No history replay on subscribe

The trades cursor starts at "now" and book subscribers receive only subsequent changes. Seed initial state from GET /v1/markets/{id}/book and …/trades, then apply pushes. Latency is ~1.5s end-to-end (relay alarm cadence).

user:<addr> — per-user portfolio channel (auth-gated)

A private channel that pushes your live balance, positions, and open orders. Unlike the public book: / trades: channels, this one requires proof you control <addr>. Two accepted proofs:

  • Session cookie — if the parti_oracle_session cookie rides along on the WS handshake (same-origin browser flow) and its verified user equals <addr>, the subscribe is admitted with no extra fields.
  • Ed25519 signature — otherwise, attach a fresh signature over the canonical bootstrap message Parti Session\nUser: {addr}\nTimestamp: {unix_seconds} (same message as POST /v1/builders/register), within a 5-minute window:
{ "type": "subscribe", "channel": "user:<addr>",
  "signature": "<hex Ed25519, no 0x>", "timestamp": <unix_seconds> }

On success the server immediately sends a portfolio_snapshot, then a portfolio_update (the full current state) on every detected change:

{ "type": "portfolio_snapshot", "user": "<addr>", "ts": <unix_ms>,
  "state": {
    "balance":  { "available": <micro>, "locked": <micro>, "total": <micro> },
    "positions": [ { "market_id": "…", "outcome": "YES" | "NO",
                     "size": <contracts>, "avg_price": <0..1>,
                     "mark_price": <0..1|null>, "cash_pnl": <micro>,
                     "slug": "…", "redeemable": <bool>, "end_date": <unix|null> } ],
    "open_orders": [ { "id": <int>, "market_id": "…",
                       "side": "buy" | "sell", "outcome": "yes" | "no",
                       "price": <bps>, "original_size": <contracts>,
                       "remaining": <contracts>, "created_at": <unix_seconds> } ] } }

portfolio_update has the identical shape with type: "portfolio_update". A rejected subscribe returns { "type": "portfolio_error", "channel": "user:<addr>", "error": "unauthorized" | "address_mismatch" | "bad_request" } and leaves any public channels on the socket untouched. A valid proof for address A can never authorise user:B.