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": "0xb09b789f98ad9cc9e2741a8ce8e15144fbbbe884",
  "market_factory_address": "0x94f86abd0117610414377935afb29f266459f9bf",
  "usdc_address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
  "ctf_address": "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045",
  "fee_sweeper_address": "0xab3f9b8f4c7c278ead851182aca57bfdd1228829",
  "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:

HeaderValue
Deprecationtrue (RFC 9745)
SunsetWed, 20 Aug 2026 00:00:00 GMT (RFC 8594)
Link</v1/platform/config>; rel="successor-version" (RFC 8288)
Warning299 - "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_idchain_id
exchangeexchange_address
market_factorymarket_factory_address
ctfctf_address
collateralusdc_address
outcome_slot_countoutcome_slot_count
default_fee_bpsplatform_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:

  1. The new contract is deployed and funded.
  2. GET /v1/platform/config flips to the new address. The order verifier on the API switches to the new EIP-712 domain in the same deploy.
  3. From the cutover instant, orders signed against the freshly-fetched address are accepted; orders signed against the old address are rejected as INVALID_SIGNATURE.
  4. 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

ParameterTypeDescription
statusstringFilter by status (active, settled, pending, …)
categorystringFilter by top-level category (e.g. Sports, Crypto, Politics)
deploy_statusstringFilter by on-chain deploy state (deployed, not_deployed, …)
searchstringFull-text search over title + description
sortstringcreated_at (default), updated_at, title, trading_starts_at, trading_ends_at, status, total_volume. Unknown values fall back to created_at.
orderstringdesc (default) or asc
limitintMax results (default 20, max 100)
offsetintPagination offset (ignored when cursor is supplied)
cursorstringKeyset 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.

FieldTypeDescription
iduuidInternal market identifier.
event_iduuid (optional)Groups related markets (e.g., the three soccer outcome markets for one fixture).
title, subtitlestringDisplay strings.
descriptionstringLong-form description / resolution criteria.
statusstringactive, pending, resolved, settled, cancelled.
resolution_typestringAlways binary today.
outcomesstring[]Outcome labels - index 0 is YES, index 1 is NO.
outcome_probabilitiesint[] (optional)Probabilities as 0-100 integers. [16, 84] means 16% YES, 84% NO. Convert to American odds client-side.
previous_outcome_probabilitiesint[] (optional)Last-known probabilities before the most recent sync - use to draw price-change indicators.
categorystringTop-level category. Title-cased (e.g. Sports, Crypto, Politics). Derived from upstream series.category for Kalshi.
subcategoriesstring[] (optional)Provider-supplied taxonomy tags. Example: ["Hockey"], ["US Elections", "Governor"]. Empty/omitted when the upstream lacks them.
external_sourcestringkalshi, polymarket, 4rho.
external_event_idstring (optional)Upstream identifier (e.g. Kalshi market ticker KXMLBHR-26APR261920LAAKC).
external_market_typestring (optional)Provider-side market kind (player_prop, moneyline, spread, total, other).
series_tickerstring (optional)Kalshi series ticker. Stable across reschedulings.
player_name, player_team_abbrev, stat_category, period, linestring / string (optional)Sports-prop attributes - present only on player-prop markets.
trading_starts_atISO 8601 timestampUTC. Orders rejected before this.
trading_ends_atISO 8601 timestampUTC. Order book closes at this time.
resolved_at, settled_atISO 8601 timestamp (optional)UTC. Set when the market resolves on-chain / payouts settle.
resolution_sourcestring (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_interestint (optional)Open interest in whole contracts (currently-open positions on the upstream provider). Liquidity signal independent of cumulative trade volume.
volume_24hint (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_volumestringCumulative lifetime volume on 4rho (decimal string, USDC).
fee_bpsintTaker fee in basis points (50 = 0.5%).
deploy_statusstringOn-chain deploy state: not_deployed, pending_deploy, deploying, deployed, failed, permanently_failed.
contract_address, condition_id, yes_token_id, no_token_id, question_idstring (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_iduuid (optional)Per-game canonical-market hint for mirror-moneyline sibling pairs. Present on BOTH siblings of a 2-team moneyline pair; the canonical market's hint equals its own id. See Mirror moneylines below. Absent everywhere else.

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_id equals its own id. 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 the market is not part of a mirror pair (single markets, 3-way soccer Home/Draw/Away) - quote it directly.

The designation is static and deterministic (lexicographic sub-ticker tiebreak, never liquidity-ranked), so it will not flip under a standing quote as depth moves between the books.

The hint is computed fail-open: a transient backend degradation omits it rather than erroring, and list responses are briefly cached (60s), so absence on a list page may lag. GET /v1/markets/:id is the authoritative read for a specific market's hint.

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"
}

Deploy Market (Bring Your Own Gas)

If your API key holds the deploy:markets scope and your wallet is a MarketFactory admin on-chain, you can deploy a market yourself and pay your own gas. Three steps:

1. Read the factory address from GET /v1/platform/configmarket_factory_address (and chain_id, ctf_address).

2. Get the createMarket arguments:

GET /v1/markets/:id/deploy-params

Scope: deploy:markets. Returns the exact arguments for the on-chain call (returns 400 if the market is already deployed):

{
  "market_id": "market-id-1",
  "title": "...",
  "question_id": "0x750d744c...",
  "outcome_slot_count": 2,
  "end_time": 1783864800,
  "fee_bps": 50
}

From your own wallet, submit the on-chain call to market_factory_address:

createMarket(bytes32 question_id, uint256 outcome_slot_count, uint256 end_time, uint256 fee_bps)

You sign and pay gas. (Note: V5+ caps end_time to roughly block.timestamp + MAX_END_TIME_HORIZON.)

3. Register the deployment so the backend records it:

POST /v1/markets/:id/register-deployment

Scope: deploy:markets. Body:

{ "tx_hash": "0x..." }

The backend fetches your transaction receipt, rejects reverts, parses the MarketCreated event, verifies questionId == keccak256(marketID), and on success records deploy_status=deployed, condition_id, yes_token_id, no_token_id, and contract_address. If the tx is not yet mined you get 202 TX_PENDING - retry after ~15-30s.

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 (+ token IDs) and an instruction to call ExchangeV2.splitFromEscrow(condition_id, amount) from your own wallet. Then place SELL orders on each outcome.

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

ParameterTypeDescription
statusstringFilter by status
categorystringFilter by category
searchstringFull-text search
limitintMax results (default 20, max 100)
offsetintPagination 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, volume_24h, open_interest, 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

ParameterTypeDescription
statusstringFilter by status. Default (and active) means events in upcoming/live with at least one active/locked/resolving market.
categorystringFilter by top-level category
sport_leaguestringFilter by league code (MLB, NFL, EPL, …)
searchstringFull-text search over event + market + team names
limitintMax results per page (default 50, max 200)
offsetintPagination offset

Response

Same envelope as /v1/events ({events, total, limit, offset}). Each event adds these aggregate fields:

FieldTypeDescription
market_count_totalintMarkets attached to the event, excluding canonical siblings and novelty.
market_count_tradeableintSubset in a tradeable status (active/locked/resolving).
market_count_deployedintSubset that is deployed on-chain.
market_type_countintDistinct normalised market types (moneyline, spread, over_under, …).
market_groupsobject[]Every returned market, grouped by normalised type. Not truncated - the sum of group lengths equals the event's eligible market count.
featured_marketMarketResponseHeadline market picked for card thumbnails.
count_is_estimatebool (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_namestring (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.

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:

EndpointSet returnedUse when
GET /v1/events/:slugWidest: everything except canonical siblings, including cancelled / draft.You want the complete set (market-making bots, reconciliation).
GET /v1/events/:slug/viewDrops cancelled / draft. markets[] keeps novelty, market_groups[] drops it.You want tradeable-ish markets plus display metadata.
GET /v1/events/browseNarrowest: 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/relatedSiblings 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).