# Refract Funding Trader API — Full Reference for Agents Base URL: https://refractfunding.com/v1 OpenAPI 3.1 spec (canonical, machine-readable): https://refractfunding.com/v1/openapi.json Refract Funding is a prediction-market prop firm selling instant funded SIMULATED trading accounts. This API trades the funded simulated account against live Kalshi and Polymarket order books; no user order is sent to a venue. The only rule that can end a funded account is the trailing equity drawdown (see /v1/risk). There is no profit target. ## Authentication Every request: `Authorization: Bearer rf_live_<64 hex>`. Create and revoke keys in the dashboard: Settings → API Keys (requires an active funded account). A key binds to ONE funded account at creation. The raw key is shown once. Scopes: `read` (all GETs + /v1/quote), `trade` (/v1/orders). Default keys carry both. Missing/invalid key → 401 `{"error":{"code":"UNAUTHORIZED",...}}`. ## Conventions - Money: integer micro-USD, fields suffixed `Micros`. 1 USD = 1,000,000. $250.50 = 250500000. - Prices/probabilities: decimals in [0,1]. A YES at 37 cents = 0.37. - Timestamps: ISO 8601 UTC strings. - Market references (anywhere a `market` is accepted): the `id` returned by this API, `kalshi:` (e.g. kalshi:KXBTCD-25AUG09-B116999.99), `kalshi_perp:` (e.g. kalshi_perp:KXBTCPERP), `polymarket:` (the YES outcome's CLOB token id), or `polymarket:` (the PM site's market slug). ## Predictable market names (construct the next window yourself) Rolling Up/Down windows have arithmetic names; you never need to wait for a listing: - Polymarket intraday: `-updown--` (e.g. btc-updown-15m-1786421700; the trailing number is the window START in unix seconds, so the NEXT 15m window is +900, 5m is +300, 1h/4h are +3600/+14400). Use as `polymarket:`. - Polymarket hourly/daily verbose slugs also resolve (e.g. bitcoin-up-or-down-august-11-2026-1am-et). - Kalshi 15m windows: series `KX15M`; the full ticker for each window appears in GET /v1/series as venueMarketId and on the Kalshi site. Use as `kalshi:`. GET /v1/series returns every live series with the CURRENT window and the next few, each carrying marketId + venueMarketId + slug + closeTime (ISO; the window rolls exactly there). - Domain rejects are HTTP 200 with `status:"REJECTED"` + `reason`. HTTP errors: 400 VALIDATION_ERROR / UNSUPPORTED_MARKET, 401 UNAUTHORIZED, 403 FORBIDDEN_SCOPE, 404 MARKET_NOT_FOUND / NO_FUNDED_SEAT, 429 RATE_LIMITED (with Retry-After seconds), 500 INTERNAL_ERROR. Envelope: {"error":{"code","message","details?"}}. - Rate limits per key, 60s fixed windows: reads 300/min, quotes 300/min, orders 60/min. - No streaming endpoints exist. Poll reads or quote on demand. ## Trading model Orders are taker-only and immediate against the live venue book; unfilled remainder is cancelled, never rested. Optional `limitPriceProb` (binary markets only, probability in (0,1)) adds IOC price protection: a BUY consumes only ask levels ≤ the limit, a SELL only bid levels ≥ it — what fits fills (possibly PARTIAL), and a best level outside the limit rejects LIMIT_NOT_MARKETABLE. Perps (dollar-priced) reject LIMIT_UNSUPPORTED. BUYs are gated by the same eligibility rules the Refract app enforces (book depth/spread floors, market open, price bands, share caps, position caps). SELLs of held positions always pass eligibility. Funded accounts share a firm-wide shadow depth pool, so displayed depth already consumed by another funded trader is not fillable — quote first: POST /v1/quote runs the identical execution path as a dry run, so its fill/fees/slippage match what the same order would do at that instant. A book update racing your order is re-priced server-side within the request (bounded), so BOOK_STALE is rare; pair a retry with limitPriceProb to bound the price you can be re-priced to. Reject `reason` codes (stable): NO_LIQUIDITY, MARKET_NOT_OPEN, WINDOW_CLOSED, WINDOW_NOT_STARTED, RESOLVED, NOT_ELIGIBLE, INSUFFICIENT_FUNDS, SHARE_CAP, MAX_POSITIONS, PRICE_BAND, MARKET_DIRECTION_LOCKED, PAUSED, PAID_DEPTH_UNAVAILABLE, PAID_DEPTH_CONSUMED, BOOK_STALE, IDEMPOTENCY_CONFLICT, OVERSELL, SIZE_TOO_SMALL, BELOW_MIN, LIMIT_NOT_MARKETABLE, LIMIT_UNSUPPORTED, FUNDED_ENDED, ACCOUNT_CLOSED, SEASON_ENDED, NO_POSITION. WINDOW_NOT_STARTED = you addressed a rolling Up/Down window (constructed name or /v1/series `next` entry) before its span began; retry at the boundary. BELOW_MIN = the order is under the venue's minOrderSize (see /v1/markets/{market}); Polymarket entries are typically 5 shares minimum. SELLs of an existing position may go below the entry minimum to allow full exits. Some rejects attach `limit` — structured context (rule, capShares, currentShares, maxOpenPositions, bandLowProb/bandHighProb, bestPriceProb) for exact client copy. ## Endpoints ### GET /v1/markets Query: q (free-text search, relevance-ranked), venue (ALL|KALSHI|POLYMARKET), category, page (default 1), limit (max 100, default 50). Row: { id, venue, venueMarketId, title, eventTitle, outcomeLabel, outcomeNames, category, status, yesPrice, volume24hUsd, totalVolumeUsd, closeTime, tradeable }. `tradeable:false` means BUYs would currently reject; do not trade rows you have not re-checked via /v1/markets/{market} or /v1/quote. ### GET /v1/markets/{market} Detail + per-outcome top-of-book: outcomes[] = { outcome: YES|NO, label, buyable, bestBid, bestAsk, mid, bidDepthShares, askDepthShares, lastTradePrice }, plus feeModel, minOrderSize, tickSize. `buyable` is per side — on Polymarket the YES and NO books are independent; one side can be buyable alone. No ladder depth is exposed anywhere. ### POST /v1/quote (scope: read) Body: { market, outcome: "YES"|"NO", side: "BUY"|"SELL", sizeShares? | notionalMicros?, limitPriceProb? }. Size with EITHER sizeShares (contracts) OR notionalMicros (micro-USD to spend/receive). limitPriceProb (binary only, in (0,1)): IOC price cap — see the trading model above. Returns: { status: FILLED|PARTIAL|REJECTED, reason?, filledShares, vwap, costMicros, feeMicros, totalMicros, slippageBps, limit? }. Dry run: nothing is booked or reserved. ### POST /v1/orders (scope: trade) Same body + REQUIRED clientOrderId (1-100 chars, unique per order intent). Idempotent: resending the same clientOrderId returns the original result and never double-fills — safe to retry on timeouts. Returns the same fill shape as /v1/quote plus clientOrderId. PARTIAL means the book absorbed only part of the size; the rest was cancelled (FAK). To close a position, SELL the same outcome you hold. ### GET /v1/account { accountId, accountType, initialBalanceMicros, balanceMicros (cash), equityMicros, unrealizedPnlMicros, pnlMicros, realizedPnlMicros, perpPnlMicros, buyingPowerMicros, markComplete, markedAt, openPositions }. equityMicros = cash + instant-liquidation value of every open position (full-quantity book walk, venue fees netted, unfillable remainder worth zero). This is the number the drawdown rule is enforced against. markComplete:false = an open position could not be valued right now; equity is a PARTIAL sum and enforcement is suspended until it completes. ### GET /v1/risk The funded account's drawdown state — poll this before sizing up: { status: active|frozen|closed, tier, nominalMicros, drawdownMicros, equityPnlMicros, equityComplete, peakEquityMicros, floorMicros, headroomMicros, safetyBufferMicros, withdrawableMicros, closeOutAvailable, closeOutMicros, bookFlat, openSharesTotal, perpMarginUsedMicros, startedAt, endedAt, endReason, breach }. Rule: floor = max(0, peakEquityPnl) − drawdown. Equity P&L strictly below floor closes the account. peak ratchets on unrealized highs and is never rebased — a withdrawal consumes headroom. headroomMicros is your distance to the floor. withdrawableMicros is profit payable now (0 unless bookFlat and status active; the safety buffer stays in the account). status closed + breach ⇒ the seat ended; orders reject FUNDED_ENDED. ### GET /v1/positions { positions: [{ marketId (pass back as `market` in quote/orders), venue, venueMarketId, title, pick, outcome, qty, avgPrice, mark, markState, markStale, uPnlMicros, feesPaidMicros, awaitingSettlement, resolutionStatus, instrument, direction, leverage }] }. awaitingSettlement:true = the market ended/resolved; the position is no longer tradeable and pays out automatically shortly (venue finalization can take up to ~1 hour). markState explains valuation: live, pinned, determined, resolved, closed, stale, expired, incomplete. ### GET /v1/fills Query: limit (max 500, default 100). Most recent first. { fills: [{ ts, kind: TRADE|SETTLEMENT|FUNDING|BONUS, marketId, venue, title, pick, outcome, side, filledShares, vwap, costMicros, feeMicros, cashDeltaMicros, realizedDeltaMicros, balanceAfterMicros, settlementResult, instrument, direction, leverage }] }. ### GET /v1/series Rolling Up/Down series with the roll schedule. Per series: { key ("venue:asset:timeframe"), venue, asset, assetName, timeframe (5m|15m|1h|4h|1d), current, next[] } where each window is { marketId, venueMarketId, slug, closeTime (ISO), yesPrice (current only), volumeUsd, listed }. current is the window whose span contains now; it rolls exactly at closeTime. listed:false marks a SYNTHESIZED upcoming Kalshi window (the venue lists only the in-flight one): its ticker and closeTime are exact - the datecode is the window's close in Eastern Time, e.g. KXBTC15M-26AUG111315-15 closes 13:15 ET. Discovery probes the constructed successor name directly, so the next window typically resolves (and rejects WINDOW_NOT_STARTED) up to ~90s before its span begins and becomes tradeable within seconds of the boundary; a 404 means the venue has not created it yet — retry. Kalshi series entries also persist through the venue's brief listing gap at each roll (synthesized current, listed:false). ## Perpetuals (venue KALSHI_PERP) Dollar-priced perpetual futures on a single book; they never expire. List via GET /v1/markets?venue=PERP; address via kalshi_perp:. Prices are DOLLARS per contract (not probabilities); fee-free; margin posted = notional / leverage. Trade through the same /v1/quote and /v1/orders with two extra fields: - direction: "LONG" | "SHORT" (default LONG). A SHORT opens on SELL and closes on BUY. - leverage: on OPEN only; clamped to the market's maxLeverage; adds inherit the position's leverage (a mismatched add rejects LEVERAGE_CHANGED). Perp rejects add: LEVERAGE_CHANGED, OPPOSING_POSITION (flat-before-flip), MAX_MARGIN, NO_POSITION. Positions carry instrument "PERP", direction, leverage, and a dollar mark; liquidation wipes posted margin at entry*(1 -/+ 1/leverage). ## Combos (multi-leg parlays) Real venue RFQs: quotes take seconds and can return NO_QUOTE when no maker answers. All legs one venue; each contract pays $1 if EVERY leg wins. 1. POST /v1/combo/quote { venue: KALSHI|POLYMARKET, legs: [{market, side}] (2-10), contracts } -> { status: QUOTED|NO_QUOTE|REJECTED, quoteId, comboRef, combinedPrice, costMicros, feeMicros, payoutIfWinMicros, ttlMs }. Accept within ttlMs (~10s). 2. POST /v1/combo/orders { venue, quoteId, comboRef, contracts, combinedPrice, feeMicros?, legs } -> { combo }. quoteId is the idempotency key: resending returns the same combo. 409 QUOTE_MOVED = price/fee worsened beyond what you accepted; 409 QUOTE_EXPIRED = re-quote. 3. Cash out: POST /v1/combo/cashout/quote { comboId, contracts } -> sell-side quote, then POST /v1/combo/cashout { comboId, quoteId, contracts, sellPrice } -> { combo } (a 1 cent slippage band protects the confirm; one cash-out per accepted quote). 4. GET /v1/combos lists open + settled combos with live marks. Combo errors arrive as the envelope with SCREAMING_SNAKE codes of the app's own reasons (SHARE_CAP, PRICE_BAND, MAX_POSITIONS, INSUFFICIENT_FUNDS, FUNDED_ENDED, PAUSED, QUOTE_EXPIRED, QUOTE_MOVED, COMBO_NOT_FOUND, MARKET_DIRECTION_LOCKED...). Kalshi allows one open RFQ per combo market venue-wide, so identical concurrent quotes can collide into NO_QUOTE; retry after a beat. Cash-outs count against the order rate bucket. ## Recommended agent loop 1. GET /v1/risk → confirm status active, read headroomMicros. 2. GET /v1/markets?q=... → find a market; GET /v1/markets/{id} → check buyable + top-of-book. 3. POST /v1/quote → verify fill size, vwap, fees, slippage. 4. POST /v1/orders with a fresh clientOrderId (a UUID works). Retry the SAME id on timeout. 5. GET /v1/positions / GET /v1/account → track marks and equity. 6. Exit: POST /v1/quote then /v1/orders with side SELL, or hold to settlement.