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
http→ws + /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 |
Trading — POST /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):
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 }] } |
Trading endpoints (session cookie)¶
POST /v1/orders— submit an order. Returns403 trading_disabledwhen 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_sessioncookie 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 asPOST /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.