Platform
Utility endpoints for server time, health checks, platform configuration, and browsing markets and events. No authentication required unless noted.
Server Time
GET /v1/time
Returns the server's current Unix timestamp. Use this to synchronize your clock for HMAC signature generation.
Response
{
"time": 1709136000
}
Health Check
GET /health
Returns the platform health status. Note: this endpoint is at the root path, not under /v1.
Response
{
"status": "ok"
}
Readiness Check
GET /ready
Returns readiness status including database and cache connectivity. Returns 503 if the database or cache is unreachable.
Response
{
"status": "ready"
}
Polygon Health
GET /health/polygon
Returns Polygon chain connectivity status.
Response
{
"status": "connected"
}
Platform Config
GET /v1/platform/config
Returns platform configuration including contract addresses and chain ID. This is the source of truth for every contract address your client needs to sign or settle orders. Cache the response per trading session and re-fetch on every deploy of your client; the API always returns the addresses currently in force.
Response
{
"chain_id": 137,
"exchange_address": "0x7dcacee62615f0f53dc7b82e1e2fc83cd92ad7e7",
"market_factory_address": "0x15f357a43a43a0061e456eec38a143f00ddb4b4d",
"usdc_address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"ctf_address": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045",
"fee_sweeper_address": "0x79efd356ebf401d3160dc4fbfa878ed0432f0662",
"outcome_slot_count": 2
}
fee_sweeper_address reflects the cutover-resolved FeeSweeper binding - the V9-bound FeeSweeper post-V9-cutover, the V7.1-bound FeeSweeper on the prior V7.1 generation, FeeSweeper-1.2.0 on V6, FeeSweeper-1.1.0 on V5, and the zero address when no FeeSweeper is configured. SDKs that read fees off-chain should consult this rather than hard-coding the FeeSweeper address.
outcome_slot_count is constant - all 4rho markets are binary (2-outcome). Surfaced here so SDK consumers migrating off the deprecated /v1/chain/config (sunset 2026-08-20) see the same field on the canonical successor.
Deprecated: /v1/chain/config
⚠️ Deprecated 2026-05-23. Sunset 2026-08-20. Use
/v1/platform/config(above) instead.
GET /v1/chain/config predates this endpoint. It returns the same cutover-aware contract addresses but under a different, narrower top-level shape and lacks the pause flags + fee-sweeper + volume metrics. As of 2026-05-23 every response carries the following standards-compliant deprecation headers - any HTTP client that respects them (curl -v, axios + interceptor, every modern monitoring agent) will surface the migration requirement automatically:
| Header | Value |
|---|---|
Deprecation | true (RFC 9745) |
Sunset | Wed, 20 Aug 2026 00:00:00 GMT (RFC 8594) |
Link | </v1/platform/config>; rel="successor-version" (RFC 8288) |
Warning | 299 - "Deprecated API. Use /v1/platform/config. Sunset 2026-08-20." (RFC 7234 §5.5) |
Key-name mapping for the migration:
/v1/chain/config (deprecated) | /v1/platform/config (canonical) |
|---|---|
chain_id | chain_id |
exchange | exchange_address |
market_factory | market_factory_address |
ctf | ctf_address |
collateral | usdc_address |
outcome_slot_count | outcome_slot_count |
default_fee_bps | platform_fee_bps |
| - | fee_sweeper_address |
| - | paused, market_factory_paused |
| - | total_markets, total_volume, total_fees_collected |
After 2026-08-20 the deprecated endpoint will be removed and clients still calling it will receive a 404. Migrate now - the canonical endpoint returns the same addresses today.
The example above shows the live mainnet addresses as of 2026-07-21 (V9 cutover, replacing the ExchangeV7.1 set live 2026-07-12 → 2026-07-21). These are not constants - they rotate whenever the operator deploys a new Exchange or MarketFactory. Hard-coding any of them in client code means orders silently break at the next cutover with INVALID_SIGNATURE (the EIP-712 typehash includes verifyingContract).
Migration policy
When a contract address changes, the rollout is atomic from the API client's perspective:
- The new contract is deployed and funded.
GET /v1/platform/configflips to the new address. The order verifier on the API switches to the new EIP-712 domain in the same deploy.- From the cutover instant, orders signed against the freshly-fetched address are accepted; orders signed against the old address are rejected as
INVALID_SIGNATURE. - The previous contract remains read-only for settlement of orders signed before the cutover. There is no quiet period and no overlap window.
If you receive INVALID_SIGNATURE on a freshly-signed order, refresh /v1/platform/config - that's the load-bearing signal that your cached address is stale.
Sports Taxonomy
GET /v1/sports/taxonomy
Returns the sports taxonomy tree (leagues, teams, categories).
Response
{
"sports": [
{
"name": "Basketball",
"leagues": [
{
"name": "NBA",
"teams": ["Lakers", "Celtics"]
}
]
}
]
}
On-Chain Probabilities
GET /v1/onchain/market-probabilities
Returns current market probabilities derived from on-chain order book data.
Response
{
"probabilities": {
"market-id-1": [0.52, 0.48],
"market-id-2": [0.70, 0.30]
}
}
List Markets
GET /v1/markets
Returns markets on the platform. All markets are binary (2 outcomes); soccer 3-way markets are modeled as three separate binaries grouped by event_id.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (active, settled, pending, …) |
category | string | Filter by top-level category (e.g. Sports, Crypto, Politics) |
deploy_status | string | Filter by on-chain deploy state (deployed, not_deployed, …) |
search | string | Full-text search over title + description |
sort | string | created_at (default), updated_at, title, trading_starts_at, trading_ends_at, status, total_volume. Unknown values fall back to created_at. |
order | string | desc (default) or asc |
limit | int | Max results (default 20, max 100) |
offset | int | Pagination offset (ignored when cursor is supplied) |
cursor | string | Keyset cursor from a previous response's next_cursor. Honoured only on the default sort=created_at&order=desc; ignored otherwise. Prefer it over offset for deep pagination. |
There is no event_id filter - this endpoint cannot enumerate the markets
of a single event. See
Enumerating every market in an event.
Ordering is a total order (<sort> <order>, id <order>), so paging with
limit/offset cannot skip or duplicate a market inside a tie group. That
matters: nearly every market shares total_volume = 0, so sort=total_volume
would otherwise be non-deterministic between pages.
Response
{
"markets": [
{
"id": "475420a3-e775-4b70-9ad3-05c4fe5f8aa4",
"event_id": "62b29bda-97c3-426b-be05-52cbeb973acb",
"title": "Bobby Witt Jr.: 1+ home runs?",
"subtitle": "Bobby Witt Jr.: 1+",
"description": "If Bobby Witt Jr. records 1+ home runs in Los Angeles A vs Kansas City professional baseball game…",
"status": "active",
"resolution_type": "binary",
"outcomes": ["Bobby Witt Jr.: 1+", "No"],
"outcome_probabilities": [16, 84],
"previous_outcome_probabilities": [18, 82],
"category": "Sports",
"subcategories": ["Baseball"],
"external_source": "kalshi",
"external_event_id": "KXMLBHR-26APR261920LAAKC",
"external_market_type": "player_prop",
"series_ticker": "KXMLBHR",
"player_name": "Bobby Witt Jr.",
"player_team_abbrev": "KCB",
"stat_category": "home_runs",
"period": "full_game",
"line": 1,
"trading_starts_at": "2026-04-26T13:29:19.037144Z",
"trading_ends_at": "2026-04-29T23:20:00Z",
"resolution_source": "https://kalshi.com/markets/kxmlbhr/kxmlbhr/kxmlbhr-26apr261920laakc",
"settlement_sources": [
{ "name": "MLB", "url": "https://www.mlb.com" }
],
"open_interest": 4231,
"volume_24h": 18920,
"total_volume": "0",
"fee_bps": 50,
"deploy_status": "not_deployed",
"current_prices": { "0": 0.16, "1": 0.84 },
"created_at": "2026-04-26T13:29:19.037150Z",
"updated_at": "2026-04-26T13:29:19.037150Z"
}
],
"total": 1906624,
"limit": 20,
"offset": 0
}
MarketResponse fields
Pricing, taxonomy, and provenance fields surfaced on every market. Fields marked
optional are omitted (not null) when absent.
| Field | Type | Description |
|---|---|---|
id | uuid | Internal market identifier. |
event_id | uuid (optional) | Groups related markets (e.g., the three soccer outcome markets for one fixture). |
title, subtitle | string | Display strings. |
description | string | Long-form description / resolution criteria. |
status | string | active, pending, resolved, settled, cancelled. |
resolution_type | string | Always binary today. |
outcomes | string[] | Outcome labels - index 0 is YES, index 1 is NO. |
outcome_probabilities | int[] (optional) | Probabilities as 0-100 integers. [16, 84] means 16% YES, 84% NO. Convert to American odds client-side. |
previous_outcome_probabilities | int[] (optional) | Last-known probabilities before the most recent sync - use to draw price-change indicators. |
category | string | Top-level category. Title-cased (e.g. Sports, Crypto, Politics). Derived from upstream series.category for Kalshi. |
subcategories | string[] (optional) | Provider-supplied taxonomy tags. Example: ["Hockey"], ["US Elections", "Governor"]. Empty/omitted when the upstream lacks them. |
external_source | string | kalshi, polymarket, 4rho. |
external_event_id | string (optional) | Upstream identifier (e.g. Kalshi market ticker KXMLBHR-26APR261920LAAKC). |
external_market_type | string (optional) | Provider-side market kind (player_prop, moneyline, spread, total, other). |
series_ticker | string (optional) | Kalshi series ticker. Stable across reschedulings. |
player_name, player_team_abbrev, stat_category, period | string (optional) | Sports-prop attributes - present only on player-prop markets. |
line | number (optional) | Stored spread, total, prop threshold, or strike. Omitted when the market has no line. |
trading_starts_at | ISO 8601 timestamp | UTC. Orders rejected before this. |
trading_ends_at | ISO 8601 timestamp | UTC. Order book closes at this time. |
resolved_at, settled_at | ISO 8601 timestamp (optional) | UTC. Set when the market resolves on-chain / payouts settle. |
resolution_source | string (optional) | URL to the canonical upstream market (e.g. the original Kalshi page). |
settlement_sources | {name, url}[] (optional) | Provider-cited resolution references - the "resolves via X" trust signal. Populated when the upstream exposes them (Kalshi sports series do; weather and binary-prediction series typically don't). |
open_interest | int (optional) | Open interest in whole contracts (currently-open positions on the upstream provider). Liquidity signal independent of cumulative trade volume. |
volume_24h | int (optional) | Trailing-24h trade volume on the upstream provider in whole units (whole contracts / whole USD), normalized identically across Kalshi and Polymarket. Distinct from cumulative total_volume. |
total_volume | string | Cumulative lifetime volume on 4rho (decimal string, USDC). |
fee_bps | int | Taker fee in basis points (50 = 0.5%). |
deploy_status | string | On-chain deploy state: not_deployed, pending_deploy, deploying, deployed, failed, permanently_failed. |
contract_address, condition_id, yes_token_id, no_token_id, question_id | string (optional) | Polygon / Gnosis CTF identifiers. contract_address is the MarketFactory address this market was deployed through - NOT the Exchange/verifyingContract used for order signing. For the signing address, fetch GET /v1/platform/config's exchange_address (above) - see EIP-712 Order Signing. Populated once deploy_status='deployed'. |
current_prices | { "0": number, "1": number } | Per-outcome mid prices (0-1). {0: 0.5, 1: 0.5} is the default 50/50 fallback when no on-chain liquidity exists yet. |
canonical_sibling_market_id | uuid (optional) | Advisory designation for an eligible mirror-moneyline pair. When computed successfully, BOTH siblings point to the canonical market (whose hint equals its own id). Omission does not establish that a market is standalone. See Mirror moneylines. |
Wire change (2026-08-28, #10189): line on list entries and single-market
detail changed from a quoted decimal (for example, "line":"-3.5") to a JSON
number ("line":-3.5). The unit did not change. Clients should accept both
forms during rolling deploys and rollbacks: list payloads have a 60-second Redis
TTL and also advertise downstream HTTP stale windows. New clients should parse
it as a number and not depend on the numeral's raw text (very small values may
use exponent notation). This note applies to the MarketResponse documented
here, not to the separate metadata, bare-event, or admin response contracts;
GET /v1/markets/:id/related was already numeric and is unchanged.
Mirror moneylines - quote one sibling
Every 2-team game sourced from Kalshi ships as TWO sibling moneyline markets (one per sub-ticker, e.g. KXMLBGAME-...-MIL and KXMLBGAME-...-CIN) with mirror-image outcome ordering and separate on-chain CTF conditions. The two order books can never cross: Team-A-YES on sibling A and Team-B-YES on sibling B are different conditions, so a two-sided quote split across the pair fragments into "no match".
If you are making markets, use canonical_sibling_market_id:
- Quote both sides of the market whose
canonical_sibling_market_idequals its ownid. Opposite-side flow on that single book matches normally. - Do not quote the sibling whose hint points at a different market id.
- No hint means no designation was supplied, not proof that the market is standalone. Single markets and 3-way soccer Home/Draw/Away carry no hint, but so do ineligible or ambiguous shapes and responses whose annotation lookup failed. Do not infer quote eligibility or a shared settlement condition from omission.
For an unchanged eligible pair, the designation is deterministic (lexicographic sub-ticker tiebreak, market UUID fallback), never liquidity-ranked. Depth changes alone do not switch it. A change to the sibling set, identifiers or outcome shape can change or remove the designation; it is not a permanent identity assignment.
The annotation is best-effort: a failed lookup omits the hint without failing the request. List responses are briefly cached (60s), so list and detail can reflect different snapshots. GET /v1/markets/:id runs the annotation lookup for that request, including on detail-cache hits, but uses the same omission-on-error behavior. Retrying detail can refresh a stale list hint; a missing detail hint still does not distinguish an ineligible pair from an unavailable lookup. This advisory field neither merges markets nor replaces the market's own contract, outcome and order-admission checks.
settlement_sources example
NHL game-line market (Kalshi KXNHL series):
{
"category": "Sports",
"subcategories": ["Hockey"],
"settlement_sources": [
{ "name": "NHL", "url": "https://www.nhl.com" }
]
}
Hourly crypto market with no settlement sources exposed:
{
"category": "Crypto",
"subcategories": ["Hourly", "BTC"],
"settlement_sources": null
}
When settlement_sources is empty/absent, fall back to resolution_source
(the upstream market URL) for provenance.
Get Market
GET /v1/markets/:id
Returns details for a single market. Response shape matches each entry in GET /v1/markets (see MarketResponse fields).
Market Metadata
GET /v1/markets/metadata
Returns market metadata including categories and counts.
Response
{
"categories": ["Sports", "Politics", "Crypto", "Soccer"],
"total_markets": 150,
"active_markets": 42
}
Categories are derived from upstream provider taxonomy
(series.category + series.tags for Kalshi) - they're not a hardcoded
list. New categories appear as upstream coverage expands.
Price History
GET /v1/markets/:id/history
Returns price history time series for a market.
Response
{
"market_id": "market-id-1",
"history": [
{"timestamp": "2024-01-15T00:00:00Z", "price": 0.50},
{"timestamp": "2024-01-16T00:00:00Z", "price": 0.52}
]
}
Market Trades
GET /v1/markets/:id/trades
Returns recent trades for a specific market.
Response
{
"trades": [
{
"id": "trade-id-1",
"price": "0.52",
"amount": "1000000",
"side": "BUY",
"timestamp": "2024-01-15T12:00:00Z"
}
]
}
Deploy Market
POST /v1/markets/:id/deploy
Scope: trade:orders
Request on-chain deployment for a market. Triggers operator gas spend (4rho's wallet signs and pays). For self-service deploys where you pay gas, use the bring-your-own-gas flow below instead.
Response
{
"status": "deploying",
"market_id": "market-id-1"
}
Minting outcome tokens (optional)
To provide two-sided liquidity you typically split USDC into YES + NO tokens first:
GET /v1/markets/:id/split-params
Scope: trade:orders. Returns the condition_id, nullable token IDs and instructions for splitFromEscrow(condition_id, amount).
Read exchange_address and chain_id from Platform Config and target that Exchange on that chain — not the market's contract_address (MarketFactory) or a hardcoded prior Exchange. Call from the wallet that owns the escrow, with enough unheld USDC escrow at that Exchange; amount is raw USDC (6 decimals). Simulate the call and confirm its transaction receipt before relying on the resulting equal YES + NO token escrow balances.
New ExchangeV10 funding requires a deposit authorization; its legacy permissionless deposit(), depositWithPermit() and depositTokens() selectors revert. If authorization issuance is unavailable, do not attempt new funding or deposit to a predecessor as a fallback. Splitting existing escrow is not new funding, and this metadata endpoint does not authorize either funding or order placement. Submit SELL orders only when current order-admission requirements are satisfied.
Outcome Tokens
Outcome tokens are ERC-1155 positions minted by the Gnosis CTF (ctf_address)
when a market is deployed. They are not exposed via a 4rho REST endpoint -
read them directly from chain. Each deployed market exposes its
yes_token_id and no_token_id on its MarketResponse,
which you pass to CTF.balanceOf(holder, tokenId) to query a position.
List Events
GET /v1/events
Returns all events on the platform. Events group related markets together.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status |
category | string | Filter by category |
search | string | Full-text search |
limit | int | Max results (default 20, max 100) |
offset | int | Pagination offset |
Response
{
"events": [
{
"id": "62b29bda-97c3-426b-be05-52cbeb973acb",
"slug": "us-election-2024",
"title": "2024 US Presidential Election",
"description": "…",
"status": "active",
"category": "Politics",
"sport_league": null,
"sport_teams": null,
"image_url": null,
"start_time": "2026-11-03T00:00:00Z",
"external_event_id": "KXPRES-2024",
"external_platform": "kalshi",
"external_event_subtitle": null,
"external_event_mutually_exclusive": true,
"market_count": 5,
"created_at": "2024-01-10T00:00:00Z",
"updated_at": "2026-05-23T00:00:00Z"
}
],
"total": 1234,
"limit": 20,
"offset": 0
}
external_event_mutually_exclusive is the upstream-asserted multi-market
exclusivity flag - used to drive carveouts in odds rendering on the client
(see the RFQ rules in Market Data). sport_teams
is populated only for sports events.
Browse Events
GET /v1/events/browse
The primary endpoint for the event listing page - adds per-event market
aggregates (market_count_tradeable,
featured_market, grouped market_groups) on top of the List Events
shape. Use this when rendering an events grid; use plain /v1/events when
you only need event metadata.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status. Default (and active) means events in upcoming/live with at least one active/locked/resolving market. |
category | string | Filter by top-level category |
sport_league | string | Filter by league code (MLB, NFL, EPL, …) |
search | string | Full-text search over event + market + team names |
limit | int | Max results per page (default 50, max 200) |
offset | int | Pagination offset |
home | 1 | Home discovery ranking and core tile market families; drops team totals and provider join fields from kept market previews. |
core_lines | 1 | The same core market-preview trim without home ranking. |
quotes | 1 | Add the page's live quote/indication fields; five-second quote cache boundary. |
quotes_only | 1 | With quotes=1, return only the quote fields plus the canonical ids manifest; without quotes=1, no effect. |
Response
Same envelope as /v1/events ({events, total, limit, offset}). Each event
adds these aggregate fields:
| Field | Type | Description |
|---|---|---|
market_count_total | int | Markets attached to the event, excluding canonical siblings and novelty. |
market_count_tradeable | int | Subset in a tradeable status (active/locked/resolving). |
market_count_deployed | int | Subset that is deployed on-chain. |
market_type_count | int | Distinct normalised market types (moneyline, spread, over_under, …). |
market_groups | object[] | Returned markets grouped by normalised type. home=1 / core_lines=1 narrow this to core tile families; aggregate counts, featured_market and contenders retain their full scope. Browse without these flags when enumerating props/periods/futures/team totals. |
featured_market | MarketResponse | Headline market picked for card thumbnails. |
count_is_estimate | bool (optional) | When true, total is a floor, not an exact count - the exact COUNT(*) exceeded its budget. Page with the full-page heuristic (len(events) == limit ⇒ maybe one more) rather than ceil(total/limit). |
home_player_full_name, away_player_full_name | string (optional) | Set on sports events that resolve to a head-to-head matchup. |
Per-event upstream volume_24h / open_interest are not returned. They
are upstream venue numbers, not native 4rho volume; they order the result set
server-side (busiest events lead) but never reach the client.
Quotes-only polling
Keep the full quotes=1 response for initial rendering and pagination. For
steady quote updates, use quotes=1"es_only=1: the decorated response
contains only ids, desk_indications, on_chain, covered, and updated_at.
It shares the full browse cache and quote producers, not a second database
browse or quote cache.
The ordered, unique canonical UUID ids list must agree with covered.
A valid empty page uses ids: [] and empty maps. Missing/invalid manifests
or an unavailable quote provider return the full undecorated browse body;
older servers may also return the full decorated response. Accept that legacy
shape, but never interpret a malformed explicit manifest as complete coverage.
Indications keep their original expiry and do not establish tradable liquidity.
Full and compact quote responses use five-second caching; the web BFF adds no
stale-while-revalidate window for them. Ordinary catalog caching is unchanged.
Get Event
GET /v1/events/:slug
Returns details for a single event, including every market attached to it.
This is the widest of the three event surfaces and the one to use when you need
a complete enumeration: no row cap, no paging, no status filter - cancelled
and draft markets appear here and nowhere else. Canonical mirror siblings are
excluded (see Mirror moneylines). Ordered by
created_at ASC, id ASC.
Response
{
"slug": "us-election-2024",
"title": "2024 US Presidential Election",
"status": "active",
"markets": [
{
"id": "market-id-1",
"title": "Will candidate X win?",
"status": "active",
"outcomes": ["Yes", "No"]
}
],
"created_at": "2024-01-10T00:00:00Z"
}
Get Event View
GET /v1/events/:slug/view
Returns a rich event view including markets, metadata, and live scores (for sports events).
Narrower than Get Event: it drops cancelled / draft markets.
Its markets[] array includes novelty markets while the sibling
market_groups[] excludes them, so market_groups legitimately sums to
fewer markets than markets contains. No row cap.
Enumerating every market in an event
GET /v1/markets has no event_id filter - it cannot enumerate an event.
Use one of:
| Endpoint | Set returned | Use when |
|---|---|---|
GET /v1/events/:slug | Widest: everything except canonical siblings, including cancelled / draft. | You want the complete set (market-making bots, reconciliation). |
GET /v1/events/:slug/view | Drops cancelled / draft. markets[] keeps novelty, market_groups[] drops it. | You want tradeable-ish markets plus display metadata. |
GET /v1/events/browse | Narrowest: drops canonical siblings, drops all novelty, and on the default status=active path keeps only active / locked / resolving. | You are rendering a grid and want counts that match what you show. |
GET /v1/markets/:id/related | Siblings of one market, de-duplicated by question text; totalMarkets is the un-deduplicated count. | You are already holding a market id and want its neighbours. |
None of these silently truncate. GET /v1/events/:slug carried a hidden
500-row cap until 2026-07-28; it is gone, and any future bound will ship with a
total or has_more alongside it.
Paging on /v1/events/browse and /v1/markets is a total order - both
append id as the final ORDER BY key, so limit/offset paging cannot skip or
duplicate a row inside a tie group (GET /v1/markets?sort=total_volume sits on
a tie group of millions of rows at 0).