# llms.txt Source: https://docs.radion.app/ai/llms-txt Docs index and full snapshot for AI tools that read text sources. Radion hosts two text files for AI tools: a short index of the docs, and one big file with everything. | File | Content | | -------------------------------------------------------------------------------- | ------------------------------ | | [`https://docs.radion.app/llms.txt`](https://docs.radion.app/llms.txt) | A list of all the docs pages | | [`https://docs.radion.app/llms-full.txt`](https://docs.radion.app/llms-full.txt) | All pages joined into one file | Use `llms.txt` when your tool follows the [llms.txt standard](https://llmstxt.org). Use `llms-full.txt` when you need all the text in one file — for example, as a system prompt or a RAG source. # MCP Source: https://docs.radion.app/ai/mcp Connect your AI tool to the Radion MCP server. The MCP server lets your AI agent talk to Radion through tools. Connect it to Claude Code, Codex, Cursor, VS Code, or any MCP-compatible tool. Each tool maps to one Radion REST endpoint, so your agent can read on-chain Polymarket data — markets, traders, events, search, and the orderbook — without writing any HTTP code. ## Endpoint ```text theme={null} https://mcp.radion.app/mcp ``` ## Authentication Every request needs an API key. Send it in the `X-API-Key` header (or as an `Authorization: Bearer` token). If your client can't set custom headers (for example, Claude Cowork), pass the key as an `api_key` query string instead: ```text theme={null} https://mcp.radion.app/mcp?api_key=YOUR_API_KEY ``` Use the header when your client supports it. Keys in URLs can leak through proxies and client history, so rotate any key you put in a query string if it may have been seen. [Get an API key from the dashboard](https://radion.app/dashboard). ## Setup ```bash theme={null} claude mcp add --transport http "Radion" "https://mcp.radion.app/mcp" \ --header "X-API-Key: YOUR_API_KEY" ``` ```bash theme={null} export RADION_API_KEY=your_api_key codex mcp add "Radion" --url "https://mcp.radion.app/mcp" --bearer-token-env-var RADION_API_KEY ``` Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): ```json theme={null} { "mcpServers": { "Radion": { "url": "https://mcp.radion.app/mcp", "headers": { "X-API-Key": "${env:RADION_API_KEY}" } } } } ``` Set the env var in your shell profile: `export RADION_API_KEY=your_api_key` Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "Radion": { "type": "http", "url": "https://mcp.radion.app/mcp", "headers": { "X-API-Key": "${input:radionApiKey}" } } } } ``` VS Code will ask for the key the first time you use it. Claude Cowork can't set custom headers, so put the key in the URL: ```text theme={null} https://mcp.radion.app/mcp?api_key=YOUR_API_KEY ``` ## Available tools Each tool returns the same JSON as the matching REST endpoint. ### Events and search | Tool | What it does | | -------------- | ------------------------------------------------------------- | | `list_events` | List events (groups of related markets). Pages with a cursor. | | `get_event` | Get one event by its numeric id or slug. | | `list_markets` | List markets. Pages with a cursor. | | `get_market` | Get one market by its condition id or slug. | | `search` | Search Polymarket markets, topics, and profiles by text. | ### Orderbook | Tool | What it does | | ---------------------- | -------------------------------------------------------------- | | `get_orderbook` | Get the full orderbook depth (bids and asks) for a `token_id`. | | `get_midpoint` | Get the midpoint price (between best bid and ask) for a token. | | `get_spread` | Get the bid-ask spread for a token. | | `get_price` | Get the best bid or ask price for a token (`side` = buy/sell). | | `get_last_trade_price` | Get the last executed trade price for a token. | ### Health | Tool | What it does | | -------- | ----------------------------------- | | `health` | Check whether the MCP server is up. | ## Example prompts Once connected, try these in your AI tool: * *"List the top 10 Polymarket markets by volume."* * *"What is the PnL for trader 0x1234…?"* * *"Search Polymarket for markets about the election."* * *"Show the most recent trades for this market slug."* * *"Who is on the Polymarket profit leaderboard right now?"* ## Rate limits and plans Rate limits match the [REST API rate limits](/api/rate-limits). Your API key plan applies. # Use Radion with AI Source: https://docs.radion.app/ai/overview Connect Radion to your AI tools via MCP, llms.txt, and skill.md. Radion gives AI tools a few ways to connect. Pick the one your tool supports. | Surface | What it does | Auth | | ------------------------ | ---------------------------------------------------------------- | ---------------- | | [MCP](/ai/mcp) | An MCP server that gives AI agents the Radion endpoints as tools | API key required | | [llms.txt](/ai/llms-txt) | Docs index and full snapshot for tools that read text | None | | [skill.md](/ai/skill) | Tells agents what Radion can do | None | # skill.md Source: https://docs.radion.app/ai/skill Agent guide — what Radion can do, what inputs it needs, and what limits apply. `skill.md` is a guide for AI agents. `llms.txt` is just a list of pages. `skill.md` is different: it tells an agent what Radion can do, what inputs each task needs, and what limits apply. This helps agents use Radion the right way. Mintlify rebuilds it every time the docs change. ```text theme={null} https://docs.radion.app/skill.md ``` ## Install via Skills CLI Works with Claude Code, Cursor, OpenCode, and 20+ other agents. ```bash theme={null} npx skills add https://docs.radion.app ``` # Create an API key Source: https://docs.radion.app/api-reference/authentication/create-an-api-key /api/openapi.json post /auth/keys # Delete an API key Source: https://docs.radion.app/api-reference/authentication/delete-an-api-key /api/openapi.json delete /auth/keys/{keyLabel} # Get current API key info Source: https://docs.radion.app/api-reference/authentication/get-current-api-key-info /api/openapi.json get /auth/me # List the current user's API keys Source: https://docs.radion.app/api-reference/authentication/list-the-current-users-api-keys /api/openapi.json get /auth/keys # Get an event by id Source: https://docs.radion.app/api-reference/events/get-an-event-by-id /api/openapi.json get /v1/polymarket/events/{event_id} # Get an event by slug Source: https://docs.radion.app/api-reference/events/get-an-event-by-slug /api/openapi.json get /v1/polymarket/events/slug/{event_slug} # List events Source: https://docs.radion.app/api-reference/events/list-events /api/openapi.json get /v1/polymarket/events # Get health status Source: https://docs.radion.app/api-reference/health/get-health-status /api/openapi.json get /health # Get a market by condition id Source: https://docs.radion.app/api-reference/markets/get-a-market-by-condition-id /api/openapi.json get /v1/polymarket/markets/{condition_id} # Get a market by slug Source: https://docs.radion.app/api-reference/markets/get-a-market-by-slug /api/openapi.json get /v1/polymarket/markets/slug/{market_slug} # List markets Source: https://docs.radion.app/api-reference/markets/list-markets /api/openapi.json get /v1/polymarket/markets # Best bid or ask price for a position Source: https://docs.radion.app/api-reference/orderbook/best-bid-or-ask-price-for-a-position /api/openapi.json get /v1/polymarket/price # Bid-ask spread for a position Source: https://docs.radion.app/api-reference/orderbook/bid-ask-spread-for-a-position /api/openapi.json get /v1/polymarket/spread # Full orderbook depth for a position Source: https://docs.radion.app/api-reference/orderbook/full-orderbook-depth-for-a-position /api/openapi.json get /v1/polymarket/orderbook # Last executed trade price for a position Source: https://docs.radion.app/api-reference/orderbook/last-executed-trade-price-for-a-position /api/openapi.json get /v1/polymarket/last-trade-price # Midpoint price for a position Source: https://docs.radion.app/api-reference/orderbook/midpoint-price-for-a-position /api/openapi.json get /v1/polymarket/midpoint # Search markets, themes, and profiles Source: https://docs.radion.app/api-reference/search/search-markets-themes-and-profiles /api/openapi.json get /v1/polymarket/search # Authentication Source: https://docs.radion.app/api/authentication How to authenticate requests to the Radion API with API keys. You need an API key for all business requests under `/v1/*`. Keys are free. Get one at [radion.app/dashboard](https://radion.app/dashboard). You do not need a paid plan. Send your key in the `X-API-Key` header: ```bash title="Authenticated request" theme={null} curl "https://api.radion.app/v1/polymarket/events?limit=10" \ -H "X-API-Key: YOUR_API_KEY" ``` Use the key value as it is. Do not put `Bearer` in front of it. ## Key types Radion has two kinds of keys: * **Secret keys** (`sk_...`) — for your own server. They give full access to your plan. Keep them secret. * **Public JWT keys** (`pk_jwt_...`) — safe to ship in a browser or mobile app. On their own they do nothing. Each request also needs a signed user token (a JWT) from your own auth provider. ## Public JWT keys Use a public JWT key when your app has no backend and talks to Radion straight from the browser. Set the key up once in the [dashboard](https://radion.app/dashboard): * Give Radion a **JWKS URL** or paste a **public key** (PEM). This is how Radion checks your users' tokens. * Optional: set an **audience** (`aud`) and **issuer** (`iss`) to check. * Optional: set a per-user rate limit. Then send two headers on each request: the public key, and the user's JWT. ```bash title="Public JWT request" theme={null} curl "https://api.radion.app/v1/polymarket/events?limit=10" \ -H "X-API-Key: pk_jwt_YOUR_PUBLIC_KEY" \ -H "Authorization: Bearer USER_JWT" ``` Radion checks the JWT signature against your configured key, checks `exp` (and `aud`/`iss` if you set them), and reads the `sub` claim. Rate limits then apply per user (`sub`), not per key. Monthly usage still counts against your plan. ### Token rules * The JWT must use an **asymmetric** algorithm: `RS256/384/512`, `PS256/384/512`, `ES256/384`, or `EdDSA`. Symmetric algorithms (`HS256` and friends) are rejected, because the public key is meant to be public. * The JWT must have `exp` (expiry) and a non-empty `sub`. * A JWKS URL must be `https` and must not point at a private or internal address. A leaked public key is low risk. It does nothing without a valid user JWT from your provider. ## Look at the current key `GET /auth/me` tells you about the key you are using. This call is free. It does not count against your quota. ```bash title="Inspect the current API key" theme={null} curl "https://api.radion.app/auth/me" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json title="Response" theme={null} { "keyLabel": "production_read", "plan": "starter", "callCount": 42, "createdAt": "2026-04-01T12:00:00Z", "lastUsedAt": "2026-04-09T15:20:00Z", "monthlyUsage": { "used": 120, "limit": 3000, "resetsAt": "2026-07-01T00:00:00Z" }, "requestsPerSecond": null } ``` | Field | What it means | | ------------------- | ------------------------------------------------------------------------------------- | | `keyLabel` | The name you gave the key when you made it | | `plan` | The plan on the key: `free`, `starter`, `pro`, or `enterprise` | | `callCount` | How many requests this key has ever made | | `createdAt` | When the key was made | | `lastUsedAt` | When the key was last used | | `monthlyUsage` | This month's usage: `used`, `limit`, and when it `resetsAt` | | `requestsPerSecond` | Per-second cap on the free plan. `null` on paid plans, which have no per-second limit | ## What errors mean | Case | Response | | --------------------------------------- | ------------------ | | You sent no key | `401 Unauthorized` | | The key is wrong or turned off | `401 Unauthorized` | | The key is fine but the plan is too low | `403 Forbidden` | ## Good habits * Keep API keys secret, like passwords. * Use a different key for each app or environment when you can. * Make a new key if you think an old one leaked. # Errors Source: https://docs.radion.app/api/errors Error codes, response format, and handling strategies for the Radion API. When something goes wrong, the API returns a JSON error. The format follows [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) (Problem Details). The `Content-Type` is `application/problem+json`. ## Error format ```json theme={null} { "type": "about:blank", "status": 401, "title": "Unauthorized", "detail": "Invalid API key", "requestId": "1748123456-1a" } ``` | Field | Type | What it means | | ----------- | ------- | -------------------------------------------------------------- | | `type` | string | The problem type. Always `about:blank` for normal HTTP errors. | | `status` | integer | The HTTP status code. | | `title` | string | A short title that matches the status code. | | `detail` | string | What went wrong this time. | | `requestId` | string | The id for this request. Send it to us when you ask for help. | ## Status codes | Status | Meaning | When you see it | | ------ | --------------------- | -------------------------------------------------------------------------- | | `400` | Bad Request | A query parameter is wrong or out of range | | `401` | Unauthorized | The key is missing, wrong, or turned off | | `403` | Forbidden | The key is fine but the plan is too low for this endpoint | | `404` | Not Found | The market, trader, or event does not exist | | `409` | Conflict | The thing already exists, like an API key with the same label | | `422` | Unprocessable Entity | The request body is valid JSON but fails the rules | | `429` | Too Many Requests | You went over a quota. Wait for the time in `Retry-After`, then try again. | | `500` | Internal Server Error | Something broke on our side | ## Rate limit errors When you go over the per-second limit, you get this detail: ```json theme={null} { "detail": "Rate limit exceeded" } ``` When you go over the monthly limit, you get this detail: ```json theme={null} { "detail": "Monthly request limit exceeded" } ``` # REST API overview Source: https://docs.radion.app/api/overview Base URL, endpoint groups, and authentication for the Radion REST API. The Radion API is a REST API. You use it to read Polymarket on-chain data: markets, events, and the orderbook. All the data comes from Radion's own on-chain indexer. ## Base URL ```text theme={null} https://api.radion.app ``` ## Endpoint groups | Group | What it does | | ------------------ | -------------------------------------------------------- | | **Authentication** | Look at the API key you are using | | **Events** | List events and read one event by numeric id or slug | | **Markets** | List markets and read one market by condition id or slug | | **Search** | Search events, topics, and profiles by text | | **Orderbook** | Get the current midpoint price for an outcome token | | **Health** | Check that the API is up | ## Authentication You need an API key for all `/v1/*` requests. Keys are free. Sign in at [radion.app/dashboard](https://radion.app/dashboard) to make one. Send your key in the `X-API-Key` header: ```http theme={null} X-API-Key: YOUR_API_KEY ``` See [Authentication](/api/authentication) for more, and [Rate limits](/api/rate-limits) for how many requests each plan allows. # Pagination Source: https://docs.radion.app/api/pagination How cursor-based pagination works on list endpoints in the Radion API. List endpoints use cursors to move through pages. A cursor is an opaque string. You do not read it; you just send it back. Pass `cursor` and `limit` as query parameters. ## Parameters | Parameter | Type | Default | Range | What it does | | --------- | ------- | ------- | ----- | ------------------------------------------------------------------------- | | `limit` | integer | 50 | 1–200 | How many items to return per page. | | `cursor` | string | — | — | The `nextCursor` from the page before. Leave it out on the first request. | ## What a page looks like Events and markets list responses use a named array and a `nextCursor` field: ```json theme={null} { "events": [...], "nextCursor": "eyJjb21wdXRlZF9hdCI6Ij..." } ``` `nextCursor` is `null` when there are no more pages. ## Getting every page ```bash cURL theme={null} # First page curl "https://api.radion.app/v1/polymarket/events?limit=50" \ -H "X-API-Key: YOUR_API_KEY" # Next page (use nextCursor from the page before) curl "https://api.radion.app/v1/polymarket/events?limit=50&cursor=NEXT_CURSOR" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} async function* listAllEvents(apiKey: string) { let cursor: string | undefined; while (true) { const url = new URL("https://api.radion.app/v1/polymarket/events"); url.searchParams.set("limit", "50"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url.toString(), { headers: { "X-API-Key": apiKey }, }); const { events, nextCursor } = await res.json(); yield* events; if (!nextCursor) break; cursor = nextCursor; } } for await (const event of listAllEvents("YOUR_API_KEY")) { console.log(event.id, event.title, event.volume); } ``` ```python Python theme={null} import httpx def list_all_events(api_key: str): client = httpx.Client( base_url="https://api.radion.app", headers={"X-API-Key": api_key}, ) cursor = None while True: params = {"limit": 50} if cursor: params["cursor"] = cursor body = client.get("/v1/polymarket/events", params=params).json() yield from body["events"] cursor = body.get("nextCursor") if not cursor: break for event in list_all_events("YOUR_API_KEY"): print(event["id"], event["title"], event["volume"]) ``` ## Knowing when to stop Stop when `nextCursor` is `null`. Do not stop just because the array looks empty. Always check `nextCursor`. # Rate limits Source: https://docs.radion.app/api/rate-limits Per-plan request quotas for the Radion API. Paid plans have no per-second rate limit. You only have a monthly quota. The free plan also has a small per-second cap. Limits only apply to the business endpoints under `/v1/*`. The auth, key, and health endpoints are free. They do not count against your monthly quota. | Endpoint group | Per-second limit | Monthly quota | Notes | | -------------- | ---------------- | ------------- | ------------------- | | Business API | Free plan only | Yes | The `/v1/*` data | | `/auth/*` | No | No | Keys and key info | | `/health` | No | No | Public health check | Business API requests need an API key. See [Authentication](/api/authentication) to get one. ## Quotas | Plan | Requests / second | Requests / month | | ---------- | ----------------- | ---------------- | | Free | 5 | 300 | | Starter | No limit | 3,000 | | Pro | No limit | 50,000 | | Enterprise | No limit | Unlimited | ## How it works Paid plans are not throttled per second. They only have a monthly quota. On the free plan, the per-second limit uses a token bucket. You can spend your whole per-second budget at once as a burst. It then fills back up over the next second. The monthly limit is a counter for the calendar month. It goes back to zero at the start of the next month, in UTC. The monthly quota counts per account, not per key. If you have many keys on one account, they all share the same monthly quota. ## When you go over a quota If you go over a quota, you get a `429 Too Many Requests` response with a `Retry-After` header. ```http title="Response headers" theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 1 ``` ```json title="Per-second limit response" theme={null} { "type": "about:blank", "status": 429, "title": "Too Many Requests", "detail": "Rate limit exceeded", "request_id": "1748123456-1a" } ``` ```json title="Monthly quota response" theme={null} { "type": "about:blank", "status": 429, "title": "Too Many Requests", "detail": "Monthly request limit exceeded", "request_id": "1748123456-1a" } ``` For per-second errors, `Retry-After` tells you how long to wait for the bucket to refill. For monthly errors, it points to the next monthly reset. ## Upgrading To move to a higher plan, go to the [Radion dashboard](https://radion.app/dashboard). # Response Shapes Source: https://docs.radion.app/api/response-shapes How Radion API responses are structured: single objects, lists, and error bodies. The API has a few response shapes. A single object comes back bare, with no wrapper. Events and markets lists come back in named arrays with a cursor. Errors use an RFC 9457 body. ## Single objects A single-object endpoint returns the object at the top level. There is no `data` wrapper. For example `GET /v1/polymarket/price`: ```json theme={null} { "tokenId": "0x1234...", "price": 0.63 } ``` And `GET /auth/me`: ```json theme={null} { "keyLabel": "production_read", "plan": "starter", "callCount": 42, "createdAt": "2026-04-01T12:00:00Z", "lastUsedAt": "2026-05-25T10:30:00Z", "monthlyUsage": { "used": 120, "limit": 3000, "resetsAt": "2026-07-01T00:00:00Z" }, "requestsPerSecond": 10 } ``` ## Lists `GET /v1/polymarket/events` returns an `events` array and a `nextCursor` field. `GET /v1/polymarket/markets` returns a `markets` array and a `nextCursor` field. You use `nextCursor` to ask for the next page. See [Pagination](/api/pagination). ```json theme={null} { "events": [ { "conditionId": "0x1234...", "totalVolume": "152340.50", "tradeCount": 812, "lastPrice": "0.63", "openInterest": "48120.00", "lastTradeTs": "2026-05-25T10:30:00Z", "updatedBlock": 71250334 } ], "nextCursor": "eyJjb21wdXRlZF9hdCI6Ij..." } ``` `nextCursor` is `null` when there are no more pages. ## Search `GET /v1/polymarket/search` returns a composite object with three arrays and its own pagination. It is page-based, not cursor-based: ```json theme={null} { "events": [...], "themes": [...], "profiles": [...], "pagination": { "hasMore": true, "totalResults": 57 } } ``` Money amounts like volume, PnL, and open interest come back as strings. This keeps full precision and avoids rounding. Parse them as decimals, not as floats. Live orderbook values like `price`, `midpoint`, `spread`, and `size` come back as JSON numbers. ## Error responses Errors use `Content-Type: application/problem+json`. The body follows [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457): ```json theme={null} { "type": "about:blank", "status": 401, "title": "Unauthorized", "detail": "Invalid API key", "requestId": "1748123456-1a" } ``` | Field | What it means | | ----------- | -------------------------------------------------------------- | | `type` | The problem type. Always `about:blank` for normal HTTP errors. | | `status` | The HTTP status code. | | `title` | A short title. | | `detail` | What went wrong this time. | | `requestId` | The id for this request. Send it to us when you ask for help. | For the full list of error codes, see [Errors](/api/errors). # REST API Changelog Source: https://docs.radion.app/changelogs/api-changelog API changes, new features, breaking changes, and migration guides for Radion. API-facing changes for Radion integrators. See the [API overview](/api/overview) for the current contract. Entries tagged **Breaking** require changes to existing integrations. Review the migration notes before upgrading. List endpoints for events and markets now return `events` and `markets` arrays instead of a shared `data` field. Detail endpoints now use separate paths for id-based and slug-based lookups. ### Breaking * `GET /v1/polymarket/events` now returns `{ events, nextCursor }`. * `GET /v1/polymarket/markets` now returns `{ markets, nextCursor }`. * `GET /v1/polymarket/events/{identifier}` is replaced by `/events/{event_id}` for numeric ids and `/events/slug/{event_slug}` for slugs. * `GET /v1/polymarket/markets/{identifier}` is replaced by `/markets/{condition_id}` for condition ids and `/markets/slug/{market_slug}` for slugs. Numeric market ids are no longer accepted by the public REST route. ### Migration * Replace `res.data` with `res.events` for events lists. * Replace `res.data` with `res.markets` for markets lists. * Send event slugs to `/v1/polymarket/events/slug/{event_slug}`. * Send market slugs to `/v1/polymarket/markets/slug/{market_slug}`. * Keep condition ids on `/v1/polymarket/markets/{condition_id}`. Single-object endpoints no longer wrap their result in a `data` field. This is a **breaking change** for any code that reads `res.data` on a single object. ### Changed * Single objects return bare: `price`, `midpoint`, `spread`, `last-trade-price`, `orderbook`, `events/{id}`, `markets/{id}`, `auth/me`, and `POST /auth/keys`. Read fields at the top level (`res.price`, not `res.data.price`). * `data` is now reserved for cursor-paged lists only (`events`, `markets`): `{ data: [...], nextCursor }`. * `GET /search` returns the composite object bare: `{ events, themes, profiles, pagination }`. * `GET /auth/keys` renames its array from `data` to `keys`: `{ keys, plan, trialEndsAt, monthlyUsage }`. * Error bodies use camelCase: `request_id` is now `requestId`. ### Migration * Single object: replace `res.data` with `res` (or read the field directly). * Search: replace `res.data` with `res`. * Keys list: replace `res.data` with `res.keys`. * Errors: read `requestId` instead of `request_id`. Markets are now a first-class endpoint group, mirroring events. Data comes from the Polymarket Gamma catalog. ### Added * `GET /v1/polymarket/markets` — list markets, cursor-paginated. * `GET /v1/polymarket/markets/{identifier}` — one market by condition id (`0x…`), numeric id, or slug. * Both are also exposed as the `list_markets` and `get_market` MCP tools. Public JWT keys let apps with no backend call Radion straight from the browser or a mobile client. A public key (`pk_jwt_...`) is safe to ship, and each request also carries a signed end-user token. Separately, per-second rate limiting was removed from all paid plans. ### Added — public JWT keys * A new key type, `pk_jwt_...`, is safe to embed in a browser or mobile app. On its own it does nothing: every request must also send a signed user JWT in the `Authorization: Bearer` header. * Configure the key in the dashboard with a **JWKS URL** or an inline **public key (PEM)**, plus optional `aud` / `iss` checks and a per-user rate limit. * Radion verifies the JWT signature, checks `exp` (and `aud` / `iss` when set), and rate-limits per user (`sub`). Monthly usage still counts against your plan. * Tokens must use an asymmetric algorithm (`RS*`, `PS*`, `ES*`, or `EdDSA`). Symmetric algorithms (`HS256` and friends) are rejected. * Browser WebSocket clients that cannot set headers may pass `api-key` and `token` as query parameters on the `/ws` upgrade. See [Authentication](/api/authentication#public-jwt-keys). ### Breaking * `requestsPerSecond` in the `GET /auth/me` response is now `null` on paid plans. It stays a number on the free plan. Handle a nullable value. The orderbook read model expanded from a single midpoint into full depth plus dedicated price endpoints. Search now paginates, events carry richer metadata, and two fields were removed. ### Breaking — removed * `GET /v1/polymarket/lookup`. Use `/search` for fuzzy text search, or the id-scoped endpoints (`/markets/{condition_id}`, `/markets/slug/{market_slug}`, and the trader endpoints by address) for exact reads. * The `expiresAt` field from `GET /auth/me`. API keys do not expire. ### Breaking — changed * `GET /v1/polymarket/orderbook` now returns full depth instead of only the midpoint. The response holds `bids` and `asks` arrays of `{ price, size }` levels, plus `lastTradePrice` and `tokenId`. It still takes `token_id`. Read the midpoint from the new `/midpoint` endpoint. ### Added * `GET /v1/polymarket/midpoint` — midpoint price for one outcome token. Takes `token_id`. * `GET /v1/polymarket/price` — best `bid` or `ask` price. Takes `token_id` and `side` (`buy` or `sell`). * `GET /v1/polymarket/spread` — bid-ask spread. Takes `token_id`. * `GET /v1/polymarket/last-trade-price` — last executed trade price. Takes `token_id`. ### Changed * `GET /v1/polymarket/search` now returns a `pagination` object with `hasMore` and `totalResults`, and each profile includes `createdAt`. Query params are `q`, `limit`, and `page`. * `GET /v1/polymarket/events` and `/events/{identifier}` now return enriched metadata: `ticker`, `subtitle`, `resolutionSource`, `image`, `icon`, `featuredImage`, `subcategory`, `seriesSlug`, the `active` / `closed` / `archived` / `new` / `featured` / `restricted` flags, `enableOrderbook`, `negRisk`, `competitive`, `volume24hr` / `volume1wk` / `volume1mo` / `volume1yr`, `commentCount`, `creationDate`, `closedTime`, and the full `markets` array. Trader endpoints now enrich responses with live Polymarket data instead of placeholders. ### Added * `GET /v1/polymarket/traders/{trader_id}/profile` now returns a real `identity` (pseudonym, name, x username, verified badge, account created at) from the trader's Polymarket public profile. * `GET /v1/polymarket/traders/{trader_id}/themes` now returns `themeLeaderboard`, `eliteThemes`, and `avoidThemes` aggregated from market tags; each per-market entry now carries the market `slug`. * `GET /v1/polymarket/traders/{trader_id}/edge` now fills `bestEdgeConditions.themes`. ### Breaking — removed * The `warnings.themeDataUnavailable` field from the trader `edge` and `themes` responses. Radion now serves Polymarket data from its own on-chain indexer instead of stored analysis snapshots. The analysis and backtesting products were removed and replaced by direct read-model endpoints for markets, traders, events, search, and the orderbook. ### Breaking — removed * `GET /v1/polymarket/markets/analysis` and `GET /v1/polymarket/markets/{slug}/analysis`. * `GET /v1/polymarket/traders/analysis` and `GET /v1/polymarket/traders/{id}/analysis`. * All `/v1/polymarket/backtesting/copytrading/*` endpoints. * Response schema (component) names dropped the `Dto` suffix, e.g. `TradeDto` → `Trade`. This affects code generated from the OpenAPI schema. * Every single-object response is now wrapped in a `data` field, so all endpoints share one shape. `GET /traders/{trader_id}/pnl`, market detail, market-by-slug, orderbook, search, and event endpoints used to return the object on its own; read the value from `data` now. ### Added * `GET /v1/polymarket/markets` — list markets by volume; plus `/markets/{condition_id}` and `/markets/slug/{market_slug}`. * `GET /v1/polymarket/markets/{condition_id}/candlestick`, `/holders`, and `/trades`. * `GET /v1/polymarket/traders/{id}/trades`, `/positions`, and `/pnl`. * `GET /v1/polymarket/traders/{id}/profile`, `/performance`, `/risk`, `/edge`, `/distribution`, and `/themes` — rebuilt on the on-chain read model. `identity` fields are `null` and theme grouping ships empty for now; `/themes` still returns best/worst markets by realized PnL, keyed by condition id. * `GET /v1/polymarket/traders/global-pnl` — PnL leaderboard. * `GET /v1/polymarket/events` and `/events/{identifier}`. * `GET /v1/polymarket/search` — venue-agnostic text search across events, topics, and profiles (proxied from Polymarket). * `GET /v1/polymarket/lookup` — exact lookup of a market or trader by id (condition id or address). This is the old `/search` behaviour, renamed. * `GET /v1/polymarket/orderbook` — live orderbook midpoint for one outcome token. The path is `orderbook` (one word, no hyphen) and takes `token_id`, not `position_id`. ### Migration * Replace analysis reads with the on-chain endpoints: market metadata via `/markets` and `/events`, trader data via `/traders/{id}/pnl`, `/positions`, and `/trades`. * Drop backtesting calls; the copytrading simulator is no longer offered. * Regenerate any OpenAPI-derived client so schema names without the `Dto` suffix resolve. * Stop reading `traderScore` and `confidence.score` / `rating` from the performance response. Request throttling moved from an hourly quota to a per-second token bucket. Monthly quotas are unchanged. The burst is keyed by your account and shared across all keys. ### Breaking * Hourly quotas are removed. Per-second rate limits are now Free: 5, Starter: 10, Pro: 50 requests/second. Enterprise stays uncapped. * `GET /auth/me`: `hourlyLimit` is renamed to `requestsPerSecond`. ### Unchanged * Monthly quotas remain Free: 300, Starter: 3,000, Pro: 50,000 requests/month, shared across all keys and reset at the start of each UTC month. * Exceeding either limit still returns `429 Too Many Requests` with a `Retry-After` header. Win rate and PnL shared one field name across endpoints while measuring different things. Trader analysis is lifetime (includes resolved-but-unclaimed positions and unrealized PnL); performance is realized-only over a trailing 365-day window. Fields were renamed so one name never means two computations. Entry-bucket fields became structured objects instead of raw price arrays. ### Breaking — field renames * `GET /v1/polymarket/traders/{trader_id}/performance`: `winRate` → `trailing365WinRate`, `realizedPnl` → `realized365Pnl` (both: closed positions, trailing 365 days). * `GET /v1/polymarket/traders/{trader_id}/analysis` and the trader analyses list: `winRate` → `lifetimeWinRate`, `profitLoss` → `lifetimePnl` (both: lifetime), `medianVolume` → `medianPositionSizeUsd` (value unchanged). * Market analysis `topTraders[]`: `winRate` → `lifetimeWinRate`, `profitLoss` → `lifetimePnl` (same lifetime basis). ### Breaking — entry buckets * `GET /v1/polymarket/traders/{trader_id}/distribution`: `sweetSpotBucket` → `sweetSpotBuckets` and `deadZoneBucket` → `deadZoneBuckets`. Both changed from a nullable price array (`number[] | null`) to a required array of `EntryBucket` objects. * `GET /v1/polymarket/backtesting/copytrading/jobs/{jobId}/summary`: `priceRange` (`number[] | null`) → required `bestBuckets` (`EntryBucket[]`). * `EntryBucket` objects gained two required fields: `realizedPnl` and `volumeUsd`. ### Migration * Update field reads to the new names. The two win rates and two PnL figures are not interchangeable — analysis values are lifetime (unclaimed + unrealized), performance values are realized over a trailing 365 days. * Read `medianPositionSizeUsd` instead of `medianVolume`; value and units (USD) unchanged. * Read bucket fields as `EntryBucket` objects, not price numbers, and drop null handling — these fields are now always present. Trader profile affinity now uses themes derived from Polymarket market tags instead of Polymarket's legacy category field. The API naming changed from categories to themes. ### Breaking * `GET /v1/polymarket/traders/{trader_id}/categories` → `GET /v1/polymarket/traders/{trader_id}/themes`. * Response types and fields renamed: `categoryLeaderboard` → `themeLeaderboard`, `eliteCategories` → `eliteThemes`, `avoidCategories` → `avoidThemes`. * Leaderboard and ROI entries now expose `theme` instead of `category`. * Warning field `categoryDataUnavailable` → `themeDataUnavailable`. * `bestEdgeConditions.categories` → `bestEdgeConditions.themes`. ### Migration * Call the `/themes` endpoint for trader theme affinity. * Update clients to read `themeLeaderboard`, `eliteThemes`, `avoidThemes`, `theme`, and `themeDataUnavailable`. * Update edge consumers to read `bestEdgeConditions.themes`. API access now requires an API key on every endpoint except `GET /health`. Dashboard key management supports multiple active keys, per-plan key caps, and shared monthly usage counters. Premium features are now enforced by plan. ### Breaking * Unauthenticated API requests are no longer accepted. Send `X-API-Key` on every request except `GET /health`. * Plan names changed from `signal` / `agent` / `custom` to `starter` / `pro` / `enterprise`. * `GET /auth/me` now returns `keyLabel` instead of `keyId`, and may return `plan: null` for Free keys. * `GET /v1/polymarket/markets/{slug}/analysis` now returns `{ "data": [...] }`. Without `from`, the array contains the latest snapshot as a single item. ### Added * `GET /auth/keys` — list active keys for the signed-in dashboard user. * `POST /auth/keys` — create a key with a `keyLabel`. The raw key is returned once as `rawKey`. * `DELETE /auth/keys/{keyLabel}` — revoke an active key by label. * `GET /auth/me` and `GET /auth/keys` now expose monthly usage via `monthlyUsage.used`, `monthlyUsage.limit`, and `monthlyUsage.resetsAt`. `GET /auth/me` also includes `hourlyLimit`. * `GET /v1/polymarket/markets/{slug}/analysis?from=YYYY-MM-DD` returns historical snapshots computed on or after the date. ### Changed * Hourly quotas are now Free: 50, Starter: 100, Pro: 1,000 requests, Enterprise: custom. * Monthly quotas are now enforced: Free: 300, Starter: 3,000, Pro: 50,000 requests, Enterprise: unlimited. * Monthly usage is shared across all keys for the same user. Extra keys do not create extra quota. * Active key caps are Free: 1, Starter: 3, Pro: 10, Enterprise: unlimited. * `POST /v1/polymarket/backtesting/copytrading/run` now requires Pro or Enterprise. * Historical market snapshots through the `from` parameter require Starter or higher. * Monthly quota exhaustion returns `429 Too Many Requests` with `detail: "Monthly request limit exceeded"` and a `Retry-After` header pointing to the next monthly reset. ### Migration * Create a free key from the dashboard and send it with `X-API-Key` on all API calls. * Update plan handling to use `starter`, `pro`, `enterprise`, or `null` for Free keys. * Read `keyLabel` instead of `keyId` from auth responses. * Update market-analysis-by-slug clients to read the first item of `data` when they need only the latest snapshot. Copytrading backtest jobs now continue when upstream price history is unavailable for individual tokens. Summary responses include warning metadata so you can detect degraded price-history coverage. The authenticated API key metadata endpoint was renamed to `GET /auth/me`. ### Breaking * `GET /auth/api-key` was removed. Use `GET /auth/me` with the same `X-API-Key` header instead. ### Changed * `GET /auth/me` returns the same API key metadata response shape that `GET /auth/api-key` returned. * `GET /backtesting/copytrading/jobs/{jobId}/summary`: completed jobs may include a `warnings` object with `missingPriceHistoryTokenCount`, `totalTokenCount`, and `priceHistoryCoveragePct`. * If price history is missing for some tokens, open-position valuation may be less precise. The simulator continues with its fallback pricing instead of failing the whole job. ### Example ```json theme={null} { "data": { "status": "done", "warnings": { "missingPriceHistoryTokenCount": 3, "totalTokenCount": 42, "priceHistoryCoveragePct": 92.86 } } } ``` API endpoints are now available under the `/v1` namespace. List endpoints use opaque cursor-based pagination. The copytrading backtest request field was renamed for multi-venue compatibility. All endpoints now validate input parameters and return structured errors on invalid input. ### Breaking * `GET /markets/analysis` and `GET /traders/analysis`: the `offset` parameter is removed. Use `cursor` instead (omit on first request). * Response shape now includes `nextCursor`: `{ "data": [...], "nextCursor": "..." | null }`. * `POST /backtesting/copytrading/run`: request field `traderAddresses` renamed to `traderIds`. The Ethereum-address format constraint was removed — any non-empty string up to 256 characters is accepted, supporting non-Ethereum venues. ### Changed * Use `/v1` as the canonical prefix for versioned analysis and backtesting requests, e.g. `GET /v1/polymarket/markets/analysis`. * Health and authentication endpoints stay at root: `GET /health` and `GET /auth/api-key`. * Existing unversioned routes such as `GET /markets/analysis` continue to work for now. * All endpoints now validate input. Invalid values return `400 Bad Request` with a structured `application/problem+json` body. * `GET /markets/{slug}/analysis`: `slug` must be 1–200 chars. * `GET /traders/{id}/analysis`: `id` must be non-empty, max 256 chars. * `GET /backtesting/copytrading/jobs/{jobId}/*`: `jobId` must be a valid UUID. * `GET /markets/analysis` and `GET /traders/analysis`: default `limit` changed from 50 to 5, max `limit` from 100 to 10. `offset` is replaced by cursor-based pagination. ### Deprecation notice * Unversioned endpoints are backward-compatible aliases and will be removed on June 16, 2026. ### Migration * Update API clients to call analysis and backtesting endpoints under `/v1/polymarket`. Generic `/v1/markets`, `/v1/traders`, and `/v1/backtesting` paths are not served. * No request-body or response-body changes are required for the `/v1/polymarket` migration. * Remove `offset` from all requests to list endpoints. * Pass the `nextCursor` value from each response as the `cursor` parameter on the next request. Stop when `nextCursor` is `null`. * Update analysis list requests that set `limit` above 10. The default is now 5, and the maximum is now 10. * Rename `traderAddresses` to `traderIds` in all `POST /backtesting/copytrading/run` request bodies. See [Pagination](/api/pagination) for full details and code examples. Trader metrics fields were renamed and deduplicated for consistency across trader-analysis and market top-trader responses. ### Changed * `walletMedianVolume` on trader-analysis responses was renamed to `medianVolume`. Value and type unchanged. * The redundant `positionCount` field was removed from market top-trader entries. It always duplicated `totalPositionCount`. ### Migration * Read `medianVolume` instead of `walletMedianVolume` wherever you consume trader-analysis responses. * Read `totalPositionCount` instead of `positionCount` on market top-trader entries. Market analysis responses now expose the canonical venue-scoped market identifier only at `market.id`. ### Changed * The top-level `marketConditionId` field was removed from market-analysis responses. Use `market.id` instead. * Request paths are unchanged. Continue to use market slugs in endpoints such as `GET /markets/{slug}/analysis`. ### Migration Update clients to read `market.id` instead of `marketConditionId`. ```json Before theme={null} { "marketConditionId": "0xabcdef..." } ``` ```json After theme={null} { "market": { "id": "0xabcdef..." } } ``` Error responses now return a structured JSON body following [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457). ### Changed * All error responses now return `type`, `status`, `title`, `detail`, and `request_id` with `Content-Type: application/problem+json`. * The `429 Too Many Requests` response still includes a `Retry-After` header. ### Migration Update error handling to read `detail` and `request_id` instead of a flat `error` string. ```json Before theme={null} { "error": "Invalid API key" } ``` ```json After theme={null} { "type": "about:blank", "status": 401, "title": "Unauthorized", "detail": "Invalid API key", "request_id": "1748123456-1a" } ``` All API endpoints except `GET /health` now enforce hourly request quotas. Unauthenticated requests are allowed and count against the free-tier quota, keyed by IP address. ### Quotas by plan * Free (no key): 10 requests/hour, keyed by IP address. * Signal: 100 requests/hour, keyed by API key. * Agent: 1,000 requests/hour, keyed by API key. * Custom: unlimited. ### Behavior * Your full hourly quota is available immediately as a burst, then replenishes continuously through the hour. * Requests exceeding the quota receive `429 Too Many Requests` with a `Retry-After` header. The endpoints for creating on-demand analysis runs and checking their status were removed. Analysis snapshots are still available via the read endpoints. ### Removed * `POST /markets/{slug}/analysis` * `GET /markets/{slug}/analysis/status` * `POST /traders/{id}/analysis` * `GET /traders/{id}/analysis/status` ### Unchanged * `GET /markets/{slug}/analysis` — read the latest market analysis snapshot. * `GET /traders/{id}/analysis` — read the latest trader analysis snapshot. ### Migration Replace run-and-poll flows with direct reads. Analysis is now refreshed automatically. ```bash Before theme={null} curl -X POST "https://api.radion.app/markets/will-trump-win-2024/analysis" \ -H "X-API-Key: YOUR_API_KEY" # then poll /analysis/status ... ``` ```bash After theme={null} curl "https://api.radion.app/markets/will-trump-win-2024/analysis" \ -H "X-API-Key: YOUR_API_KEY" ``` Analysis responses now expose additional trader-history fields without changing route shapes or request parameters. ### Added * `winRate` on trader-analysis responses and `topTraders` entries in market-analysis responses. Based on closed positions only. * `latestPositionTimestamp` on trader-analysis responses and `topTraders` entries in market-analysis responses. Both fields may be `null` on older snapshots created before these fields were tracked. Radion's API now separates featured-feed browsing, search, and analysis flows. ### Added * `GET /traders/{id}/analysis` — read trader analysis snapshots. * `venue` query filtering for search and market-list flows. `polymarket` is the currently supported venue. # SDKs Changelog Source: https://docs.radion.app/changelogs/sdks-changelog Releases, additions, and breaking changes for the official Radion TypeScript, Python, and Rust SDKs. Changes to the official Radion SDKs. See the [SDKs overview](/sdks/overview) for the current API. Entries tagged **Breaking** require changes to existing integrations. The pending feed moved from a `mempool.` channel prefix to a `confirmed` boolean, and the pending payload now carries per-order detail. ### Changed (Breaking) * The `mempool.` channel prefix is removed. To subscribe to the pending feed, set `confirmed: false` on the subscription (default `true`) with the bare channel name — e.g. `{ id, channel: "trading", confirmed: false }`. In Rust, `SubscribableChannel::Mempool(Channel::Trading)` is gone; use `Subscription::new(id, Channel::Trading).pending()`. * Event frames are unified: `confirmed` is now a boolean on the frame envelope, and `channel` is always the bare name for both feeds. Route pending vs confirmed on `event.confirmed`, not on a `mempool.`-prefixed channel string. `ChannelPayloadMap` (TS) and the `on_channel` `mempool.` overloads (Python) drop their `mempool.`-prefixed keys. * The pending-transaction payload dropped its inner `confirmed` field (now on the envelope). * The decoded `call` object renames `usd` to `notional_usd`. ### Added * The decoded `call` object gains an `orders` array for exchange trades: each entry has `maker`, `taker` (nullable), `token_id`, `side` (`buy`/`sell`), `maker_amount`, and `taker_amount`. Empty for non-trade calls. No derived price yet — compute it from the amounts. * A `mempool_unavailable` warning frame is sent right after a `confirmed: false` subscribe when the upstream node exposes no pending stream, so the subscription no longer goes silently empty. `min_usd` keeps its name on both feeds, but the confirmed feed measures the **filled** amount while the pending feed measures the order's **intended fill** (`call.notional_usd`). Realtime payload types now cover every field each channel's events carry. ### Fixed * Channel docs now document all 77 confirmed events, and each channel's typed payload is the union of its events' fields. * Corrected wrong field names: `fees` uses `receiver` / `tokenId` / `amount`; `resolution` uses `payoutNumerators` / `result`; the transfers batch uses `ids` / `amounts`. * Added previously-missing fields (asset ids, UMA question parameters, neg-risk / combinatorial / bridge fields, `accounts` `id` / `implementation`, and more), and dropped stray fields no event carries. Non-breaking. Topic-channel payloads keep their loose, all-optional shape (Rust fields are `Option`), so existing code keeps working — there are just more fields available. The CLOB order-book feed is now a first-class, typed channel family. ### Added * CLOB channels are now subscribable directly: `clob.book`, `clob.prices`, `clob.last_trade`, `clob.midpoint`, `clob.tick_size`, `clob.best_bid_ask`. Each has a typed payload, requires a `token_ids` filter, has no `data.type` discriminator, and has no `mempool.` companion. `clob.prices` is the CLOB price-change feed — a separate family from the topic channels, unrelated to the derived last-trade `prices` channel removed in v0.4.0. The realtime channel taxonomy was redesigned. Every decoded event now routes to exactly one of nine topic channels, plus the two cross-cutting filter channels (`wallets`, `markets`). ### Changed (Breaking) * `trades` is renamed to `trading`. Exchange fees it carried moved to the new `fees` channel. `TradesPayload` is now `TradingPayload`; in Rust, `Payload::Trades` is now `Payload::Trading`. * Removed the `global`, `activity`, `large_trades`, `prices`, and `collateral` channels, along with the `ActivityPayload`, `CollateralPayload`, and `PricesPayload` types (and their Rust variants). Migrate: * `global` → subscribe to the specific channels you need * `activity` → `positions` and `combos` * `large_trades` → `trading` with a `min_usd` filter * `prices` → `trading` (fills carry the price) * `collateral` → `positions` ### Added * New confirmed channels `fees`, `resolution`, `positions`, `transfers`, and `accounts`, each with a typed payload (`FeesPayload`, `ResolutionPayload`, `PositionsPayload`, `TransfersPayload`, `AccountsPayload`) and its own `data.type` discriminator set. Each gains a `mempool.` companion. Mempool channels now carry the same typed payload as their confirmed channel across all three SDKs. All changes are backward compatible. ### Changed * **TypeScript** — `onChannel` / `offChannel` narrow `event.data` for `mempool.` channels. `mempool.trading` now yields `TradingPayload` instead of `AnyChannelPayload`. `ChannelPayloadMap` is derived from the new `ConfirmedChannelPayloadMap` and includes the `mempool.` keys. * **Python** — `on_channel` gains `@overload` signatures for every `mempool.` companion, so handlers narrow to the channel's payload type (e.g. `ChannelHandlerFor[TradingPayload]`). * **Rust** — no API change; the SDK already decoded `mempool.` events into the typed `Payload` enum. Version bumped to stay in lockstep. The official Rust SDK ships as `radion-sdk` on crates.io (imported as `radion_sdk`), at feature parity with the TypeScript and Python SDKs. ### Added * `radion-sdk` — async-first Rust SDK built on `tokio`. Requires Rust 1.85+. * Unified `Radion` client with a builder; the realtime surface is reached as `radion.realtime`. * Stream-based, fully-typed events: `subscribe(...)` returns a `Stream` of that subscription's events, with `events()` and `lifecycle()` for the firehose and connection events. Each event's `data` is the typed `Payload` enum; unknown channels and event types are preserved as `Payload::Other`. * Auto-reconnect with exponential backoff and jitter, subscription replay, and heartbeat / stale-connection detection — matching the other SDKs' defaults. * Cargo features: `realtime` (default), `rustls` (default), `native-tls`, and `tracing`. Every frame, channel, and payload is now fully typed in both SDKs, validated at the parse boundary. ### Added * Typed event payloads for every channel — `event.data` is the channel's payload type, with every `data.type` discriminator enumerated. Handlers registered with `client.onChannel("", …)` receive `event.data` narrowed to that channel's payload. * Typed channels and `mempool.` companions, plus per-channel filter validation — `subscribe` now rejects a subscription that omits a required filter (e.g. `wallets` or `markets`). * Runtime frame validation at the parse boundary — `zod` (TypeScript) and `msgspec` (Python). Malformed frames are dropped; event types added server-side are forwarded untouched. * Typed error frames, including `skipped` on a `lagged` error. ### Changed (Breaking) * `ChannelEvent.data` is now the channel's typed payload instead of `unknown` (TypeScript) / `Any` (Python). Code that treated it as untyped may need to narrow on `data.type`. * `Subscription.channel` is typed to the known channel set (confirmed or `mempool.`-prefixed). * The overloaded realtime `on` / `off` are replaced by intent-specific methods. TypeScript: `onChannel` / `offChannel`, `onAnyChannel` / `offAnyChannel` (formerly `on("event")`), and `onLifecycle` / `offLifecycle` (`open`, `close`, `reconnect`, `error`). Python uses the same names in snake\_case. Migrate `on("", h)` → `onChannel("", h)`, `on("event", h)` → `onAnyChannel(h)`, and `on("open"|"close"|"reconnect"|"error", h)` → `onLifecycle(…, h)`. * TypeScript adds `zod` and Python adds `msgspec` — both SDKs keep a minimal dependency footprint. Official Radion SDKs are now available — `@radion-app/sdk` and `radion-sdk`, one typed client per language, one API key. ### Added * `@radion-app/sdk` — TypeScript SDK, ships ESM + CJS and its own type definitions. Requires Node.js 18+. * `radion-sdk` — async-first Python SDK (imported as `radion`). Requires Python 3.10+. * Unified `Radion` client: one entry point per language, holding a single API key. The realtime (WebSocket) API is available under `radion.realtime`. * Realtime client with a subscription model — `{ id, channel, filters }` — plus automatic reconnect with exponential backoff, heartbeats, and subscription restore after reconnect. * Server-side filters: `wallets`, `market_ids`, `token_ids`, `min_usd`. Channels may carry a `mempool.` prefix. * Typed channels, frames, and a structured error hierarchy (`RadionError`, `RadionConnectionError`, `RadionServerError`). # WebSockets Changelog Source: https://docs.radion.app/changelogs/websockets-changelog Channel additions, breaking changes, and protocol updates for the Radion WebSocket API. WebSocket-facing changes for Radion integrators. See the [WebSockets overview](/websockets/overview) for the current protocol. Entries tagged **Breaking** require changes to existing integrations. The pending feed moved from a `mempool.` channel prefix to a `confirmed` boolean on the subscribe message, event frames are unified, and the decoded `call` now carries per-order detail. ### Changed (Breaking) The `mempool.` channel prefix is gone. Subscribe to the pending feed by setting `confirmed: false` (default `true`) with the bare channel name: ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "trading", "confirmed": false } ``` Event frames now carry `confirmed` on the envelope and always use the bare `channel` name for both feeds. Tell pending from confirmed by the `confirmed` flag, and route by your subscription `id`: ```json theme={null} { "type": "event", "id": "pending", "channel": "trading", "confirmed": false, "data": { … } } ``` The pending `data` object dropped its inner `confirmed` field (now on the envelope), and the decoded `call` renames `usd` to `notional_usd`. ### Added * The decoded `call` gains an `orders` array for exchange trades — each with `maker`, `taker` (nullable), `token_id`, `side` (`buy`/`sell`), `maker_amount`, and `taker_amount`. Empty for non-trade calls. No derived price yet. * A `mempool_unavailable` warning frame follows a `confirmed: false` subscribe when the upstream node exposes no pending stream, so a pending subscription no longer sits silently empty. Confirmed event payloads now match the documented shape, and every event has a full payload reference. ### Fixed (Breaking) Confirmed event frames now put the snake\_case discriminator inside `data.type`, with the event's fields alongside it — the shape the docs and SDKs always described: ```json theme={null} { "type": "event", "channel": "trading", "data": { "type": "order_filled_v2", "orderHash": "0x…" } } ``` Before, the server sent an externally-tagged `data` (`{ "OrderFilledV2": { … } }`). If you parsed that raw shape, read `data.type` plus the sibling fields instead. The official SDKs already expect this. ### Docs Every channel page now documents **all** its events — each with a payload example and a full field table generated from the on-chain ABI (77 confirmed events across [`trading`](/websockets/channels/trading), [`fees`](/websockets/channels/fees), [`oracle`](/websockets/channels/oracle), [`resolution`](/websockets/channels/resolution), [`lifecycle`](/websockets/channels/lifecycle), [`positions`](/websockets/channels/positions), [`combos`](/websockets/channels/combos), [`transfers`](/websockets/channels/transfers), and [`accounts`](/websockets/channels/accounts)). The `clob.*` channels are unchanged: their `data` has no `type` discriminator, because each clob channel has one fixed payload shape. Channels were reworked into a clean topic taxonomy. Every decoded event now routes to exactly one topic channel. The `wallets` and `markets` filter channels still scope across all topics. ### Removed (Breaking) * `global` (and `mempool.global`) — it matched everything. Subscribe to each topic channel you need, or use `wallets` / `markets` to scope across topics. * `activity` — its events split into [`positions`](/websockets/channels/positions) (plain CTF splits, merges, redemptions) and [`combos`](/websockets/channels/combos) (module, neg-risk, conversion events). * `large_trades` — use [`trading`](/websockets/channels/trading) with a `min_usd` filter. * The derived `prices` channel (last-trade ticks). The unrelated CLOB `clob.prices` channel is unaffected. ### Renamed (Breaking) * `trades` → [`trading`](/websockets/channels/trading) (and `mempool.trades` → `mempool.trading`). Fees moved off it to the new `fees` channel. ### Added * Confirmed channels [`fees`](/websockets/channels/fees), [`resolution`](/websockets/channels/resolution), [`transfers`](/websockets/channels/transfers), [`accounts`](/websockets/channels/accounts), and [`positions`](/websockets/channels/positions), each with a `mempool.` twin. * Settlement events (resolutions, reported results, pauses) moved off `lifecycle` and `combos` onto `resolution`. ERC-1155 transfers moved off `combos` onto `transfers`. * `min_usd` now applies to `trading`; `market_ids` / `token_ids` apply across the topic channels that accept them. Filters combine **AND across axes, OR within an axis**. See [Filters](/websockets/filters). Radion no longer watches the USDC.e and pUSD collateral token contracts. These ERC-20 transfers, approvals, and wraps fed no analytics, and the USDC.e filter matched every USDC.e transfer on Polygon, not just Polymarket ones. ### Removed (Breaking) * Confirmed channel `collateral` and its twin `mempool.collateral`. Subscribing now returns an `unknown channel` error. * Events `Transfer`, `Approval`, `Wrapped`, and `Unwrapped` are no longer decoded or streamed. * The `collateral` value is removed from `contract_kinds` on mempool frames. Collateral-adapter splits, merges, redemptions, and conversions are unaffected — they still stream on the `activity` channel. Radion WebSockets are live. Subscribe to decoded Polymarket contract events and speculative mempool transactions over one persistent connection. ### Added * Confirmed channels: `global`, `trades`, `oracle`, `lifecycle`, `activity`, `collateral`, `combos`, `wallets`, `markets`, `large_trades`, `prices`. * Mempool channels: the same set with a `mempool.` prefix. * Filters: `wallets`, `market_ids`, `token_ids`, `min_usd`. `wallets` narrows the `trades` channel to specific traders (matching a fill's maker or taker). All filters mirror to the `mempool.*` variants, matched against transaction calldata. * Control frames: `subscribe`, `unsubscribe`, `ping` / `pong`, and a `lagged` error with skipped event count. * Per-plan connection and subscription limits. See [Plan limits](/websockets/overview#plan-limits). # Onchain Data Source: https://docs.radion.app/concepts/onchain-data Every Polymarket contract event Radion decodes, and how each supports market and trader analytics. Radion reads the logs from every Polymarket contract on Polygon. It turns those raw logs into a clean, typed event stream. That stream is what we use to rebuild trades, track positions, watch markets resolve, and compute trader PnL. ## Source Every event on this page comes from one of 18 Polymarket contracts on Polygon. These are the contracts Radion watches for logs. ### Exchanges Order fills, matches, fees, and the order lifecycle. | Contract | Address | | ----------------------- | -------------------------------------------- | | CTFExchange (v1) | `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` | | CTFExchange (v2) | `0xE111180000d2663C0091e4f400237545B87B996B` | | NegRiskCTFExchange (v1) | `0xC5d563A36AE78145C45a50134d48A1215220f80a` | | NegRiskCTFExchange (v2) | `0xe2222d279d744050d28e00520010520000310F59` | | Combos Exchange | `0xe3333700cA9d93003F00f0F71f8515005F6c00Aa` | ### Conditional tokens and collateral Position splits, merges, redemptions, and ERC-1155 transfers at the base layer. | Contract | Address | | --------------------------- | -------------------------------------------- | | ConditionalTokens (CTF) | `0x4D97DCd97eC945f40cF65F87097ACe5EA0476045` | | CtfCollateralAdapter | `0xAdA100Db00Ca00073811820692005400218FcE1f` | | NegRiskCtfCollateralAdapter | `0xadA2005600Dec949baf300f4C6120000bDB6eAab` | ### Market setup and resolution NegRisk market setup, outcome reporting, and UMA oracle resolution. | Contract | Address | | ------------------- | -------------------------------------------- | | NegRiskAdapter | `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` | | UMAAdapter | `0x6A9D222616C90FcA5754cd1333cFD9b7fb6a4F74` | | UMAOptimisticOracle | `0xCB1822859cEF82Cd2Eb4E6276C7916e692995130` | ### Modules and redemption Binary, neg-risk, and combinatorial modules, plus automatic redemption and combos position transfers. | Contract | Address | | ------------------- | -------------------------------------------- | | AutoRedeemer | `0xa1200000d0002264C9a1698e001292D00E1b00af` | | BinaryModule | `0x1000008dD9001B968442c1000017eaE6E0dA00Ba` | | NegRiskModule | `0x200000900045e3B6259600682756002200028933` | | CombinatorialModule | `0x30000034706C7d8e12009DAB006Be20000c031A8` | | PositionManager | `0x006F54F7f9A22e0000CC2AB60031000000ae9fEF` | ### Wallet factories Deposit wallet and Gnosis Safe proxy creation. | Contract | Address | | ------------------------ | -------------------------------------------- | | DepositWalletFactory | `0x00000000000Fb5C9ADea0298D729A0CB3823Cc07` | | Gnosis Safe ProxyFactory | `0xaacfeea03eb1561c4e67d661e40682bd20e3541b` | ### Adapter contracts Some of these contracts are adapters. An adapter wraps Polymarket's base ConditionalTokens (CTF) protocol and adds market-specific logic on top. The base CTF contract is generic. It only knows conditions, positions, and payouts. It does not know about neg-risk markets, fees, or how a market resolves. Adapters fill that gap: * **CtfCollateralAdapter** and **NegRiskCtfCollateralAdapter** handle collateral. They split and merge positions and redeem them into collateral, so trades settle in USDC instead of raw CTF positions. * **NegRiskAdapter** runs neg-risk markets. It prepares markets and questions, reports outcomes, and converts exposure across neg-risk outcomes. * **UMAAdapter** connects markets to the UMA optimistic oracle. It initializes questions, applies results, and handles pauses and emergency resolution. Each adapter emits its own events. That is why Radion decodes both the base CTF events and the adapter events. Together they show the full path of a position: how it is created, moved, and resolved. ## What we decode | Area | Events | Why it matters | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Trades | `OrderFilledV1`, `OrderFilledV2`, `OrdersMatchedV1`, `OrdersMatchedV2`, `FeeChargedV1`, `FeeChargedV2`, `OrderCancelled`, `OrderPreapproved`, `OrderPreapprovalInvalidated`, `TradingPaused`, `TradingUnpaused`, `TokenRegistered` | Full order lifecycle across exchange v1 and v2. | | Redemptions | `Redemption`, `BinaryRedemption`, `NegRiskRedemption`, `PositionsRedeemed` | All paths by which positions are redeemed into collateral. | | Collateral operations | `CollateralPositionSplit`, `CollateralPositionsMerged`, `CollateralPositionsConverted` | Collateral-layer splits, merges, and neg-risk conversions. | | Conditional tokens | `ConditionPreparation`, `ConditionResolution`, `CtfPositionSplit`, `CtfPositionsMerge`, `CtfPayoutRedemption`, `TransferSingle`, `TransferBatch` | Base CTF layer: position creation, merging, redemption, and transfer. | | NegRisk adapter | `MarketPrepared`, `NegRiskQuestionPrepared`, `OutcomeReported`, `NegRiskPositionsConverted` | NegRisk market setup and outcome reporting. | | UMA resolution | 16 events across UMAAdapter and UMAOptimisticOracle | Full oracle lifecycle: initialization, proposals, disputes, settlements, pauses, emergency resolution. | | Module lifecycle | `EventPrepared`, `ResultReported`, `PositionRedeemed`, `ModulePositionsMerged`, `ModulePositionsSplit`, `HorizontalMerge`, `HorizontalSplit`, `PositionConverted`, `ConditionResolved`, `ResolutionPaused`, `ResolutionUnpaused`, `ResolverPaused`, `ResolverUnpaused` | Binary and NegRisk module operation and resolution control. | | Bridge and migration | `BridgePositionMinted`, `BridgePositionsBurned`, `LegacyCollateralSettled`, `MigrationConditionRegistered`, `MigrationResolved`, `PositionMigrated` | Cross-module bridge and v1→v2 migration flows. | | Combinatorial | `CombinatorialConditionPrepared`, `Compressed`, `ConvertedToYesBasket`, `Extracted`, `Injected`, `MergedFromYesBasket`, `MergedOnCondition`, `SplitOnCondition`, `CombinatorialWrapped`, `CombinatorialUnwrapped` | Combinatorial market operations. | | Wallet factories | `WalletDeployed`, `ProxyCreation` | Deposit wallet and Gnosis Safe proxy creation. | ## Event reference ### Trades and order lifecycle Shared across CTFExchange v1/v2 and NegRiskCTFExchange v1/v2. | Event | Role | Contract | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | | `OrderFilledV1` | Records an executed fill on exchange v1. Carries maker, taker, asset ids, amounts, and fee. Primary trade event for v1 markets. | CTFExchange / NegRiskCTFExchange (v1) | | `OrderFilledV2` | Records an executed fill on exchange v2. Adds side, builder, and metadata fields compared to v1. Primary trade event for v2 markets. | CTFExchange / NegRiskCTFExchange (v2) | | `OrdersMatchedV1` | Records the matched order pair around a v1 execution. Explains how taker intent matched maker liquidity. | CTFExchange / NegRiskCTFExchange (v1) | | `OrdersMatchedV2` | Records the matched order pair around a v2 execution. Includes side and tokenId. | CTFExchange / NegRiskCTFExchange (v2) | | `FeeChargedV1` | Records exchange fees on v1. Separates gross trading volume from fee impact. | CTFExchange / NegRiskCTFExchange (v1) | | `FeeChargedV2` | Records exchange fees on v2. Simplified to receiver and amount. | CTFExchange / NegRiskCTFExchange (v2) | | `OrderCancelled` | Records a canceled order by its hash. Useful for understanding liquidity that disappeared before execution. | CTFExchange / NegRiskCTFExchange (v1) | | `OrderPreapproved` | Records a v2 order preapproval. Supports authorization context for orders that can later execute. | CTFExchange / NegRiskCTFExchange (v2) | | `OrderPreapprovalInvalidated` | Records invalidated v2 order preapprovals. Shows when preapproved order intent becomes unusable. | CTFExchange / NegRiskCTFExchange (v2) | | `TokenRegistered` | Maps exchange-tradable token ids (token0, token1) back to a condition id. Required to connect fills to markets. | CTFExchange / NegRiskCTFExchange (v1) | | `TradingPaused` | Records when an exchange operator pauses trading. Signals periods of suspended execution. | CTFExchange / NegRiskCTFExchange (v2) | | `TradingUnpaused` | Records when trading resumes after a pause. | CTFExchange / NegRiskCTFExchange (v2) | ### Redemptions | Event | Role | Contract | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | `Redemption` | Records an AutoRedeemer redemption. Tracks automatic position settlement. | AutoRedeemer | | `BinaryRedemption` | Records a BinaryModule redemption. Tracks binary market position settlement. | AutoRedeemer | | `NegRiskRedemption` | Records a NegRiskModule redemption. Tracks neg-risk position settlement. | AutoRedeemer | | `PositionsRedeemed` | Records collateral-adapter level redemption: initiator, condition, outcome amounts, and payout. Connects resolved conditions to realized collateral flows. | CtfCollateralAdapter / NegRiskCtfCollateralAdapter | ### Collateral operations Shared between CtfCollateralAdapter and NegRiskCtfCollateralAdapter. | Event | Role | Contract | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `CollateralPositionSplit` | Records collateral split into outcome positions. Identifies position creation at the collateral-adapter layer. | CtfCollateralAdapter / NegRiskCtfCollateralAdapter | | `CollateralPositionsMerged` | Records outcome positions merged back at the collateral layer. Identifies position closing before redemption. | CtfCollateralAdapter / NegRiskCtfCollateralAdapter | | `CollateralPositionsConverted` | Records NegRisk collateral conversion between outcome positions. Tracks equivalent exposure moving across neg-risk token structures. | NegRiskCtfCollateralAdapter | ### Conditional token base layer Events emitted by the ConditionalTokens (CTF) contract and the CtfCollateralAdapter. | Event | Role | Contract | | ---------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | `ConditionPreparation` | Records a new condition being prepared. Connects condition id to oracle, question id, and outcome slot count. | ConditionalTokens | | `ConditionResolution` | Records final payout numerators for a condition. Defines how a resolved condition distributes collateral. | ConditionalTokens | | `CtfPositionSplit` | Records collateral split into conditional token positions at the CTF base layer. | ConditionalTokens | | `CtfPositionsMerge` | Records conditional token positions merged at the CTF base layer. | ConditionalTokens | | `CtfPayoutRedemption` | Records redemption of conditional tokens into payout collateral at the CTF base layer. | ConditionalTokens | | `TransferSingle` | Records one ERC-1155 conditional-token transfer. Tracks individual position-token movement between wallets. | ConditionalTokens / PositionManager | | `TransferBatch` | Records multiple ERC-1155 transfers in one transaction. Needed when positions move as a batch. | ConditionalTokens / PositionManager | ### NegRisk adapter | Event | Role | Contract | | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------- | | `MarketPrepared` | Records neg-risk market setup. Connects a market id to its oracle, fee, and preparation metadata. | NegRiskAdapter | | `NegRiskQuestionPrepared` | Records neg-risk question setup. Connects a question to its market id and index. | NegRiskAdapter | | `OutcomeReported` | Records a reported neg-risk outcome. Provides resolution context for neg-risk markets. | NegRiskAdapter | | `NegRiskPositionsConverted` | Records neg-risk position conversion at the adapter layer. Tracks exposure moving across neg-risk token structures. | NegRiskAdapter | ### UMA resolution — UMAAdapter | Event | Role | Contract | | ------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------- | | `UmaAdapterQuestionInitialized` | Records initialization of a UMA-resolved question. | UMAAdapter | | `UmaAdapterQuestionResolved` | Records final resolution of a UMA question. | UMAAdapter | | `UmaAdapterQuestionEmergencyResolved` | Records an emergency override resolution. Signals admin intervention outside normal dispute flow. | UMAAdapter | | `UmaAdapterQuestionFlagged` | Records a question flagged for review. | UMAAdapter | | `UmaAdapterQuestionPaused` | Records a UMA question paused mid-lifecycle. | UMAAdapter | | `UmaAdapterQuestionUnpaused` | Records a paused UMA question resuming. | UMAAdapter | | `UmaAdapterQuestionReset` | Records a question reset back to initialized state. | UMAAdapter | | `UmaAdapterAncillaryDataUpdated` | Records updated ancillary data for a question. | UMAAdapter | ### UMA resolution — UMAOptimisticOracle | Event | Role | Contract | | ------------------------------------------------ | -------------------------------------------------------- | ------------------- | | `UmaOptimisticQuestionInitialized` | Records initialization of an optimistic oracle question. | UMAOptimisticOracle | | `UmaOptimisticQuestionResolved` | Records final resolution via the optimistic oracle. | UMAOptimisticOracle | | `UmaOptimisticQuestionPaused` | Records an optimistic oracle question paused. | UMAOptimisticOracle | | `UmaOptimisticQuestionUnpaused` | Records an optimistic oracle question unpaused. | UMAOptimisticOracle | | `UmaOptimisticQuestionSettled` | Records a question settled after dispute window. | UMAOptimisticOracle | | `UmaOptimisticResolutionDataRequested` | Records a resolution data request being submitted. | UMAOptimisticOracle | | `UmaOptimisticQuestionUpdated` | Records an update to an optimistic oracle question. | UMAOptimisticOracle | | `UmaOptimisticQuestionFlaggedForAdminResolution` | Records a question escalated to admin resolution. | UMAOptimisticOracle | ### Module lifecycle Events from BinaryModule and NegRiskModule. | Event | Role | Contract | | ----------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------- | | `EventPrepared` | Records a new event prepared in a module. | BinaryModule / NegRiskModule | | `ResultReported` | Records a result reported to a module. | BinaryModule / NegRiskModule | | `PositionRedeemed` | Records a position redeemed through a module. | BinaryModule / NegRiskModule | | `ModulePositionsMerged` | Records positions merged within a module. | BinaryModule / NegRiskModule | | `ModulePositionsSplit` | Records positions split within a module. | BinaryModule / NegRiskModule | | `HorizontalMerge` | Records a NegRisk horizontal merge across outcomes. Tracks multi-outcome position collapsing. | NegRiskModule | | `HorizontalSplit` | Records a NegRisk horizontal split across outcomes. | NegRiskModule | | `PositionConverted` | Records a position converted within the NegRisk module. | NegRiskModule | | `ConditionResolved` | Records a condition resolved within the module system. | BinaryModule / NegRiskModule | | `ResolutionPaused` | Records resolution paused for a module. Signals that market resolution is temporarily blocked. | BinaryModule / NegRiskModule | | `ResolutionUnpaused` | Records resolution unpaused. | BinaryModule / NegRiskModule | | `ResolverPaused` | Records the resolver paused at the module level. | BinaryModule / NegRiskModule | | `ResolverUnpaused` | Records the resolver unpaused. | BinaryModule / NegRiskModule | ### Bridge and migration | Event | Role | Contract | | ------------------------------ | --------------------------------------------------------------------- | ---------------------------- | | `BridgePositionMinted` | Records a bridge position minted. Tracks cross-module bridging flows. | BinaryModule / NegRiskModule | | `BridgePositionsBurned` | Records bridge positions burned on the source side. | BinaryModule / NegRiskModule | | `LegacyCollateralSettled` | Records settlement of legacy collateral during migration. | BinaryModule / NegRiskModule | | `MigrationConditionRegistered` | Records a condition registered for migration. | BinaryModule / NegRiskModule | | `MigrationResolved` | Records a migration resolved. | BinaryModule / NegRiskModule | | `PositionMigrated` | Records a position migrated across module versions. | BinaryModule / NegRiskModule | ### Combinatorial markets | Event | Role | Contract | | -------------------------------- | --------------------------------------------------------------------------- | ------------------- | | `CombinatorialConditionPrepared` | Records a new combinatorial condition prepared. | CombinatorialModule | | `Compressed` | Records positions compressed in the combinatorial module. | CombinatorialModule | | `ConvertedToYesBasket` | Records positions converted into a yes basket. | CombinatorialModule | | `Extracted` | Records positions extracted from a combinatorial structure. | CombinatorialModule | | `Injected` | Records positions injected into a combinatorial structure. | CombinatorialModule | | `MergedFromYesBasket` | Records positions merged out of a yes basket. | CombinatorialModule | | `MergedOnCondition` | Records a merge scoped to a single condition within a combinatorial market. | CombinatorialModule | | `SplitOnCondition` | Records a split scoped to a single condition within a combinatorial market. | CombinatorialModule | | `CombinatorialWrapped` | Records positions wrapped in the combinatorial module. | CombinatorialModule | | `CombinatorialUnwrapped` | Records positions unwrapped in the combinatorial module. | CombinatorialModule | ### Wallet factories | Event | Role | Contract | | ---------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `WalletDeployed` | Records a new deposit wallet deployed via DepositWalletFactory. Connects wallet address to owner and implementation. | DepositWalletFactory | | `ProxyCreation` | Records a new Gnosis Safe proxy deployed. Tracks smart-wallet creation for institutional participants. | Gnosis Safe ProxyFactory | # Smart Money Source: https://docs.radion.app/concepts/smart-money How Radion labels wallets by trading performance: every label, its rule, and how the primary style is picked. Smart money means wallets with a strong trading record. Radion looks at every wallet's on-chain history and gives it a set of labels. The labels come from indexed fills only — no self-reporting, no off-chain data. You can use them to find good traders and see how they position in a market. Smart money API endpoints are coming soon. This page explains the data model behind them. ## How it works A background job runs every hour. It reads each wallet's trading stats — realized PnL, volume, trade counts, hold times, entry prices — and applies the rules below. All numbers come from the on-chain trade ledger, with realized PnL computed by average-cost accounting. * A wallet needs at least **10 trades** to get labels. Below that, it stays unlabeled. * A wallet can carry **several labels at once** (a whale can also be a market maker). * One label is picked as the wallet's **primary style** (see below). ## The labels | Label | Meaning | Rule | | ----------------- | ------------------------------- | ---------------------------------------------------------- | | `whale` | Very large total flow | Lifetime volume ≥ \$1M | | `market_maker` | Provides both sides of the book | ≥ 1,000 trades and buy/sell flow within 10% of balance | | `active_trader` | Trades a lot, right now | ≥ 100 trades and traded in the last 30 days | | `buy_and_hold` | Buys and waits | Average hold ≥ 30 days and sells \< half of buys | | `degen` | Many small, fast bets | ≥ 200 trades, average trade \< \$50, average hold \< 1 day | | `high_conviction` | Few markets, big size | ≤ 10 markets and average buy ≥ \$10k | | `contrarian` | Buys long shots | Average entry price ≤ 35¢ | | `value_hunter` | Buys long shots and wins | Average entry price ≤ 30¢ and profitable overall | The thresholds are a starting point. We tune them as the data grows, so do not treat exact values as a stable contract. The label names and their meaning stay stable. ## Primary style When a wallet matches several labels, the most specific behavior wins. The priority order: 1. `market_maker` 2. `whale` 3. `high_conviction` 4. `degen` 5. `active_trader` 6. `buy_and_hold` 7. `value_hunter` 8. `contrarian` A wallet that matches none of the rules has no primary style. ## Definitions | Term | How it is computed | | ------------------- | -------------------------------------------------------------------------- | | Average entry price | Total buy USD ÷ total shares bought | | Average trade size | Total volume ÷ number of trades | | Hold time | Time from opening a position to closing it (sold to zero or resolved) | | Win / loss | A position closed by market resolution with payout (win) or without (loss) | | Realized PnL | Profit and loss from closed positions, average-cost accounting | ## Caveats * Labels refresh every hour. A brand-new wallet shows up after its first pass. * A wallet is not always one person. Polymarket users trade through proxy wallets, and one person can run many wallets. * Labels describe past behavior. They are not financial advice and do not predict future results. # Quickstart Source: https://docs.radion.app/get-started/quickstart Make your first Radion API request in under 5 minutes. Get an API key and make your first API call. ## 1. Get an API key You manage API keys at [radion.app/dashboard](https://radion.app/dashboard). Sign in and create your free key. Keys start with `sk_`. Move to a paid plan to drop the per-second rate limit and raise your monthly quota. ## 2. Check your key Make sure your key works before you build: ```bash cURL theme={null} curl "https://api.radion.app/auth/me" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} const res = await fetch("https://api.radion.app/auth/me", { headers: { "X-API-Key": "YOUR_API_KEY" }, }); const data = await res.json(); console.log(data.plan, data.monthlyUsage); ``` ```python Python theme={null} import httpx client = httpx.Client(headers={"X-API-Key": "YOUR_API_KEY"}) res = client.get("https://api.radion.app/auth/me") data = res.json() print(data["plan"], data["monthlyUsage"]) ``` ```json Response theme={null} { "keyLabel": "production_read", "plan": "free", "callCount": 0, "createdAt": "2026-04-01T12:00:00Z", "lastUsedAt": null, "monthlyUsage": { "used": 0, "limit": 300, "resetsAt": "2026-07-01T00:00:00Z" }, "requestsPerSecond": 5 } ``` If you get this back, your key works. You are ready to make requests. ## 3. Get an event Read one Polymarket event by its id or URL slug. For example, `will-bitcoin-close-above-100k`: ```bash cURL theme={null} curl "https://api.radion.app/v1/polymarket/events/will-bitcoin-close-above-100k" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} const identifier = "will-bitcoin-close-above-100k"; const res = await fetch( `https://api.radion.app/v1/polymarket/events/${identifier}`, { headers: { "X-API-Key": "YOUR_API_KEY" }, } ); const event = await res.json(); console.log(event.title); console.log(event.closed); // false while the event is live console.log(event.markets); // the markets inside the event ``` ```python Python theme={null} import httpx client = httpx.Client( base_url="https://api.radion.app", headers={"X-API-Key": "YOUR_API_KEY"}, ) res = client.get("/v1/polymarket/events/will-bitcoin-close-above-100k") event = res.json() print(event["title"]) print(event["closed"]) # False while the event is live print(event["markets"]) # the markets inside the event ``` ```json Response theme={null} { "id": "12345", "slug": "will-bitcoin-close-above-100k", "title": "Will Bitcoin close above $100k?", "description": "Resolves Yes if BTC closes above $100,000.", "closed": false, "volume": 4820000, "startDate": 1714521600, "endDate": 1735689600, "marketCount": 1, "markets": [ { "conditionId": "0x1234567890abcdef1234567890abcdef12345678", "question": "Will Bitcoin close above $100k?", "status": "open", "outcomes": [ { "label": "Yes", "tokenId": "123456789" }, { "label": "No", "tokenId": "987654321" } ] } ] } ``` ### Reading the response | Field | What it tells you | | ----------- | ------------------------------------------------------ | | `id` | The event id (numeric string) | | `title` | The event title | | `slug` | The event's URL slug | | `closed` | Whether the event is closed | | `startDate` | When the event opened (Unix time, seconds) | | `endDate` | When the event ends (Unix time, seconds) | | `markets` | The markets inside the event, each with its `outcomes` | ## 4. List events by volume Get the top events, ordered by trade volume. Use `limit` to set the page size and `cursor` to page through. ```bash cURL theme={null} curl "https://api.radion.app/v1/polymarket/events?limit=5" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} const res = await fetch("https://api.radion.app/v1/polymarket/events?limit=5", { headers: { "X-API-Key": "YOUR_API_KEY" }, }); const { events, nextCursor } = await res.json(); for (const event of events) { console.log(event.id, event.title, event.volume); } console.log("next page:", nextCursor); ``` ```python Python theme={null} import httpx client = httpx.Client( base_url="https://api.radion.app", headers={"X-API-Key": "YOUR_API_KEY"}, ) res = client.get("/v1/polymarket/events", params={"limit": 5}) body = res.json() for event in body["events"]: print(event["id"], event["title"], event["volume"]) print("next page:", body["nextCursor"]) ``` Events and markets list endpoints return their array under `events` or `markets`, plus a `nextCursor`. Pass `nextCursor` back as the `cursor` query parameter to get the next page. See [Pagination](/api/pagination). ## 5. Search Polymarket Search across events, themes, and profiles by text. Use `limit` for the page size and `page` to move through results. ```bash cURL theme={null} curl "https://api.radion.app/v1/polymarket/search?q=bitcoin&limit=5" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} const res = await fetch( "https://api.radion.app/v1/polymarket/search?q=bitcoin&limit=5", { headers: { "X-API-Key": "YOUR_API_KEY" }, } ); const { events } = await res.json(); for (const event of events) { console.log(event.title, event.slug); } ``` ```python Python theme={null} import httpx client = httpx.Client( base_url="https://api.radion.app", headers={"X-API-Key": "YOUR_API_KEY"}, ) res = client.get("/v1/polymarket/search", params={"q": "bitcoin", "limit": 5}) for event in res.json()["events"]: print(event["title"], event["slug"]) ``` ## Next steps Learn how Radion reads Polymarket contract events. Full endpoint reference with schemas and examples. Connect Radion to Claude, Cursor, and other AI tools. Stream trades and market events live. # Backfill full market history Source: https://docs.radion.app/guides/backfill-market-history Load a past block range of decoded Polymarket events into your own Postgres or Redpanda. Load a market's full history into your own database. [Indexing & Backfilling](/indexing/overview) writes Radion's decoded Polymarket events straight into your Postgres or pushes them to your Redpanda topics. You do not run an indexer yourself. Indexing & Backfilling is on the **Enterprise** plan, on demand. Radion sets up the pipeline for you from the scope you choose below. ## What you get Decoded, typed events across Polymarket — trades, positions, holders, and resolutions. The full event list is in [Onchain Data](/concepts/onchain-data). ## 1. Choose the scope Pick what you receive, so you store only what you need: | Option | What it does | | ------------ | ----------------------------------------------- | | Contracts | Keep only certain Polymarket contract addresses | | Event topics | Keep only the event types you choose | | Block range | Set the start and end of the backfill | For one market's full history, scope to its contracts and set the block range from the market's first block to now. ## 2. Pick a destination | Destination | What happens | | ------------ | -------------------------------------------------------------- | | **Postgres** | Radion makes the schema and writes rows, with upsert and dedup | | **Redpanda** | Radion pushes decoded events to your topics, in order | ## 3. How the backfill runs * **Backfill** loads the past block range in one pass. If it stops, it resumes where it left off instead of starting over. * **Live sync** takes over once the backfill reaches the tip and keeps your data current as new blocks land. You can run backfill and live sync at the same time with no double rows. ## What Radion promises * **Reorg-safe.** When the chain rewrites a block, Radion fixes the affected rows so your data lands on the canonical chain. * **No duplicates.** Every row is keyed by `(chain_id, block_number, log_index)`. Re-sending a row overwrites it. * **In order.** Events arrive in block and log order per stream. ## Get access Choose **Get Enterprise Plan** on the [landing page](https://www.radion.app) or in the [dashboard](https://radion.app/dashboard). Tell Radion the scope (contracts, topics, block range) and destination you want, and the pipeline gets set up for you. ## Next steps The full pipeline, promises, and scope options. Every decoded event you can receive. # Copy-trade a whale Source: https://docs.radion.app/guides/copy-trade-a-whale Watch a wallet's fills live and mirror its Polymarket trades with the trading channel. Pick a wallet you trust and follow its trades in real time. The [`trading`](/websockets/channels/trading) channel sends every confirmed fill. Filter it by `wallets` and you get only that wallet's order flow, decoded and ready to act on. ## What you need * An API key from [radion.app/dashboard](https://radion.app/dashboard). * The wallet addresses you want to follow (`0x…`). * The [`@radion-app/sdk`](https://github.com/radion-app/radion-typescript) package. ## Follow the wallet Subscribe to `trading` with a `wallets` filter. The SDK opens the socket, sends the `X-API-Key` header, and resubscribes after a reconnect. ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); const whales = ["0xWHALE_ADDRESS"]; radion.realtime.onChannel("trading", (event) => { const fill = event.data; if (fill.type !== "order_filled_v2") return; const side = fill.side === 0 ? "buy" : "sell"; console.log(`whale ${side} on token ${fill.tokenId}`); // place your own order here }); radion.realtime.subscribe({ id: "whale-trades", channel: "trading", filters: { wallets: whales }, }); await radion.realtime.connect(); ``` ```python Python theme={null} import asyncio from radion import Radion, ChannelFilters, Subscription radion = Radion(api_key="YOUR_API_KEY") whales = ["0xWHALE_ADDRESS"] @radion.realtime.on_channel("trading") async def on_fill(event): fill = event.data if fill["type"] != "order_filled_v2": return side = "buy" if fill["side"] == 0 else "sell" print(f"whale {side} on token {fill['tokenId']}") # place your own order here async def main(): await radion.realtime.subscribe( Subscription(id="whale-trades", channel="trading", filters=ChannelFilters(wallets=whales)) ) await radion.realtime.connect() asyncio.run(main()) ``` The `wallets` filter keeps events where a listed address is the maker or taker. Read `side` (`0` = buy, `1` = sell), `tokenId`, and the filled amounts to size your own trade. ## Front-run the confirmation Want to act before the fill lands in a block? Add a second [`trading`](/websockets/channels/trading) subscription with `confirmed: false` to get the pending feed. Pending frames are guesses, so treat them as a hint and reconcile against the confirmed `trading` event by `transaction_hash`. See [Mempool front-run alert](/guides/mempool-front-run-alert) for the full pattern. ## Notes * Multiple wallets in one subscription — the filter matches any of them. * Add `min_usd` to skip small fills and copy only meaningful size. * The `trading` channel is order flow only. For a wallet's full activity (transfers, splits, redemptions), use the [`wallets`](/websockets/channels/overview) filter channel. ## Next steps Every order-flow event and its payload. Runnable `copytrade` example you can clone. # Mempool front-run alert Source: https://docs.radion.app/guides/mempool-front-run-alert See large pending Polymarket buys before they land, then reconcile against the confirmed fill. Get an alert the moment a big order hits the mempool, before it is in a block. Subscribe to [`trading`](/websockets/channels/trading) with `confirmed: false` to stream pending exchange transactions. Pair it with the confirmed `trading` feed to know when the trade actually lands. Mempool messages are guesses, not facts. A pending transaction can be dropped, replaced, reordered, or reverted. Never treat a mempool alert as a settled trade — always confirm against `trading`. ## What you need * An API key from [radion.app/dashboard](https://radion.app/dashboard). * A size threshold in whole USD (for example, `10000`). ## Alert on pending, confirm on landing Subscribe to both feeds. Alert on the pending frame, then match it to the confirmed event by `transaction_hash`. Both arrive on the `trading` channel, so you tell them apart by the `confirmed` flag on the frame. ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); const pending = new Map(); radion.realtime.onChannel("trading", (event) => { if (!event.confirmed) { const call = event.data.call; if (!call?.notional_usd || call.notional_usd < 10_000) return; pending.set(event.data.transaction_hash, call.notional_usd); console.log( `pending $${call.notional_usd} order — ${event.data.transaction_hash}` ); return; } const hash = event.data.transactionHash; if (pending.has(hash)) { console.log(`landed — ${hash}`); pending.delete(hash); } }); radion.realtime.subscribe({ id: "pending", channel: "trading", confirmed: false, }); radion.realtime.subscribe({ id: "confirmed", channel: "trading", confirmed: true, filters: { min_usd: 10_000 }, }); await radion.realtime.connect(); ``` ```python Python theme={null} import asyncio from radion import Radion, ChannelFilters, Subscription radion = Radion(api_key="YOUR_API_KEY") pending = {} @radion.realtime.on_channel("trading") async def on_trading(event): if not event.confirmed: call = event.data.get("call") if not call or (call.get("notional_usd") or 0) < 10_000: return pending[event.data["transaction_hash"]] = call["notional_usd"] print(f"pending ${call['notional_usd']} order — {event.data['transaction_hash']}") return tx_hash = event.data.get("transactionHash") if tx_hash in pending: print(f"landed — {tx_hash}") pending.pop(tx_hash) async def main(): await radion.realtime.subscribe(Subscription(id="pending", channel="trading", confirmed=False)) await radion.realtime.subscribe( Subscription(id="confirmed", channel="trading", confirmed=True, filters=ChannelFilters(min_usd=10_000)) ) await radion.realtime.connect() asyncio.run(main()) ``` ## How the size filter differs * On the confirmed `trading` feed, `min_usd` measures the **filled** amount. * On the pending feed (`confirmed: false`), the `call.notional_usd` field is the pending **order** size. It may only partly fill or never land. So filter the pending side in your own code (as above), and let `min_usd` narrow the confirmed side. ## Read the pending frame The `call` object holds decoded calldata: `method`, `market_ids`, `token_ids`, `wallets`, `notional_usd`, and an `orders` array (each order is `{ maker, taker, token_id, side, maker_amount, taker_amount }`). `call` is `null` when the calldata is not a known Polymarket call. See [Mempool](/websockets/mempool) for the full frame and filter reach. ## Next steps Frame shape, the `call` object, and filter limits. Mirror a wallet's trades in real time. # Build a prices dashboard Source: https://docs.radion.app/guides/prices-dashboard Load market prices over REST, then keep them live with the trading channel. Show live Polymarket odds on a board. Load the first snapshot over the [REST API](/api/overview), then stream fills from the [`trading`](/websockets/channels/trading) channel to keep each price fresh. ## What you need * An API key from [radion.app/dashboard](https://radion.app/dashboard). * The token ids you want to track (each outcome has one). ## 1. Load the snapshot Read the markets you care about, then pull a price per outcome token. Use `midpoint` for a fair mid, `price` for the best bid or ask, and `spread` for the gap. ```bash cURL theme={null} curl "https://api.radion.app/v1/polymarket/midpoint?token_id=123456789" \ -H "X-API-Key: YOUR_API_KEY" curl "https://api.radion.app/v1/polymarket/spread?token_id=123456789" \ -H "X-API-Key: YOUR_API_KEY" ``` ```typescript TypeScript theme={null} const headers = { "X-API-Key": "YOUR_API_KEY" }; const base = "https://api.radion.app/v1/polymarket"; async function snapshot(tokenId: string) { const [mid, spread] = await Promise.all([ fetch(`${base}/midpoint?token_id=${tokenId}`, { headers }).then((r) => r.json() ), fetch(`${base}/spread?token_id=${tokenId}`, { headers }).then((r) => r.json() ), ]); return { midpoint: mid.midpoint, spread: spread.spread }; } ``` ```python Python theme={null} import httpx client = httpx.Client( base_url="https://api.radion.app", headers={"X-API-Key": "YOUR_API_KEY"}, ) def snapshot(token_id: str): mid = client.get("/v1/polymarket/midpoint", params={"token_id": token_id}).json() spread = client.get("/v1/polymarket/spread", params={"token_id": token_id}).json() return {"midpoint": mid["midpoint"], "spread": spread["spread"]} ``` Render the board from these values. For the best bid or ask on one side, call `price` with `token_id` and `side`. ## 2. Keep it live Subscribe to `trading` filtered by your token ids. Update the matching cell on every fill instead of polling. ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); const tokenIds = ["123456789", "987654321"]; radion.realtime.onChannel("trading", (event) => { const fill = event.data; if (fill.type !== "order_filled_v2") return; updateBoard(fill.tokenId, fill); // your render function }); radion.realtime.subscribe({ id: "board", channel: "trading", filters: { token_ids: tokenIds }, }); await radion.realtime.connect(); ``` ```python Python theme={null} import asyncio from radion import Radion, ChannelFilters, Subscription radion = Radion(api_key="YOUR_API_KEY") token_ids = ["123456789", "987654321"] @radion.realtime.on_channel("trading") async def on_fill(event): fill = event.data if fill["type"] != "order_filled_v2": return update_board(fill["tokenId"], fill) # your render function async def main(): await radion.realtime.subscribe( Subscription(id="board", channel="trading", filters=ChannelFilters(token_ids=token_ids)) ) await radion.realtime.connect() asyncio.run(main()) ``` ## Tips * Refresh the REST snapshot on a slow timer (say every minute) to correct any drift; let the WebSocket handle the fast updates. * To list many markets at once, page [`/v1/polymarket/markets`](/api/overview) with `limit` and `cursor`. * Need full depth, not just top of book? Use [`/v1/polymarket/orderbook`](/api/overview). ## Next steps Endpoints, schemas, and query parameters. Live fill payloads to drive your board. # Resolution + oracle tracker Source: https://docs.radion.app/guides/resolution-oracle-tracker Follow a market from UMA oracle proposal to final settlement with the oracle and resolution channels. Know the moment a market settles. The [`oracle`](/websockets/channels/oracle) channel streams the UMA lifecycle — question started, resolved, settled — and the [`resolution`](/websockets/channels/resolution) channel streams the on-chain settlement that pays out. Subscribe to both to track a market end to end. ## What you need * An API key from [radion.app/dashboard](https://radion.app/dashboard). * The market condition ids you want to watch (`bytes32` hex). ## The two feeds * **`oracle`** — UMA reporting: `uma_optimistic_question_settled`, `uma_adapter_question_resolved`, plus pauses, disputes, and emergency overrides. * **`resolution`** — the settlement itself: `condition_resolution` saves the final payout numerators that set how collateral splits. Filter both channels by `market_ids`. ## Track a market ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); const markets = ["0xCONDITION_ID"]; radion.realtime.onChannel("oracle", (event) => { const data = event.data; if (data.type === "uma_optimistic_question_settled") { console.log(`UMA settled ${data.questionID} at ${data.settledPrice}`); } }); radion.realtime.onChannel("resolution", (event) => { const data = event.data; if (data.type === "condition_resolution") { console.log( `resolved ${data.conditionId} payouts ${data.payoutNumerators}` ); // notify holders, close positions, redeem } }); radion.realtime.subscribe({ id: "oracle", channel: "oracle", filters: { market_ids: markets }, }); radion.realtime.subscribe({ id: "resolution", channel: "resolution", filters: { market_ids: markets }, }); await radion.realtime.connect(); ``` ```python Python theme={null} import asyncio from radion import Radion, ChannelFilters, Subscription radion = Radion(api_key="YOUR_API_KEY") markets = ["0xCONDITION_ID"] @radion.realtime.on_channel("oracle") async def on_oracle(event): data = event.data if data["type"] == "uma_optimistic_question_settled": print(f"UMA settled {data['questionID']} at {data['settledPrice']}") @radion.realtime.on_channel("resolution") async def on_resolution(event): data = event.data if data["type"] == "condition_resolution": print(f"resolved {data['conditionId']} payouts {data['payoutNumerators']}") # notify holders, close positions, redeem async def main(): await radion.realtime.subscribe( Subscription(id="oracle", channel="oracle", filters=ChannelFilters(market_ids=markets)) ) await radion.realtime.subscribe( Subscription(id="resolution", channel="resolution", filters=ChannelFilters(market_ids=markets)) ) await radion.realtime.connect() asyncio.run(main()) ``` ## Reading the result * `payoutNumerators` on `condition_resolution` sets the payout per outcome slot. `["0x1", "0x0"]` means the first outcome won. * `settledPrice` on the UMA events is a signed value; `"-1"` marks a disputed or unresolved price. * Watch `resolution_paused` / `resolver_paused` — settlement can stall before it finishes. Both channels accept only `market_ids`. `wallets`, `token_ids`, and `min_usd` are ignored. ## Front-run the settlement Subscribe to `oracle` or `resolution` with `confirmed: false` to see the resolving transaction while it is still pending. Pending frames are guesses — reconcile against the confirmed event by `transaction_hash`. ## Next steps Every UMA lifecycle event. Settlement events and payout numerators. # Radion Docs Source: https://docs.radion.app/index Polymarket data from Radion's own on-chain indexer. Read markets, events, search, and order books. Radion is an API for Polymarket data. We run our own on-chain indexer. We read Polymarket's smart contracts straight from the blockchain and turn the raw events into clean, ready-to-use data. You get markets, events, search, and orderbooks — all from one place. Get your API key and make your first request in under 5 minutes. Learn how Radion reads Polymarket contract events and mempool transactions. Connect Radion to Claude, Cursor, and other AI tools via MCP. Endpoints, schemas, authentication, and rate limits. ## How it works We watch Polymarket's smart contracts on Polygon. Every trade, position change, and market event is read straight from the blockchain. We turn the raw chain events into a tidy read model: markets, events, search, and order books. No raw logs to parse yourself. Pull the data with the REST API, or stream it live over WebSockets. Use it in your tools, dashboards, or bots. # Indexing & Backfilling Source: https://docs.radion.app/indexing/overview Sync Radion's decoded Polymarket event stream into your own Postgres or Redpanda — historical backfills and live updates, scoped to what you need. Indexing & Backfilling sends Radion's decoded Polymarket events into your own systems. You get the same data that powers Radion — markets, trades, positions, and resolutions — written straight into your database or pushed to your topics. You do not have to run an indexer yourself. ## How the pipeline works Radion reads Polygon logs through an optimized ingestion approach that goes beyond traditional JSON-RPC, making us faster than our competitors. It decodes them and pushes them through Redpanda into a Postgres read-model. The same decoded stream is what we deliver to you. ## What you get Decoded, typed events from across Polymarket: | Group | Includes | | --------------- | ----------------------------------------------------------------- | | **Trades** | Order fills, matches, and fees across exchange versions | | **Positions** | Conditional-token splits, merges, redemptions, and conversions | | **Holders** | ERC-1155 transfers, turned into balances per holder and position | | **Resolutions** | Condition resolutions, neg-risk outcomes, and the UMA oracle flow | The full event list is in [Onchain Data](/concepts/onchain-data). ## Backfill and live sync * **Backfill** loads a past block range into your destination in one pass. If a backfill stops, it picks up where it left off instead of starting over. * **Live sync** takes over once the backfill reaches the tip. It keeps your destination up to date as new blocks land. ## What we promise * **Reorg-safe.** When the chain rewrites a block, we fix the affected rows for you. Your data lands on the canonical chain. * **No duplicates.** Every row is keyed by its on-chain spot — `(chain_id, block_number, log_index)`. Sending a row again just overwrites it, so backfill and live sync can overlap with no doubles. * **In order.** Events arrive in block and log order per stream. ## Scope Pick what you receive, so you only store what you need: | Option | What it does | | ------------ | ----------------------------------------------- | | Contracts | Keep only certain Polymarket contract addresses | | Event topics | Keep only the event types you choose | | Block range | Set the start and end of the backfill | ## Destinations | Destination | What happens | | ------------ | -------------------------------------------------------------- | | **Postgres** | Radion makes the schema and writes rows, with upsert and dedup | | **Redpanda** | Radion pushes decoded events to your topics, in order | ## Get access Indexing & Backfilling is on the **Enterprise** plan, on demand. Choose **Get Enterprise Plan** on the [landing page](https://www.radion.app) or in the [dashboard](https://radion.app/dashboard) to reach us through the contact form. Tell us the scope and destination you want, and we set up the pipeline for you. # Authentication Source: https://docs.radion.app/rpc/authentication How to authenticate RPC requests with a paid Radion API key. RPC uses the same API keys as the REST API. Send your key in the `X-API-Key` header on every request. ```bash title="Authenticated RPC request" theme={null} curl "https://rpc.radion.app" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' ``` Use the key value as it is. Do not put `Bearer` in front of it. ## Paid plans only RPC needs a **paid** plan: `starter`, `pro`, or `enterprise`. A valid key on the **free** plan is rejected. You can check the plan on a key with `GET /auth/me` on the REST API. See [REST Authentication](/api/authentication). ## What errors mean | Case | Response | | ------------------------------------- | ------------------ | | You sent no key | `401 Unauthorized` | | The key is wrong or turned off | `401 Unauthorized` | | The key is valid but on the free plan | `403 Forbidden` | Errors come back as JSON-RPC error objects. ```json title="Free plan rejected" theme={null} { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "RPC requires a paid plan" } } ``` ## Good habits * Keep secret keys (`sk_...`) on your server, never in a browser. * Use a different key for each app or environment when you can. * Make a new key if you think an old one leaked. # MCP Source: https://docs.radion.app/rpc/mcp Connect an AI client to the Radion RPC node over MCP. Read-only, authenticated with your paid Radion API key. The RPC node exposes an [MCP](https://modelcontextprotocol.io) endpoint at `https://rpc.radion.app/mcp`. An AI client (Claude Code, Codex, Cursor, and others) can call the node with structured, **read-only** tools instead of hand-written JSON-RPC. It is the same node and the same key as the [RPC](/rpc/overview). MCP is a **paid** feature; the free plan gets a `403 Forbidden`. ## Endpoint ``` https://rpc.radion.app/mcp ``` Transport is **SSE** (Server-Sent Events) over HTTPS. Authenticate with your API key in the `X-API-Key` header, same as the RPC. ## Connect a client Add the endpoint to your MCP client config. Set your API key as a header. ```json title="Claude / Cursor mcp config" theme={null} { "mcpServers": { "radion-rpc": { "url": "https://rpc.radion.app/mcp", "headers": { "X-API-Key": "YOUR_API_KEY" } } } } ``` Once connected, the client lists the node's tools and can call them on its own. ## What it exposes Read-only tools over the Polygon node, including: * **Ethereum standard** — `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getBalance`, `eth_call`, `eth_getLogs`, `eth_getTransactionReceipt`. * **Erigon** — `erigon_getBlockByTimestamp`, `erigon_getBalanceChangesInBlock`, and more. * **Otterscan tracing** — `ots_getInternalOperations`, `ots_traceTransaction`, `ots_searchTransactionsAfter`. No write tools. Nothing that submits a transaction or changes state. ## Limits * `eth_getLogs` block ranges are capped, same as the [RPC](/rpc/methods#limits-on-eth_getlogs). Keep ranges small and filter by address and topic. * Heavy tracing tools may be capped or restricted on lower tiers. ## Get access MCP is on the **Starter**, **Pro**, and **Enterprise** plans. Use the same key as the REST API and RPC. See [Authentication](/rpc/authentication). Looking for Polymarket market and orderbook data over MCP instead of the raw node? See the [Radion MCP server](/ai/mcp). # Methods Source: https://docs.radion.app/rpc/methods Which Polygon JSON-RPC methods the Radion RPC endpoint supports. Radion RPC speaks standard Polygon JSON-RPC. Send a `POST` with a JSON body: `jsonrpc`, `id`, `method`, and `params`. ## Common methods | Method | What it does | | --------------------------- | ------------------------------------- | | `eth_blockNumber` | Latest block number | | `eth_getBlockByNumber` | A block and its transactions | | `eth_getBalance` | Native balance of an address | | `eth_call` | Read a contract without a transaction | | `eth_getLogs` | Event logs for a filter | | `eth_getTransactionReceipt` | Receipt for a transaction hash | | `eth_chainId` | Chain id (`137` for Polygon) | Only read methods are open. Methods that submit transactions, like `eth_sendRawTransaction`, are not part of this endpoint. ## Batch requests Send an array of calls in one request. You get an array of results back, in the same order. ```bash title="Batch" theme={null} curl "https://rpc.radion.app" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '[ {"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}, {"jsonrpc":"2.0","id":2,"method":"eth_chainId","params":[]} ]' ``` ## Limits on `eth_getLogs` Large log queries are capped to keep the node fast. Keep block ranges small and filter by address and topic. If a query is too wide, split it into smaller ranges. # RPC Source: https://docs.radion.app/rpc/overview A fast Polygon JSON-RPC endpoint, authenticated with your Radion API key. Available on paid plans. Radion RPC gives you a Polygon JSON-RPC endpoint at `https://rpc.radion.app`. It is the same node infrastructure that powers Radion's ingestion. You send standard JSON-RPC calls and authenticate with your Radion API key. RPC is a **paid** feature. The free plan cannot use it. See [Get access](#get-access). ## Base URL ``` https://rpc.radion.app ``` You call it like any Polygon node. Send a JSON-RPC request over HTTP `POST`, with your API key in the `X-API-Key` header. ```bash title="eth_blockNumber" theme={null} curl "https://rpc.radion.app" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' ``` ```json title="Response" theme={null} { "jsonrpc": "2.0", "id": 1, "result": "0x4a8f2c1" } ``` ## Use it with a client Point any Ethereum client at the endpoint. Set your API key as a header. ```ts title="viem" wrap theme={null} import { createPublicClient, http } from "viem"; import { polygon } from "viem/chains"; const client = createPublicClient({ chain: polygon, transport: http("https://rpc.radion.app", { fetchOptions: { headers: { "X-API-Key": "YOUR_API_KEY" } }, }), }); const block = await client.getBlockNumber(); ``` ```ts title="ethers" wrap theme={null} import { JsonRpcProvider, FetchRequest } from "ethers"; const request = new FetchRequest("https://rpc.radion.app"); request.setHeader("X-API-Key", "YOUR_API_KEY"); const provider = new JsonRpcProvider(request); const block = await provider.getBlockNumber(); ``` ## What you can call Standard Polygon JSON-RPC methods, such as `eth_blockNumber`, `eth_getBalance`, `eth_call`, `eth_getLogs`, and `eth_getTransactionReceipt`. Read [Methods](/rpc/methods) for what is allowed and what is capped. ## Get access RPC is on the **Starter**, **Pro**, and **Enterprise** plans. The free plan gets a `403 Forbidden`. Upgrade in the [dashboard](https://radion.app/dashboard), then use the same API key you use for the REST API. See [Authentication](/rpc/authentication). # Authentication Source: https://docs.radion.app/sdks/authentication Authenticate the Radion SDK with your API key. The SDK logs in with one Radion API key. The SDK sends it as the `X-API-Key` header when it opens the WebSocket. Keys start with `sk_`. You make and manage them in the [dashboard](https://radion.app/dashboard). Pass the key when you build the client: ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); ``` ```python Python theme={null} import os from radion import Radion radion = Radion(api_key=os.getenv("RADION_API_KEY")) ``` ```rust Rust theme={null} use radion_sdk::Radion; let radion = Radion::builder() .api_key(std::env::var("RADION_API_KEY")?) .build()?; ``` Never put keys in source control or in URLs. Read them from an environment variable (`RADION_API_KEY`) or your secrets manager. The SDK only sends the key in the upgrade header. It never puts the key in the query string. If the key is missing or empty, the SDK fails right away with a `RadionConnectionError` (Rust: `RadionError::Connection`), before any network call. Your key also sets the [rate limits](/websockets/rate-limits) for the connection. If the key is revoked while you are connected, the server closes the connection with a `key_revoked` [error](/sdks/realtime/errors). # Configuration Source: https://docs.radion.app/sdks/configuration Configure the Radion client — API key, endpoints, and realtime tuning. The `Radion` client takes a few options. Only `apiKey` is required. | Option | Type | Default | Description | | --------- | -------- | ------------------------- | --------------------------------------------- | | `apiKey` | `string` | — | **Required.** Sent as the `X-API-Key` header. | | `baseUrl` | `string` | `https://api.radion.app` | The base URL for the Radion API. | | `wsUrl` | `string` | `wss://api.radion.app/ws` | Use your own realtime endpoint. | ```typescript TypeScript theme={null} const radion = new Radion({ apiKey: process.env.RADION_API_KEY, // wsUrl: "wss://api.radion.app/ws", realtime: { reconnect: { initialDelayMs: 500, maxDelayMs: 30_000 }, heartbeat: { intervalMs: 15_000, timeoutMs: 10_000 }, }, }); ``` ```python Python theme={null} radion = Radion( api_key="sk_...", # ws_url="wss://api.radion.app/ws", reconnect=True, heartbeat=True, heartbeat_interval=15.0, heartbeat_timeout=10.0, ) ``` ```rust Rust theme={null} use radion_sdk::Radion; use radion_sdk::realtime::{HeartbeatOptions, ReconnectOptions, RealtimeClient, RealtimeOptions}; use std::time::Duration; // The top-level builder takes the API key and endpoints: let radion = Radion::builder() .api_key("sk_...") // .ws_url("wss://api.radion.app/ws") .build()?; // Realtime tuning lives on RealtimeOptions (standalone client): let client = RealtimeClient::new( RealtimeOptions::new("sk_...") .reconnect(ReconnectOptions { initial_delay: Duration::from_millis(500), max_delay: Duration::from_secs(30), factor: 2.0, jitter: 0.2, }) .heartbeat(HeartbeatOptions { interval: Duration::from_secs(15), timeout: Duration::from_secs(10), }), ); ``` ## Realtime tuning To turn off reconnect or heartbeat, pass `false` (TypeScript) or `False` (Python). In Rust, build a `RealtimeClient` with `RealtimeOptions` and call `.disable_reconnect()` or `.disable_heartbeat()`. See [Reconnect & heartbeats](/sdks/realtime/reconnect) to learn how they work. # Installation Source: https://docs.radion.app/sdks/installation Install the Radion SDK for TypeScript, Python, or Rust. ## TypeScript [`@radion-app/sdk`](https://www.npmjs.com/package/@radion-app/sdk) is on npm. It ships both ESM and CommonJS builds and brings its own types. It has few dependencies — just `ws` and `zod`. ```bash pnpm theme={null} pnpm add @radion-app/sdk ``` ```bash npm theme={null} npm install @radion-app/sdk ``` ```bash yarn theme={null} yarn add @radion-app/sdk ``` ```bash bun theme={null} bun add @radion-app/sdk ``` **Needs** Node.js 18 or later (for the global `fetch` and a modern `ws`). ## Python [`radion-sdk`](https://pypi.org/project/radion-sdk/) is on PyPI (you import it as `radion`). It is async-first and has few dependencies — just `websockets` and `msgspec`. ```bash uv theme={null} uv add radion-sdk ``` ```bash pip theme={null} pip install radion-sdk ``` **Needs** Python 3.10 or later. ## Rust [`radion-sdk`](https://crates.io/crates/radion-sdk) is on crates.io (you import it as `radion_sdk`). It is async-first and built on `tokio`. It uses cargo features, so you build only the parts you use. ```bash theme={null} cargo add radion-sdk ``` The `realtime` and `rustls` features are on by default. You can change the TLS backend or turn on `tracing`: ```bash theme={null} cargo add radion-sdk --no-default-features --features realtime,native-tls,tracing ``` **Needs** Rust 1.85 or later. ## Verify ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); console.log(radion.realtime.connected); // false until connect() ``` ```python Python theme={null} from radion import Radion radion = Radion(api_key="sk_...") print(radion.realtime.connected) # False until connect() ``` ```rust Rust theme={null} use radion_sdk::Radion; let radion = Radion::builder().api_key("sk_...").build()?; println!("{}", radion.realtime.connected()); // false until connect() ``` Next step: [Authentication](/sdks/authentication). # SDKs Source: https://docs.radion.app/sdks/overview One typed Radion client per language, built to grow. The Radion SDKs give you one typed client for each language. You build it once with your API key, then use it to talk to Radion. Right now the client does realtime. It opens the connection, reconnects after a drop, sends heartbeats, and puts your subscriptions back for you. More of the platform will come later. ## The client ```typescript TypeScript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); // radion.realtime — WebSocket client ``` ```python Python theme={null} from radion import Radion radion = Radion(api_key="sk_...") # radion.realtime — WebSocket client ``` ```rust Rust theme={null} use radion_sdk::Radion; let radion = Radion::builder().api_key("sk_...").build()?; // radion.realtime — WebSocket client ``` The realtime client is also exported on its own (`RealtimeClient`). Use it if you only need the stream. ## Choose a language `@radion-app/sdk` — ESM + CJS, ships its own types. Node.js 18+. `radion-sdk` — async-first, sync or async handlers. Python 3.10+. `radion-sdk` — async-first, stream-based, cargo features. Rust 1.85+. ## How it relates to the raw API The SDKs use the same protocol shown under [WebSockets](/websockets/overview). Same channels, same filters, same frames. If you want to handle the socket yourself, that reference still works. The SDK just adds the reconnect, heartbeat, and resubscribe code you would otherwise write by hand. ## Next steps Add the package to your project. Pass your API key. Client options and realtime tuning. Connect, subscribe, and handle events. # Python Source: https://docs.radion.app/sdks/python The radion Python package — async-first, fully typed. [`radion-sdk`](https://pypi.org/project/radion-sdk/) is the Python SDK (you import it as `radion`). It is async-first and built on `asyncio`. It has few dependencies — just `websockets` and `msgspec`. Needs Python 3.10+. ```bash theme={null} uv add radion-sdk # or: pip install radion-sdk ``` ## Usage ```python theme={null} import asyncio import os from radion import Radion, Subscription async def main() -> None: radion = Radion(api_key=os.getenv("RADION_API_KEY")) await radion.realtime.connect() await radion.realtime.subscribe(Subscription(id="trading", channel="trading")) @radion.realtime.on_channel("trading") async def handle(event): print(event.id, event.channel, event.data) await asyncio.sleep(60) await radion.realtime.close() asyncio.run(main()) ``` ## Handlers `on_channel`, `on_any_channel`, and `on_lifecycle` are decorators. Your handlers can be **sync or async** — the SDK awaits both for you. Use `on_channel(name)` for one channel, `on_any_channel()` for every channel event, or `on_lifecycle(event)` for a lifecycle event (`open`, `close`, `reconnect`, `error`). ```python theme={null} @radion.realtime.on_any_channel() async def on_any(event): print(event.channel, event.data) @radion.realtime.on_lifecycle("error") def on_error(err): # sync is fine too print(err) ``` ## Standalone realtime client ```python theme={null} from radion import RealtimeClient client = RealtimeClient(api_key="sk_...") await client.connect() ``` ## Exports | Export | Kind | | ----------------------------------------------------------- | -------- | | `Radion`, `RealtimeClient` | classes | | `RadionError`, `RadionConnectionError`, `RadionServerError` | errors | | `Subscription`, `ChannelFilters`, `ChannelEvent` | models | | `CHANNELS`, `Channel`, `is_channel` | channels | | `RadionConfig`, `DEFAULT_BASE_URL`, `DEFAULT_WS_URL` | config | `ChannelEvent` is generic over its payload (`ChannelEvent[T]`), and defaults to `Any`. The package ships a `py.typed` marker, so types work right away. # Errors Source: https://docs.radion.app/sdks/realtime/errors The Radion SDK error hierarchy and server error codes. The SDK groups its errors in a small tree. TypeScript and Python use the same set of classes. Rust uses one `RadionError` enum with matching variants. | Type / variant | Raised when | | --------------------------------------------------- | -------------------------------------------------------------------------------- | | `RadionError` | The base class for every SDK error (the enum itself in Rust). | | `RadionConnectionError` / `RadionError::Connection` | You used the client in a bad state (for example, no key, or used after `close`). | | `RadionServerError` / `RadionError::Server` | The server sent an `error` frame. Holds `code`, `channel`, and `id`. | | `RadionError::Transport` (Rust) | The WebSocket transport under the hood failed. | The SDK does not throw server errors or handler errors from a method. It sends them to the `error` lifecycle event instead. ```typescript TypeScript theme={null} import { RadionConnectionError, RadionServerError } from "@radion-app/sdk"; radion.realtime.onLifecycle("error", (err) => { if (err instanceof RadionServerError) { console.error("server error", err.code, err.channel, err.id); } else if (err instanceof RadionConnectionError) { console.error("connection error", err.message); } }); ``` ```python Python theme={null} from radion import RadionConnectionError, RadionServerError @radion.realtime.on_lifecycle("error") async def errored(err): if isinstance(err, RadionServerError): print("server error", err.code, err.channel, err.id) elif isinstance(err, RadionConnectionError): print("connection error", err) ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::LifecycleEvent; use radion_sdk::RadionError; let mut lifecycle = radion.realtime.lifecycle(); while let Some(event) = lifecycle.next().await { if let LifecycleEvent::Error(err) = event { match err { RadionError::Server { code, channel, id, .. } => { eprintln!("server error {code:?} {channel:?} {id:?}"); } RadionError::Connection(message) => eprintln!("connection error {message}"), RadionError::Transport(message) => eprintln!("transport error {message}"), _ => {} } } } ``` ## Server error codes `RadionServerError.code` matches the server's [error frames](/websockets/frames#error-frames). Common codes: | Code | Meaning | | --------------------- | ---------------------------------------------------------------------------- | | `unknown_channel` | The channel name is unknown, or a required filter is missing. | | `subscription_limit` | You went over your plan's subscription limit for the connection. | | `lagged` | You read too slowly and fell behind the buffer, so some events were dropped. | | `key_revoked` | The API key was revoked, so the connection is closed. | | `revalidation_failed` | The key could not be checked again, so the connection is closed. | Treat `key_revoked` and `revalidation_failed` as fatal: call `close()` and stop. For normal drops, the client reconnects on its own. # Events Source: https://docs.radion.app/sdks/realtime/events Handle channel events, the event wildcard, and connection lifecycle events. Add handlers with one of three methods, based on what you want to catch: * **`onChannel` / `on_channel`** — events on one channel. * **`onAnyChannel` / `on_any_channel`** — every channel event, on any channel. * **`onLifecycle` / `on_lifecycle`** — connection state: `open`, `close`, `reconnect`, `error`. ## Channel events A channel event has an `id` (the subscription it came from), a `channel`, and `data` (the decoded payload). ```typescript TypeScript theme={null} radion.realtime.onChannel("trading", (e) => { console.log(e.id, e.channel, e.data); }); // every channel event radion.realtime.onAnyChannel((e) => console.log(e.channel, e.data)); ``` ```python Python theme={null} @radion.realtime.on_channel("trading") async def on_trade(e): print(e.id, e.channel, e.data) # every channel event @radion.realtime.on_any_channel() async def on_any(e): print(e.channel, e.data) ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::{Channel, Payload, Subscription}; let mut trading = radion .realtime .subscribe(Subscription::new("trading", Channel::Trading)) .await?; while let Some(e) = trading.next().await { match e.data { Payload::Trading(trade) => println!("{} {} {:?}", e.id, e.channel, trade.kind), _ => {} } } // every channel event let mut all = radion.realtime.events(); while let Some(e) = all.next().await { println!("{} {:?}", e.channel, e.data); } ``` In TypeScript, `onChannel(name, …)` narrows `event.data` to that channel's typed payload; the catch-all `events()` stream yields `AnyChannelPayload`. In Python `ChannelEvent` is generic over the payload and defaults to `Any`. In Rust `data` is the typed `Payload` enum, so `match` on it and the compiler checks you handle every case. ## Lifecycle events | Event | Payload | | ----------- | ------------------------------------------------------------------------------------------------------- | | `open` | — | | `close` | `{ code, reason }` | | `reconnect` | `{ attempt, delayMs }` (TS) / `{ "attempt", "delay" }` (Python) / `Reconnect { attempt, delay }` (Rust) | | `error` | An `Error` (often `RadionServerError`) / `Error(RadionError)` (Rust) | ```typescript TypeScript theme={null} radion.realtime.onLifecycle("open", () => console.log("connected")); radion.realtime.onLifecycle("close", ({ code, reason }) => console.log("closed", code, reason) ); radion.realtime.onLifecycle("reconnect", ({ attempt, delayMs }) => console.log(`reconnect #${attempt} in ${delayMs}ms`) ); radion.realtime.onLifecycle("error", (err) => console.error(err)); ``` ```python Python theme={null} @radion.realtime.on_lifecycle("open") async def opened(_): print("connected") @radion.realtime.on_lifecycle("close") async def closed(info): print(info["code"], info["reason"]) @radion.realtime.on_lifecycle("error") async def errored(err): print(err) ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::LifecycleEvent; let mut lifecycle = radion.realtime.lifecycle(); while let Some(event) = lifecycle.next().await { match event { LifecycleEvent::Open => println!("connected"), LifecycleEvent::Close { code, reason } => println!("closed {code} {reason}"), LifecycleEvent::Reconnect { attempt, delay } => { println!("reconnect #{attempt} in {delay:?}"); } LifecycleEvent::Error(err) => eprintln!("{err}"), _ => {} } } ``` In Python, handlers can be sync or async. In TypeScript, each `on*` method returns the client, so you can chain calls. In Rust, channel and lifecycle events come as `Stream`s (`events()`, per-subscription `subscribe(...)`, and `lifecycle()`); drop the stream to stop reading. To remove a handler, use the matching `off*` method (`offChannel` / `off_channel`, `offAnyChannel` / `off_any_channel`, `offLifecycle` / `off_lifecycle`). Leave out the handler to remove all of them for that target. If one of your handlers throws, the SDK reports it through the `error` event and does not retry it. It never blocks the other handlers from getting the event. # Realtime Source: https://docs.radion.app/sdks/realtime/overview Connect to the Radion realtime API with radion.realtime. `radion.realtime` is the WebSocket client. It runs the whole connection for you. It connects, reconnects after an unexpected drop, puts your subscriptions back, and sends each incoming frame to the handlers you set up. ## Connect `connect()` opens the socket and finishes once the connection is up. If the first try fails, it errors. ```typescript TypeScript theme={null} const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); await radion.realtime.connect(); radion.realtime.subscribe({ id: "trading", channel: "trading" }); radion.realtime.onChannel("trading", (event) => console.log(event.data)); ``` ```python Python theme={null} radion = Radion(api_key="sk_...") await radion.realtime.connect() await radion.realtime.subscribe(Subscription(id="trading", channel="trading")) @radion.realtime.on_channel("trading") async def handle(event): print(event.data) ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::{Channel, Subscription}; let radion = Radion::builder().api_key("sk_...").build()?; radion.realtime.connect().await?; let mut trading = radion .realtime .subscribe(Subscription::new("trading", Channel::Trading)) .await?; while let Some(event) = trading.next().await { println!("{:?}", event.data); } ``` The client just remembers what you want. You can call `subscribe` before or after `connect`. Either way, the client sends your subscriptions once the socket is open. ## Lifecycle 1. **Connect** — `connect()` opens the socket with your `X-API-Key` header. 2. **Subscribe** — add your subscriptions. The client sends them on open and again on every reconnect. 3. **Receive** — channel events go to your handlers. Lifecycle events tell you the connection state. 4. **Close** — `close()` shuts down cleanly and stops all reconnect tries. ## Reference Subscribe with ids and filters. Channel, wildcard, and lifecycle handlers. Backoff, keep-alive, and resubscribe. The error hierarchy and server codes. # Reconnect & heartbeats Source: https://docs.radion.app/sdks/realtime/reconnect Automatic reconnect, subscription restore, and heartbeat keep-alive. The realtime client keeps the connection healthy for you. Both reconnect and heartbeat are on by default, and you can tune them in [configuration](/sdks/configuration). ## Reconnect & subscription restore If the connection drops by surprise, the client reconnects on its own. It waits a bit longer between each try (exponential backoff) and adds a little random delay (jitter). Once the socket is back, it re-sends every active subscription. After you call `close()`, it stops trying. Each reconnect try fires a `reconnect` lifecycle event with the try number and the wait before the next try. ```typescript TypeScript theme={null} const radion = new Radion({ apiKey: process.env.RADION_API_KEY, realtime: { reconnect: { initialDelayMs: 500, maxDelayMs: 30_000, factor: 2, jitter: 0.2, }, }, }); radion.realtime.onLifecycle("reconnect", ({ attempt, delayMs }) => console.log(`reconnect #${attempt} in ${delayMs}ms`) ); // disable auto-reconnect entirely: // new Radion({ apiKey, realtime: { reconnect: false } }); ``` ```python Python theme={null} radion = Radion(api_key="sk_...", reconnect=True) @radion.realtime.on_lifecycle("reconnect") async def reconnecting(info): print(info["attempt"], info["delay"]) # disable auto-reconnect entirely: # Radion(api_key="sk_...", reconnect=False) ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::{LifecycleEvent, ReconnectOptions, RealtimeClient, RealtimeOptions}; use std::time::Duration; let client = RealtimeClient::new( RealtimeOptions::new("sk_...").reconnect(ReconnectOptions { initial_delay: Duration::from_millis(500), max_delay: Duration::from_secs(30), factor: 2.0, jitter: 0.2, }), ); let mut lifecycle = client.lifecycle(); while let Some(event) = lifecycle.next().await { if let LifecycleEvent::Reconnect { attempt, delay } = event { println!("reconnect #{attempt} in {delay:?}"); } } // disable auto-reconnect entirely: // RealtimeOptions::new("sk_...").disable_reconnect(); ``` ## Heartbeats The client sends a ping every so often. Any incoming frame proves the connection is alive. If nothing arrives before the timeout, the client treats the connection as dead, closes it, and reconnects. * **TypeScript** — `heartbeat: { intervalMs, timeoutMs }`, or `heartbeat: false` to disable. * **Python** — `heartbeat=True/False`, `heartbeat_interval`, `heartbeat_timeout` (seconds). * **Rust** — `RealtimeOptions::heartbeat(HeartbeatOptions { interval, timeout })` (both `Duration`), or `.disable_heartbeat()`. This is separate from the WebSocket ping/pong the server uses at the protocol level. See [Connection state](/websockets/connection-state) for how that works. # Subscriptions Source: https://docs.radion.app/sdks/realtime/subscriptions Subscribe to channels with ids and filters. A subscription is `{ id, channel, confirmed?, filters? }`. The `id` is your own string. The server sends it back on every event, so you can tell your subscriptions apart. `channel` is a channel name. `confirmed` defaults to `true` (the final on-chain feed); set it to `false` for the pending (mempool) feed of the same channel. `filters` are optional and run on the server. ```typescript TypeScript theme={null} radion.realtime.subscribe({ id: "trading", channel: "trading" }); radion.realtime.subscribe({ id: "whales", channel: "trading", filters: { min_usd: 10_000 }, }); radion.realtime.unsubscribe("whales"); ``` ```python Python theme={null} from radion import ChannelFilters, Subscription await radion.realtime.subscribe(Subscription(id="trading", channel="trading")) await radion.realtime.subscribe( Subscription(id="whales", channel="trading", filters=ChannelFilters(min_usd=10_000)) ) await radion.realtime.unsubscribe("whales") ``` ```rust Rust theme={null} use radion_sdk::realtime::{Channel, ChannelFilters, Subscription}; let _trading = radion .realtime .subscribe(Subscription::new("trading", Channel::Trading)) .await?; let _whales = radion .realtime .subscribe(Subscription::new("whales", Channel::Trading).with_filters( ChannelFilters { min_usd: Some(10_000.0), ..Default::default() }, )) .await?; radion.realtime.unsubscribe("whales").await?; ``` If you subscribe again with an `id` you already use, the new subscription replaces the old one on that id. To stop, unsubscribe with the same `id`. ## Filters `ChannelFilters` narrows a channel on the server: | Filter | Type | Used by | | ------------ | ---------- | ---------------------------------------------------------------------------------------------------------- | | `wallets` | `string[]` | `wallets` (required); `trading`, `fees`, `positions`, `combos`, `transfers`, `accounts` | | `market_ids` | `string[]` | `markets` (one of market/token ids); `trading`, `oracle`, `resolution`, `lifecycle`, `positions`, `combos` | | `token_ids` | `string[]` | `markets`; `trading`, `fees`, `lifecycle`, `positions`, `combos`, `transfers` | | `min_usd` | `number` | `trading` | Only the two filter channels need a filter. `wallets` needs `wallets`. `markets` needs at least one of `market_ids` / `token_ids`. If you subscribe to either without its filter, you get an [error](/sdks/realtime/errors). On topic channels, filters are optional — an empty filter means the whole channel. ## Pending feed Set `confirmed: false` on a subscription to get pending transactions before they land in a block. These are not final yet. Every event carries a `confirmed` flag, so you route on that (and tell subscriptions apart by `id`): ```typescript TypeScript theme={null} radion.realtime.subscribe({ id: "pending", channel: "trading", confirmed: false, }); radion.realtime.subscribe({ id: "confirmed", channel: "trading" }); radion.realtime.onAnyChannel((e) => { if (!e.confirmed) { // pending } }); ``` ```python Python theme={null} await radion.realtime.subscribe(Subscription(id="pending", channel="trading", confirmed=False)) await radion.realtime.subscribe(Subscription(id="confirmed", channel="trading")) @radion.realtime.on_any_channel() async def on_any(e): if not e.confirmed: ... # pending ``` ```rust Rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::{Channel, Subscription}; radion .realtime .subscribe(Subscription::new("pending", Channel::Trading).pending()) .await?; radion .realtime .subscribe(Subscription::new("confirmed", Channel::Trading)) .await?; let mut events = radion.realtime.events(); while let Some(e) = events.next().await { if !e.confirmed { // pending } } ``` See the [WebSockets filters](/websockets/filters) and [mempool](/websockets/mempool) references for how this works under the hood. # Rust Source: https://docs.radion.app/sdks/rust The radion-sdk Rust crate — async-first, fully typed, stream-based. [`radion-sdk`](https://crates.io/crates/radion-sdk) is the Rust SDK (you import it as `radion_sdk`). It is async-first, built on `tokio`, and has few dependencies. Events come as typed [`Stream`](https://docs.rs/futures)s, not callbacks. You `match` on the payload enum, so the compiler checks you handle every case. Needs Rust 1.85+. ```bash theme={null} cargo add radion-sdk ``` ## Usage ```rust theme={null} use futures_util::StreamExt; use radion_sdk::realtime::{Channel, Payload, Subscription}; use radion_sdk::Radion; #[tokio::main] async fn main() -> anyhow::Result<()> { let radion = Radion::builder() .api_key(std::env::var("RADION_API_KEY")?) .build()?; radion.realtime.connect().await?; let mut trading = radion .realtime .subscribe(Subscription::new("trading", Channel::Trading)) .await?; while let Some(event) = trading.next().await { if let Payload::Trading(trade) = event.data { println!("{} {:?}", event.id, trade.kind); } } Ok(()) } ``` ## Features The crate uses cargo features, so you build only the parts you use: | Feature | Default | Description | | ------------ | ------- | -------------------------------------------------- | | `realtime` | ✅ | The WebSocket features. | | `rustls` | ✅ | rustls TLS backend (no system OpenSSL dependency). | | `native-tls` | | Use the platform native TLS backend instead. | | `tracing` | | Emit [`tracing`](https://docs.rs/tracing) events. | ```bash theme={null} # realtime + native TLS + tracing, without the default rustls backend cargo add radion-sdk --no-default-features --features realtime,native-tls,tracing ``` ## Standalone realtime client If you only need the stream, build `RealtimeClient` on its own. This is also where you tune reconnect and heartbeat: ```rust theme={null} use radion_sdk::realtime::{RealtimeClient, RealtimeOptions}; let client = RealtimeClient::new(RealtimeOptions::new("sk_...")); client.connect().await?; ``` ## Exports | Export | Kind | | ----------------------------------------------------------------- | -------- | | `Radion`, `RadionBuilder` | client | | `realtime::RealtimeClient`, `RealtimeOptions` | realtime | | `RadionError` | errors | | `realtime::{Subscription, ChannelFilters, ChannelEvent, Payload}` | types | | `realtime::{Channel, SubscribableChannel, CHANNELS}` | channels | | `realtime::{LifecycleEvent, ReconnectOptions, HeartbeatOptions}` | options | Each channel event's `data` is the typed `Payload` enum (`Payload::Trading`, `Payload::Oracle`, …). The SDK keeps unknown channels or event types as `Payload::Other(serde_json::Value)`, so a new server event is never lost. # TypeScript Source: https://docs.radion.app/sdks/typescript The @radion-app/sdk TypeScript package — ESM + CJS, fully typed. [`@radion-app/sdk`](https://www.npmjs.com/package/@radion-app/sdk) is the TypeScript SDK. It ships both ESM and CommonJS builds and brings its own types. It has few dependencies — just `ws` and `zod`. Needs Node.js 18+. ```bash theme={null} pnpm add @radion-app/sdk ``` ## Usage ```typescript theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); await radion.realtime.connect(); radion.realtime.subscribe({ id: "trading", channel: "trading" }); radion.realtime.onChannel("trading", (event) => console.log(event.id, event.data) ); ``` ## Modules Both module systems load from the package `exports`: ```typescript theme={null} import { Radion } from "@radion-app/sdk"; // ESM ``` ```javascript theme={null} const { Radion } = require("@radion-app/sdk"); // CommonJS ``` ## Standalone realtime client If you only need the stream, import `RealtimeClient` on its own: ```typescript theme={null} import { RealtimeClient } from "@radion-app/sdk"; const client = new RealtimeClient({ apiKey: process.env.RADION_API_KEY }); await client.connect(); ``` ## Exports | Export | Kind | | -------------------------------------------------------------------------- | ------------ | | `Radion`, `RealtimeClient` | classes | | `RadionError`, `RadionConnectionError`, `RadionServerError` | errors | | `CHANNELS`, `isChannel` | values | | `Channel`, `Subscription`, `ChannelFilters`, `ChannelEvent` | types | | `RadionOptions`, `RealtimeOptions`, `ReconnectOptions`, `HeartbeatOptions` | option types | `onChannel(name, …)` narrows `event.data` to that channel's typed payload. For the catch-all `events()` stream, use `ChannelEvent` to set the payload type. ## Examples You can find ready-to-run Node.js + TypeScript examples for every channel in the [WebSocket examples](/websockets/examples). # Authentication Source: https://docs.radion.app/websockets/authentication Authenticate WebSocket connections with your Radion API key. Every WebSocket connection needs a valid API key. Send it as a header in the WebSocket upgrade request: ```http theme={null} X-API-Key: YOUR_API_KEY ``` Most WebSocket client libraries let you set custom headers for the upgrade request. ## Public JWT keys If you connect with a [public JWT key](/api/authentication#public-jwt-keys) (`pk_jwt_...`), also send the user's token in the `Authorization` header: ```http theme={null} X-API-Key: pk_jwt_YOUR_PUBLIC_KEY Authorization: Bearer USER_JWT ``` ## From a browser Browsers cannot set headers on a WebSocket handshake. For that one case, pass the credentials as query parameters instead: ```text theme={null} wss://api.radion.app/ws?api-key=YOUR_API_KEY&token=USER_JWT ``` Use `token` only with a public JWT key. Radion reads query parameters on the WebSocket upgrade only, never on plain HTTP requests. Prefer headers. Proxies and servers log URLs, so a key in the query string can leak into their logs. Use the query string only when your client cannot set headers, such as a browser `WebSocket`. The key you connect with sets the [rate limits](/websockets/rate-limits) for that connection. Radion checks the key again about every 30 seconds while the connection is open. If the key is revoked, Radion closes the connection with a `key_revoked` [error frame](/websockets/frames#error-frames). If the key is missing or wrong, the upgrade is rejected. A bad key returns HTTP `401`. # accounts Source: https://docs.radion.app/websockets/channels/accounts Proxy wallet creation from the Polymarket DepositWalletFactory and Gnosis Safe factory contracts. The `accounts` channel sends every proxy wallet creation event. They come from the DepositWalletFactory (deposit wallets) and the Gnosis Safe factory (smart-wallet proxies). We decode each one and send it after the transaction is confirmed on-chain. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "accounts" } ``` With no filters, you get every confirmed wallet creation event. ## Filters | Filter | Type | Required | Description | | --------- | ---------- | -------- | ------------------------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Keeps events for a listed new wallet, owner, or proxy address. | This channel accepts only `wallets`. `market_ids`, `token_ids`, and `min_usd` are ignored here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends both wallet-factory events. | Event | `data.type` | Description | | ---------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- | | `WalletDeployed` | `wallet_deployed` | A new deposit wallet deployed via DepositWalletFactory. Links the wallet address to its owner and implementation. | | `ProxyCreation` | `proxy_creation` | A new Gnosis Safe proxy deployed. Tracks smart-wallet creation for institutional participants. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "accounts", "confirmed": true, "data": { "type": "wallet_deployed", "wallet": "0x…", "owner": "0x…", "…": "…" } } ``` ### `wallet_deployed` `WalletDeployed` from the DepositWalletFactory. A new deposit wallet, linked to its owner and implementation. ```json theme={null} { "type": "wallet_deployed", "wallet": "0x…", "owner": "0x…", "id": "0x…", "implementation": "0x…" } ``` | Field | Solidity type | Description | | ---------------- | --------------- | ------------------------------------------ | | `type` | string | `wallet_deployed`. | | `wallet` | `address` (hex) | Address of the new deposit wallet. | | `owner` | `address` (hex) | Owner of the new wallet. | | `id` | `bytes32` (hex) | Factory id (salt) for the wallet. | | `implementation` | `address` (hex) | Implementation contract behind the wallet. | ### `proxy_creation` `ProxyCreation` from the Gnosis Safe factory. A new smart-wallet proxy. Both fields are unindexed on-chain. ```json theme={null} { "type": "proxy_creation", "proxy": "0x…", "owner": "0x…" } ``` | Field | Solidity type | Description | | ------- | --------------- | -------------------------------- | | `type` | string | `proxy_creation`. | | `proxy` | `address` (hex) | Address of the new proxy wallet. | | `owner` | `address` (hex) | Owner the proxy was created for. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the wallet-factory contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "accounts", "confirmed": false } ``` # clob Source: https://docs.radion.app/websockets/channels/clob The Polymarket CLOB order-book feed — book, prices, last trade, midpoint, tick size, and best bid/ask — proxied over the Radion WebSocket. The `clob.*` channels carry the Polymarket CLOB (central limit order book) live feed. Radion proxies it over the same WebSocket as the on-chain channels, so you get order-book state, prices, and trades next to the confirmed chain events, on one connection. This is a **separate channel family** from the topic channels ([`trading`](/websockets/channels/trading), [`positions`](/websockets/channels/positions), and the rest). It differs in three ways: * **Every clob channel requires a `token_ids` filter.** You subscribe per outcome token. With no `token_ids`, the subscribe fails. * **The `data` payload has no `type` discriminator.** Each clob channel has one fixed payload shape. Topic channels put a snake\_case `type` inside `data`; clob channels do not. * **There is no pending feed.** CLOB channels have no pending/confirmed concept — `confirmed: false` does not apply. The CLOB feed is off-chain order-book state, not pending transactions. ## Channels | Channel | Sends | | ------------------- | ------------------------------------------------------------------- | | `clob.book` | Full order-book snapshot: all bid and ask levels. | | `clob.prices` | Price-change batches: which levels moved, and the new best bid/ask. | | `clob.last_trade` | The last trade price and size for the token. | | `clob.midpoint` | The midpoint between best bid and best ask. | | `clob.tick_size` | Tick-size changes for the token. | | `clob.best_bid_ask` | The best bid and best ask. | `clob.prices` is the CLOB price-change feed. It is not the removed derived `prices` topic channel — that one carried last-trade price ticks and no longer exists. `clob.prices` is unrelated and stays. ## Subscribe Every clob channel requires at least one `token_ids` value: ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "clob.book", "filters": { "token_ids": [ "71321045679252212594626385532706912750332728571942532289631379312455583992563" ] } } ``` ## Filters | Filter | Type | Required | Description | | ----------- | ---------- | -------- | --------------------------------------------------------------------------- | | `token_ids` | `string[]` | Yes | Outcome token IDs as decimal strings. At least one, or the subscribe fails. | Clob channels accept only `token_ids`. `wallets`, `market_ids`, and `min_usd` are ignored here. See [Filters](/websockets/filters) for the combination rule. ## Payloads Each frame is a standard event frame: `type: "event"`, your `id`, the `channel` name, and a `data` object. Unlike the topic channels, `data` carries **no** `type` field — the channel name already tells you the shape. In every payload, ids are strings (`asset_id` is a U256 decimal string, `market` is a `0x` hex string), `timestamp` is a number, and prices and sizes are numbers. Fields marked optional may be absent. ### `clob.book` Full order-book snapshot for the token. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.book", "data": { "asset_id": "71321045…992563", "market": "0x…", "timestamp": 1730000000, "bids": [{ "price": 0.52, "size": 1200 }], "asks": [{ "price": 0.54, "size": 800 }] } } ``` | Field | Type | Description | | ----------- | --------- | -------------------------------------------------------- | | `asset_id` | string | Outcome token id (U256 decimal string). | | `market` | string | Market id (`0x` hex string). | | `timestamp` | number | Feed timestamp. | | `bids` | `Level[]` | Bid levels. Each `Level` is `{ price, size }` (numbers). | | `asks` | `Level[]` | Ask levels. Each `Level` is `{ price, size }` (numbers). | ### `clob.prices` A batch of level changes for a market, with the new best bid/ask per token. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.prices", "data": { "market": "0x…", "timestamp": 1730000000, "changes": [ { "asset_id": "71321045…992563", "price": 0.53, "size": 500, "best_bid": 0.52, "best_ask": 0.54 } ] } } ``` | Field | Type | Description | | ----------- | --------------- | ------------------------------ | | `market` | string | Market id (`0x` hex string). | | `timestamp` | number | Feed timestamp. | | `changes` | `PriceChange[]` | The changed levels. See below. | Each `PriceChange`: | Field | Type | Optional | Description | | ---------- | ------ | -------- | --------------------------- | | `asset_id` | string | No | Outcome token id. | | `price` | number | No | The changed price level. | | `size` | number | Yes | New size at the level. | | `best_bid` | number | Yes | New best bid for the token. | | `best_ask` | number | Yes | New best ask for the token. | ### `clob.last_trade` The last trade for the token. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.last_trade", "data": { "asset_id": "71321045…992563", "market": "0x…", "price": 0.53, "size": 250, "timestamp": 1730000000 } } ``` | Field | Type | Optional | Description | | ----------- | ------ | -------- | ---------------------------- | | `asset_id` | string | No | Outcome token id. | | `market` | string | No | Market id (`0x` hex string). | | `price` | number | No | Last trade price. | | `size` | number | Yes | Last trade size. | | `timestamp` | number | No | Feed timestamp. | ### `clob.midpoint` The midpoint between best bid and best ask. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.midpoint", "data": { "asset_id": "71321045…992563", "market": "0x…", "midpoint": 0.53, "timestamp": 1730000000 } } ``` | Field | Type | Description | | ----------- | ------ | ---------------------------- | | `asset_id` | string | Outcome token id. | | `market` | string | Market id (`0x` hex string). | | `midpoint` | number | Midpoint price. | | `timestamp` | number | Feed timestamp. | ### `clob.tick_size` A tick-size change for the token. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.tick_size", "data": { "asset_id": "71321045…992563", "market": "0x…", "timestamp": 1730000000 } } ``` | Field | Type | Description | | ----------- | ------ | ---------------------------- | | `asset_id` | string | Outcome token id. | | `market` | string | Market id (`0x` hex string). | | `timestamp` | number | Feed timestamp. | ### `clob.best_bid_ask` The best bid and best ask for the token. ```json theme={null} { "type": "event", "id": "s1", "channel": "clob.best_bid_ask", "data": { "asset_id": "71321045…992563", "market": "0x…", "best_bid": 0.52, "best_ask": 0.54, "timestamp": 1730000000 } } ``` | Field | Type | Description | | ----------- | ------ | ---------------------------- | | `asset_id` | string | Outcome token id. | | `market` | string | Market id (`0x` hex string). | | `best_bid` | number | Best bid price. | | `best_ask` | number | Best ask price. | | `timestamp` | number | Feed timestamp. | ## No pending feed Clob channels have no pending/confirmed concept — `confirmed: false` does not apply. The pending feed streams pending on-chain transactions; the CLOB feed is off-chain order-book state, so there is nothing to mirror. See [Mempool](/websockets/mempool) for the channels that do have a pending feed. # combos Source: https://docs.radion.app/websockets/channels/combos Module, neg-risk, combinatorial, bridge, and migration position events from the BinaryModule, NegRiskModule, and combinatorial contracts. The `combos` channel sends every module, neg-risk, combinatorial, bridge, and migration position event from the BinaryModule, the NegRiskModule, the AutoRedeemer, and the combinatorial market contracts. We decode each one and send it after the transaction is confirmed on-chain. Plain CTF base-layer moves are on [`positions`](/websockets/channels/positions). Settlement events (resolution, pauses) are on [`resolution`](/websockets/channels/resolution). ERC-1155 transfers are on [`transfers`](/websockets/channels/transfers). Combinatorial **setup** (`CombinatorialConditionPrepared`) is on [`lifecycle`](/websockets/channels/lifecycle). ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "combos" } ``` With no filters, you get every confirmed combos event from all markets. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | ----------------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Keeps events where a listed address is a participant. | | `market_ids` | `string[]` | No | Condition/market IDs as hex strings. Keeps events for those markets. | | `token_ids` | `string[]` | No | Token/position IDs as decimal strings. Keeps events for the listed positions. | `market_ids` and `token_ids` are one axis. `min_usd` is ignored here. Module and combinatorial events use compact `bytes31`/`bytes29` ids and position ids, so market matching here is coarser than on `trading`. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends all twenty-five module, combinatorial, bridge, and migration events. See [Onchain Data](/concepts/onchain-data) for the part each event plays. ### Redemptions | Event | `data.type` | Description | | ------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- | | `Redemption` | `redemption` | An AutoRedeemer redemption. Tracks automatic position settlement by position id. | | `BinaryRedemption` | `binary_redemption` | A BinaryModule redemption. Tracks binary market position settlement by condition id. | | `NegRiskRedemption` | `neg_risk_redemption` | A NegRiskModule redemption. Tracks neg-risk position settlement by condition id. | | `PositionRedeemed` | `position_redeemed` | A position redeemed through a module. Carries the initiator, position id, recipient, amount, and payout. | ### NegRisk conversions | Event | `data.type` | Description | | ------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `CollateralPositionsConverted` | `collateral_positions_converted` | A NegRisk collateral swap between outcome positions. Tracks risk moving across neg-risk structures. | | `NegRiskPositionsConverted` | `neg_risk_positions_converted` | A NegRisk position swap at the adapter layer. Tracks risk moving across neg-risk token structures. | | `PositionConverted` | `position_converted` | A position converted inside the NegRisk module. Carries the initiator, event id, recipient, condition index, and amount. | ### Module position ops | Event | `data.type` | Description | | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `ModulePositionsMerged` | `module_positions_merged` | Positions merged inside a module. Carries the initiator, condition id, recipient, and amount. | | `ModulePositionsSplit` | `module_positions_split` | Positions split inside a module. Carries the initiator, condition id, two recipients, and amount. | | `HorizontalMerge` | `horizontal_merge` | A NegRisk horizontal merge across outcomes. Turns a full set of outcome positions back into collateral in an event. | | `HorizontalSplit` | `horizontal_split` | A NegRisk horizontal split across outcomes. Turns collateral into a full set of outcome positions in an event. | ### Combinatorial | Event | `data.type` | Description | | ------------------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `SplitOnCondition` | `split_on_condition` | A split for just one condition in a combinatorial market. Carries parent and child condition ids and amount. | | `MergedOnCondition` | `merged_on_condition` | A merge for just one condition in a combinatorial market. Carries parent and child condition ids and amount. | | `ConvertedToYesBasket` | `converted_to_yes_basket` | Positions turned into a yes basket. Carries the user, full condition id, and amount. | | `MergedFromYesBasket` | `merged_from_yes_basket` | Positions merged out of a yes basket. Carries the user, full condition id, and amount. | | `Extracted` | `extracted` | Positions taken out of a combinatorial structure. Carries the user, full and reduced condition ids, residual condition id, and amount. | | `Injected` | `injected` | Positions put into a combinatorial structure. Carries the user, full and reduced condition ids, residual condition id, and amount. | | `Compressed` | `compressed` | Positions compressed in the combinatorial module. Combines many positions into one token. | | `CombinatorialWrapped` | `combinatorial_wrapped` | Positions wrapped in the combinatorial module. Carries the user, underlying position id, new combinatorial position id, and amount. | | `CombinatorialUnwrapped` | `combinatorial_unwrapped` | Positions unwrapped in the combinatorial module. Carries the user, combinatorial position id, underlying position id, and amount. | ### Bridge and migration | Event | `data.type` | Description | | ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `PositionMigrated` | `position_migrated` | A position moved across module versions. Carries the sender, condition id, position id, outcome index, and amount. | | `MigrationResolved` | `migration_resolved` | A migration resolved. Carries both condition ids, legacy payouts, and v2 result numerators. | | `BridgePositionMinted` | `bridge_position_minted` | A bridge position minted. Carries the recipient address, position id, and amount. | | `BridgePositionsBurned` | `bridge_positions_burned` | Bridge positions burned on the source side. Carries arrays of position ids and amounts. | | `LegacyCollateralSettled` | `legacy_collateral_settled` | Legacy collateral settled during migration. Carries the legacy token, vault address, and amount. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "combos", "confirmed": true, "data": { "type": "neg_risk_redemption", "from": "0x…", "conditionId": "0x…", "…": "…" } } ``` Module events use compact ids: `conditionId` is `bytes31` and `eventId` is `bytes29` (not the `bytes32` used on `trading` and base-layer channels). Position ids and amounts are `uint256`, serialized as `0x…` hex. ### Redemptions #### `redemption` `Redemption` on the AutoRedeemer. An automatic position settlement, keyed by position id. ```json theme={null} { "type": "redemption", "from": "0x…", "positionId": "0x…", "payout": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ----------------------------------- | | `type` | string | `redemption`. | | `from` | `address` (hex) | Address that redeemed the position. | | `positionId` | `uint256` (hex) | Position id redeemed. | | `payout` | `uint256` (hex) | Collateral paid out. | #### `binary_redemption` `BinaryRedemption` on the BinaryModule. A binary-market position settled, keyed by condition id. ```json theme={null} { "type": "binary_redemption", "from": "0x…", "conditionId": "0x…", "payout": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | ----------------------------------- | | `type` | string | `binary_redemption`. | | `from` | `address` (hex) | Address that redeemed the position. | | `conditionId` | `bytes32` (hex) | Condition being redeemed. | | `payout` | `uint256` (hex) | Collateral paid out. | #### `neg_risk_redemption` `NegRiskRedemption` on the NegRiskModule. A neg-risk position settled, keyed by condition id. ```json theme={null} { "type": "neg_risk_redemption", "from": "0x…", "conditionId": "0x…", "payout": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | ----------------------------------- | | `type` | string | `neg_risk_redemption`. | | `from` | `address` (hex) | Address that redeemed the position. | | `conditionId` | `bytes32` (hex) | Condition being redeemed. | | `payout` | `uint256` (hex) | Collateral paid out. | #### `position_redeemed` `PositionRedeemed` on a module. A position redeemed through a module, with the initiator, recipient, and amounts. ```json theme={null} { "type": "position_redeemed", "initiator": "0x…", "positionId": "0x…", "recipient": "0x…", "amount": "0x…", "payout": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ------------------------------------ | | `type` | string | `position_redeemed`. | | `initiator` | `address` (hex) | Address that started the redemption. | | `positionId` | `uint256` (hex) | Position id redeemed. | | `recipient` | `address` (hex) | Address that received the payout. | | `amount` | `uint256` (hex) | Position amount redeemed. | | `payout` | `uint256` (hex) | Collateral paid out. | ### NegRisk conversions #### `collateral_positions_converted` `PositionsConverted` on the NegRiskCtfCollateralAdapter. A collateral swap between outcome positions, with the amount out. ```json theme={null} { "type": "collateral_positions_converted", "initiator": "0x…", "marketId": "0x…", "indexSet": "0x…", "amount": "0x…", "amountOut": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ------------------------------------- | | `type` | string | `collateral_positions_converted`. | | `initiator` | `address` (hex) | Address that converted the positions. | | `marketId` | `bytes32` (hex) | Neg-risk market the conversion is in. | | `indexSet` | `uint256` (hex) | Index set of the outcomes converted. | | `amount` | `uint256` (hex) | Amount converted in. | | `amountOut` | `uint256` (hex) | Amount produced by the conversion. | #### `neg_risk_positions_converted` `PositionsConverted` on the NegRiskAdapter. A neg-risk position swap at the adapter layer. ```json theme={null} { "type": "neg_risk_positions_converted", "stakeholder": "0x…", "marketId": "0x…", "indexSet": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | ------------------------------------- | | `type` | string | `neg_risk_positions_converted`. | | `stakeholder` | `address` (hex) | Address that converted the positions. | | `marketId` | `bytes32` (hex) | Neg-risk market the conversion is in. | | `indexSet` | `uint256` (hex) | Index set of the outcomes converted. | | `amount` | `uint256` (hex) | Amount converted. | #### `position_converted` `PositionConverted` on the NegRiskModule. A position converted inside the module, keyed by event id and condition index. ```json theme={null} { "type": "position_converted", "initiator": "0x…", "eventId": "0x…", "recipient": "0x…", "conditionIndex": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ---------------- | --------------- | ------------------------------------ | | `type` | string | `position_converted`. | | `initiator` | `address` (hex) | Address that converted the position. | | `eventId` | `bytes29` (hex) | Module event the conversion is in. | | `recipient` | `address` (hex) | Address that received the result. | | `conditionIndex` | `uint256` (hex) | Index of the condition converted. | | `amount` | `uint256` (hex) | Amount converted. | ### Module position ops #### `module_positions_merged` `PositionsMerged` on a module. Positions merged inside a module, keyed by condition id. ```json theme={null} { "type": "module_positions_merged", "initiator": "0x…", "conditionId": "0x…", "recipient": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | ---------------------------------- | | `type` | string | `module_positions_merged`. | | `initiator` | `address` (hex) | Address that merged the positions. | | `conditionId` | `bytes31` (hex) | Module condition id. | | `recipient` | `address` (hex) | Address that received the merge. | | `amount` | `uint256` (hex) | Amount merged. | #### `module_positions_split` `PositionsSplit` on a module. Positions split inside a module, into two recipients. ```json theme={null} { "type": "module_positions_split", "initiator": "0x…", "conditionId": "0x…", "recipient0": "0x…", "recipient1": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | --------------------------------- | | `type` | string | `module_positions_split`. | | `initiator` | `address` (hex) | Address that split the positions. | | `conditionId` | `bytes31` (hex) | Module condition id. | | `recipient0` | `address` (hex) | First recipient of the split. | | `recipient1` | `address` (hex) | Second recipient of the split. | | `amount` | `uint256` (hex) | Amount split. | #### `horizontal_merge` `HorizontalMerge` on the NegRiskModule. A full set of outcome positions merged back into collateral across an event. ```json theme={null} { "type": "horizontal_merge", "initiator": "0x…", "eventId": "0x…", "recipient": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ---------------------------------- | | `type` | string | `horizontal_merge`. | | `initiator` | `address` (hex) | Address that merged the positions. | | `eventId` | `bytes29` (hex) | Module event the merge is in. | | `recipient` | `address` (hex) | Address that received the merge. | | `amount` | `uint256` (hex) | Amount merged. | #### `horizontal_split` `HorizontalSplit` on the NegRiskModule. Collateral split into a full set of outcome positions across an event. ```json theme={null} { "type": "horizontal_split", "initiator": "0x…", "eventId": "0x…", "recipient": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | --------------------------------- | | `type` | string | `horizontal_split`. | | `initiator` | `address` (hex) | Address that split the positions. | | `eventId` | `bytes29` (hex) | Module event the split is in. | | `recipient` | `address` (hex) | Address that received the split. | | `amount` | `uint256` (hex) | Amount split. | ### Combinatorial #### `split_on_condition` `SplitOnCondition` on the combinatorial module. A split on one condition inside a combinatorial market. ```json theme={null} { "type": "split_on_condition", "user": "0x…", "parentConditionId": "0x…", "childYesConditionId": "0x…", "childNoConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | --------------------- | --------------- | ----------------------------------- | | `type` | string | `split_on_condition`. | | `user` | `address` (hex) | Address that split the position. | | `parentConditionId` | `bytes31` (hex) | Parent condition being split. | | `childYesConditionId` | `bytes31` (hex) | Child condition for the yes branch. | | `childNoConditionId` | `bytes31` (hex) | Child condition for the no branch. | | `amount` | `uint256` (hex) | Amount split. | #### `merged_on_condition` `MergedOnCondition` on the combinatorial module. A merge on one condition inside a combinatorial market. ```json theme={null} { "type": "merged_on_condition", "user": "0x…", "parentConditionId": "0x…", "childYesConditionId": "0x…", "childNoConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | --------------------- | --------------- | ----------------------------------- | | `type` | string | `merged_on_condition`. | | `user` | `address` (hex) | Address that merged the position. | | `parentConditionId` | `bytes31` (hex) | Parent condition being merged. | | `childYesConditionId` | `bytes31` (hex) | Child condition for the yes branch. | | `childNoConditionId` | `bytes31` (hex) | Child condition for the no branch. | | `amount` | `uint256` (hex) | Amount merged. | #### `converted_to_yes_basket` `ConvertedToYesBasket` on the combinatorial module. Positions turned into a yes basket. ```json theme={null} { "type": "converted_to_yes_basket", "user": "0x…", "fullConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ----------------- | --------------- | ------------------------------------ | | `type` | string | `converted_to_yes_basket`. | | `user` | `address` (hex) | Address that converted the position. | | `fullConditionId` | `bytes31` (hex) | Full combinatorial condition id. | | `amount` | `uint256` (hex) | Amount converted. | #### `merged_from_yes_basket` `MergedFromYesBasket` on the combinatorial module. Positions merged out of a yes basket. ```json theme={null} { "type": "merged_from_yes_basket", "user": "0x…", "fullConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ----------------- | --------------- | --------------------------------- | | `type` | string | `merged_from_yes_basket`. | | `user` | `address` (hex) | Address that merged the position. | | `fullConditionId` | `bytes31` (hex) | Full combinatorial condition id. | | `amount` | `uint256` (hex) | Amount merged. | #### `extracted` `Extracted` on the combinatorial module. Positions taken out of a combinatorial structure, leaving a residual. ```json theme={null} { "type": "extracted", "user": "0x…", "fullConditionId": "0x…", "reducedConditionId": "0x…", "residualConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | --------------------- | --------------- | --------------------------------------------- | | `type` | string | `extracted`. | | `user` | `address` (hex) | Address that extracted the position. | | `fullConditionId` | `bytes31` (hex) | Full condition before extraction. | | `reducedConditionId` | `bytes31` (hex) | Condition after the extracted leg is removed. | | `residualConditionId` | `bytes31` (hex) | Residual condition left behind. | | `amount` | `uint256` (hex) | Amount extracted. | #### `injected` `Injected` on the combinatorial module. Positions put into a combinatorial structure — the inverse of `extracted`. ```json theme={null} { "type": "injected", "user": "0x…", "fullConditionId": "0x…", "reducedConditionId": "0x…", "residualConditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | --------------------- | --------------- | ------------------------------------------- | | `type` | string | `injected`. | | `user` | `address` (hex) | Address that injected the position. | | `fullConditionId` | `bytes31` (hex) | Full condition after injection. | | `reducedConditionId` | `bytes31` (hex) | Condition before the injected leg is added. | | `residualConditionId` | `bytes31` (hex) | Residual condition combined in. | | `amount` | `uint256` (hex) | Amount injected. | #### `compressed` `Compressed` on the combinatorial module. Many positions combined into one token. ```json theme={null} { "type": "compressed", "user": "0x…", "oldPositionId": "0x…", "newPositionId": "0x…", "amount": "0x…", "positionAmount": "0x…", "collateralOut": "0x…" } ``` | Field | Solidity type | Description | | ---------------- | --------------- | --------------------------------------- | | `type` | string | `compressed`. | | `user` | `address` (hex) | Address that compressed the positions. | | `oldPositionId` | `uint256` (hex) | Position id being compressed. | | `newPositionId` | `uint256` (hex) | Resulting compressed position id. | | `amount` | `uint256` (hex) | Amount compressed. | | `positionAmount` | `uint256` (hex) | Resulting position amount. | | `collateralOut` | `uint256` (hex) | Collateral released by the compression. | #### `combinatorial_wrapped` `Wrapped` on the combinatorial module. A position wrapped into a combinatorial position token. ```json theme={null} { "type": "combinatorial_wrapped", "user": "0x…", "underlyingPositionId": "0x…", "combinatorialPositionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------------------- | --------------- | ------------------------------------ | | `type` | string | `combinatorial_wrapped`. | | `user` | `address` (hex) | Address that wrapped the position. | | `underlyingPositionId` | `uint256` (hex) | Underlying position id wrapped. | | `combinatorialPositionId` | `uint256` (hex) | Resulting combinatorial position id. | | `amount` | `uint256` (hex) | Amount wrapped. | #### `combinatorial_unwrapped` `Unwrapped` on the combinatorial module. A combinatorial position token unwrapped back to its underlying — the inverse of `combinatorial_wrapped`. ```json theme={null} { "type": "combinatorial_unwrapped", "user": "0x…", "combinatorialPositionId": "0x…", "underlyingPositionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------------------- | --------------- | ------------------------------------ | | `type` | string | `combinatorial_unwrapped`. | | `user` | `address` (hex) | Address that unwrapped the position. | | `combinatorialPositionId` | `uint256` (hex) | Combinatorial position id unwrapped. | | `underlyingPositionId` | `uint256` (hex) | Resulting underlying position id. | | `amount` | `uint256` (hex) | Amount unwrapped. | ### Bridge and migration #### `position_migrated` `PositionMigrated` on a module. A position moved across module versions. ```json theme={null} { "type": "position_migrated", "from": "0x…", "conditionId": "0x…", "positionId": "0x…", "outcomeIndex": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | -------------- | --------------- | ----------------------------------- | | `type` | string | `position_migrated`. | | `from` | `address` (hex) | Address whose position migrated. | | `conditionId` | `bytes31` (hex) | Module condition id. | | `positionId` | `uint256` (hex) | Position id migrated. | | `outcomeIndex` | `uint256` (hex) | Outcome index within the condition. | | `amount` | `uint256` (hex) | Amount migrated. | #### `migration_resolved` `MigrationResolved` on a module. A migration resolved, with both condition ids, the legacy payouts, and v2 result numerators. ```json theme={null} { "type": "migration_resolved", "conditionId": "0x…", "legacyConditionId": "0x…", "legacyPayout0": "0x…", "legacyPayout1": "0x…", "result0": "0x…", "result1": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | -------------------------------------- | | `type` | string | `migration_resolved`. | | `conditionId` | `bytes31` (hex) | v2 condition id. | | `legacyConditionId` | `bytes32` (hex) | Matching legacy condition id. | | `legacyPayout0` | `uint256` (hex) | Legacy payout numerator for outcome 0. | | `legacyPayout1` | `uint256` (hex) | Legacy payout numerator for outcome 1. | | `result0` | `uint256` (hex) | v2 result numerator for outcome 0. | | `result1` | `uint256` (hex) | v2 result numerator for outcome 1. | #### `bridge_position_minted` `BridgePositionMinted` on a module. A bridge position minted on the destination side. ```json theme={null} { "type": "bridge_position_minted", "to": "0x…", "positionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ----------------------------------- | | `type` | string | `bridge_position_minted`. | | `to` | `address` (hex) | Address that received the position. | | `positionId` | `uint256` (hex) | Position id minted. | | `amount` | `uint256` (hex) | Amount minted. | #### `bridge_positions_burned` `BridgePositionsBurned` on a module. Bridge positions burned on the source side. The `positionIds` and `amounts` arrays line up index for index. ```json theme={null} { "type": "bridge_positions_burned", "positionIds": ["0x…", "0x…"], "amounts": ["0x…", "0x…"] } ``` | Field | Solidity type | Description | | ------------- | ----------------- | ---------------------------------------- | | `type` | string | `bridge_positions_burned`. | | `positionIds` | `uint256[]` (hex) | Position ids burned. | | `amounts` | `uint256[]` (hex) | Amount burned per id, in the same order. | #### `legacy_collateral_settled` `LegacyCollateralSettled` on a module. Legacy collateral settled during migration. ```json theme={null} { "type": "legacy_collateral_settled", "legacyToken": "0x…", "vault": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | -------------------------------- | | `type` | string | `legacy_collateral_settled`. | | `legacyToken` | `address` (hex) | Legacy collateral token settled. | | `vault` | `address` (hex) | Vault the collateral settled to. | | `amount` | `uint256` (hex) | Amount settled. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the BinaryModule, NegRiskModule, AutoRedeemer, and combinatorial contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "combos", "confirmed": false } ``` # fees Source: https://docs.radion.app/websockets/channels/fees Exchange fees charged on Polymarket trades, from the CTFExchange and NegRiskCTFExchange contracts. The `fees` channel sends every exchange fee event from the Polymarket exchange contracts (v1 and v2). We decode each one and send it after the transaction is confirmed on-chain. Fees are no longer part of the `trading` channel; they stream here so you can track fee flow on its own. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "fees" } ``` With no filters, you get every confirmed fee event from all markets. Add filters to narrow to certain traders or tokens. ## Filters | Filter | Type | Required | Description | | ----------- | ---------- | -------- | ----------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Keeps fees whose `receiver` is a listed address. | | `token_ids` | `string[]` | No | Token IDs as decimal strings. Keeps fees on the listed outcome tokens. | This channel ignores `market_ids` and `min_usd` — they have no effect here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends both exchange fee events. | Event | `data.type` | Description | | -------------- | ---------------- | ----------------------------------------------------------------------------- | | `FeeChargedV1` | `fee_charged_v1` | A fee charged on exchange v1. Carries the receiver, token id, and fee amount. | | `FeeChargedV2` | `fee_charged_v2` | A fee charged on exchange v2. Simplified to the receiver and fee amount. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the [ABI](https://github.com/radion-app) decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "fees", "confirmed": true, "data": { "type": "fee_charged_v1", "receiver": "0x…", "tokenId": "0x…", "amount": "0x…" } } ``` ### `fee_charged_v1` `FeeCharged` on CTFExchange v1 / NegRiskCTFExchange v1. Carries the receiver, the token the fee was taken on, and the fee amount. ```json theme={null} { "type": "fee_charged_v1", "receiver": "0x…", "tokenId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ---------- | --------------- | ----------------------------------- | | `type` | string | `fee_charged_v1`. | | `receiver` | `address` (hex) | Address that received the fee. | | `tokenId` | `uint256` (hex) | Outcome token the fee was taken on. | | `amount` | `uint256` (hex) | Fee amount. | ### `fee_charged_v2` `FeeCharged` on CTFExchange v2 / NegRiskCTFExchange v2. The v2 signature dropped `tokenId`, so it carries only the receiver and the amount. ```json theme={null} { "type": "fee_charged_v2", "receiver": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ---------- | --------------- | ------------------------------ | | `type` | string | `fee_charged_v2`. | | `receiver` | `address` (hex) | Address that received the fee. | | `amount` | `uint256` (hex) | Fee amount. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending exchange transactions, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "fees", "confirmed": false } ``` # lifecycle Source: https://docs.radion.app/websockets/channels/lifecycle Market creation and preparation events from the Polymarket NegRisk adapter, CTF base layer, module, and exchange contracts. The `lifecycle` channel sends every market setup and preparation event: markets, events, conditions, questions, tokens, and combinatorial conditions being made ready to trade. They come from the NegRisk adapter, the CTF base layer, the module contracts, and the exchange contracts. We decode each one and send it after the transaction is confirmed on-chain. Settlement events (resolutions, reported outcomes, pauses) moved to the [`resolution`](/websockets/channels/resolution) channel. This channel is now setup and preparation only. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "lifecycle" } ``` With no filters, you get every confirmed lifecycle event from all markets. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | -------------------------------------------------------------------- | | `market_ids` | `string[]` | No | Condition/market IDs as hex strings. Keeps events for those markets. | | `token_ids` | `string[]` | No | Token IDs as decimal strings. Keeps events for the listed tokens. | `market_ids` and `token_ids` are one axis. `wallets` and `min_usd` are ignored here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends all seven market and condition preparation events. See [Onchain Data](/concepts/onchain-data) for the part each event plays in the market lifecycle. | Event | `data.type` | Description | | -------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `MarketPrepared` | `market_prepared` | A NegRisk market set up. Links a market id to its oracle, fee bips, and setup data. | | `EventPrepared` | `event_prepared` | A new event set up in a BinaryModule or NegRiskModule. Carries the event id, condition count, and legacy event id. | | `ConditionPreparation` | `condition_preparation` | A new CTF condition set up. Links the condition id to its oracle, question id, and outcome slot count. | | `TokenRegistered` | `token_registered` | An exchange token pair registered. Maps token0 and token1 to a condition id, which links fills to markets. | | `NegRiskQuestionPrepared` | `neg_risk_question_prepared` | A NegRisk question set up in a market. Links a question to its market id and index. | | `CombinatorialConditionPrepared` | `combinatorial_condition_prepared` | A new combinatorial condition set up. Carries the condition id and the leg position id array. | | `MigrationConditionRegistered` | `migration_condition_registered` | A condition registered for migration. Maps a v2 condition id to its legacy condition id. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "lifecycle", "confirmed": true, "data": { "type": "condition_preparation", "conditionId": "0x…", "oracle": "0x…", "…": "…" } } ``` ### `market_prepared` `MarketPrepared` on the NegRiskAdapter. A neg-risk market set up, linked to its oracle, fee, and raw setup data. ```json theme={null} { "type": "market_prepared", "marketId": "0x…", "oracle": "0x…", "feeBips": "0x…", "data": "0x…" } ``` | Field | Solidity type | Description | | ---------- | --------------- | ------------------------------------------------- | | `type` | string | `market_prepared`. | | `marketId` | `bytes32` (hex) | Neg-risk market id being prepared. | | `oracle` | `address` (hex) | Oracle that will report outcomes for this market. | | `feeBips` | `uint256` (hex) | Market fee in basis points. | | `data` | `bytes` (hex) | Raw setup data passed at market creation. | ### `event_prepared` `EventPrepared` on the BinaryModule / NegRiskModule. A new event scaffold, with its condition count and legacy id. ```json theme={null} { "type": "event_prepared", "eventId": "0x…", "conditionCount": "0x…", "legacyEventId": "0x…" } ``` | Field | Solidity type | Description | | ---------------- | --------------- | ----------------------------------- | | `type` | string | `event_prepared`. | | `eventId` | `bytes29` (hex) | Module event id being prepared. | | `conditionCount` | `uint256` (hex) | Number of conditions in the event. | | `legacyEventId` | `bytes32` (hex) | Matching id from the legacy system. | ### `condition_preparation` `ConditionPreparation` on ConditionalTokens. A new CTF condition registered with its oracle, question, and outcome slot count. ```json theme={null} { "type": "condition_preparation", "conditionId": "0x…", "oracle": "0x…", "questionId": "0x…", "outcomeSlotCount": "0x2" } ``` | Field | Solidity type | Description | | ------------------ | --------------- | --------------------------------------------------------- | | `type` | string | `condition_preparation`. | | `conditionId` | `bytes32` (hex) | Condition id being set up. | | `oracle` | `address` (hex) | Oracle that will report the outcome. | | `questionId` | `bytes32` (hex) | Question id linked to this condition. | | `outcomeSlotCount` | `uint256` (hex) | Number of outcome slots (for example `"0x2"` for binary). | ### `token_registered` `TokenRegistered` on the exchange. Maps the two outcome-token ids to a condition id, which is how fills link back to a market. ```json theme={null} { "type": "token_registered", "token0": "0x…", "token1": "0x…", "conditionId": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | -------------------------------- | | `type` | string | `token_registered`. | | `token0` | `uint256` (hex) | First outcome-token id. | | `token1` | `uint256` (hex) | Second outcome-token id. | | `conditionId` | `bytes32` (hex) | Condition both tokens belong to. | ### `neg_risk_question_prepared` `QuestionPrepared` on the NegRiskAdapter. A question added to a neg-risk market, with its index and raw setup data. ```json theme={null} { "type": "neg_risk_question_prepared", "marketId": "0x…", "questionId": "0x…", "index": "0x…", "data": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ------------------------------------------- | | `type` | string | `neg_risk_question_prepared`. | | `marketId` | `bytes32` (hex) | Neg-risk market the question belongs to. | | `questionId` | `bytes32` (hex) | Question id being prepared. | | `index` | `uint256` (hex) | Question index within the market. | | `data` | `bytes` (hex) | Raw setup data passed at question creation. | ### `combinatorial_condition_prepared` `CombinatorialConditionPrepared` on the combinatorial module. A combinatorial condition set up over a set of leg position ids. ```json theme={null} { "type": "combinatorial_condition_prepared", "conditionId": "0x…", "legs": ["0x…", "0x…"] } ``` | Field | Solidity type | Description | | ------------- | ----------------- | ----------------------------------------------- | | `type` | string | `combinatorial_condition_prepared`. | | `conditionId` | `bytes31` (hex) | Combinatorial condition id being set up. | | `legs` | `uint256[]` (hex) | Position ids that make up the condition's legs. | ### `migration_condition_registered` `MigrationConditionRegistered` on a module. Maps a v2 condition id to its legacy condition id so positions can migrate. ```json theme={null} { "type": "migration_condition_registered", "v2ConditionId": "0x…", "legacyConditionId": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | --------------------------------- | | `type` | string | `migration_condition_registered`. | | `v2ConditionId` | `bytes31` (hex) | Condition id in the v2 system. | | `legacyConditionId` | `bytes32` (hex) | Matching id in the legacy system. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the NegRisk adapter, CTF, module, and exchange contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "lifecycle", "confirmed": false } ``` # oracle Source: https://docs.radion.app/websockets/channels/oracle UMA oracle lifecycle events — initialization, proposals, disputes, settlements, pauses, and emergency resolutions — from the UMAAdapter and UMAOptimisticOracle contracts. The `oracle` channel sends every UMA oracle lifecycle event from the UMAAdapter and UMAOptimisticOracle contracts. We decode each one and send it after the transaction is confirmed on-chain. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "oracle" } ``` With no filters, you get every confirmed oracle event from all markets. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | -------------------------------------------------------------------- | | `market_ids` | `string[]` | No | Condition/market IDs as hex strings. Keeps events for those markets. | This channel accepts only `market_ids`. `wallets`, `token_ids`, and `min_usd` are ignored here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends all sixteen UMA resolution events. See [Onchain Data](/concepts/onchain-data) for the part each event plays in the full oracle lifecycle. ### UMAAdapter | Event | `data.type` | Description | | ------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | | `UmaAdapterQuestionInitialized` | `uma_adapter_question_initialized` | A UMA-resolved question was started. | | `UmaAdapterQuestionResolved` | `uma_adapter_question_resolved` | A UMA question reached its final result. | | `UmaAdapterQuestionEmergencyResolved` | `uma_adapter_question_emergency_resolved` | An emergency override result. Means an admin stepped in, outside the normal dispute flow. | | `UmaAdapterQuestionFlagged` | `uma_adapter_question_flagged` | A question was flagged for review. | | `UmaAdapterQuestionPaused` | `uma_adapter_question_paused` | A UMA question was paused part-way through. | | `UmaAdapterQuestionUnpaused` | `uma_adapter_question_unpaused` | A paused UMA question started again. | | `UmaAdapterQuestionReset` | `uma_adapter_question_reset` | A question was reset back to its started state. | | `UmaAdapterAncillaryDataUpdated` | `uma_adapter_ancillary_data_updated` | The ancillary data for a question was updated. | ### UMAOptimisticOracle | Event | `data.type` | Description | | ------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------- | | `UmaOptimisticQuestionInitialized` | `uma_optimistic_question_initialized` | An optimistic oracle question was started. | | `UmaOptimisticQuestionResolved` | `uma_optimistic_question_resolved` | A question reached its final result via the optimistic oracle. | | `UmaOptimisticQuestionPaused` | `uma_optimistic_question_paused` | An optimistic oracle question was paused. | | `UmaOptimisticQuestionUnpaused` | `uma_optimistic_question_unpaused` | An optimistic oracle question was unpaused. | | `UmaOptimisticQuestionSettled` | `uma_optimistic_question_settled` | A question settled after the dispute window closed. | | `UmaOptimisticResolutionDataRequested` | `uma_optimistic_resolution_data_requested` | A request for resolution data was sent. | | `UmaOptimisticQuestionUpdated` | `uma_optimistic_question_updated` | An optimistic oracle question was updated. | | `UmaOptimisticQuestionFlaggedForAdminResolution` | `uma_optimistic_question_flagged_for_admin_resolution` | A question was sent up to admin resolution. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "oracle", "confirmed": true, "data": { "type": "uma_adapter_question_resolved", "questionID": "0x…", "…": "…" } } ``` The field is `questionID` — capital `ID`, matching the on-chain event signature. `uint256` fields serialize as `0x…` hex; the signed `int256` `settledPrice` serializes as a decimal string (for example `"-1"`); `bytes` fields (`ancillaryData`, `update`) serialize as `0x…` hex. ### UMAAdapter events #### `uma_adapter_question_initialized` `QuestionInitialized` on the UMAAdapter. A UMA-resolved question started, with its reward and bond parameters. ```json theme={null} { "type": "uma_adapter_question_initialized", "questionID": "0x…", "requestTimestamp": "0x…", "creator": "0x…", "ancillaryData": "0x…", "rewardToken": "0x…", "reward": "0x…", "proposalBond": "0x…" } ``` | Field | Solidity type | Description | | ------------------ | --------------- | ------------------------------------------- | | `type` | string | `uma_adapter_question_initialized`. | | `questionID` | `bytes32` (hex) | Question id being initialized. | | `requestTimestamp` | `uint256` (hex) | Unix time (seconds) the request was made. | | `creator` | `address` (hex) | Address that created the question. | | `ancillaryData` | `bytes` (hex) | UMA ancillary data describing the question. | | `rewardToken` | `address` (hex) | ERC-20 token the reward is paid in. | | `reward` | `uint256` (hex) | Reward offered to the proposer. | | `proposalBond` | `uint256` (hex) | Bond a proposer must post. | #### `uma_adapter_question_resolved` `QuestionResolved` on the UMAAdapter. A UMA question reached its final result, with a settled price and payout split. ```json theme={null} { "type": "uma_adapter_question_resolved", "questionID": "0x…", "settledPrice": "1000000000000000000", "payouts": ["0x0", "0x1"] } ``` | Field | Solidity type | Description | | -------------- | ----------------- | ------------------------------------- | | `type` | string | `uma_adapter_question_resolved`. | | `questionID` | `bytes32` (hex) | Resolved question id. | | `settledPrice` | `int256` (string) | Signed price the question settled at. | | `payouts` | `uint256[]` (hex) | Payout numerator per outcome slot. | #### `uma_adapter_question_emergency_resolved` `QuestionEmergencyResolved` on the UMAAdapter. An admin override result, outside the normal dispute flow. ```json theme={null} { "type": "uma_adapter_question_emergency_resolved", "questionID": "0x…", "payouts": ["0x0", "0x1"] } ``` | Field | Solidity type | Description | | ------------ | ----------------- | ------------------------------------------ | | `type` | string | `uma_adapter_question_emergency_resolved`. | | `questionID` | `bytes32` (hex) | Question id resolved by override. | | `payouts` | `uint256[]` (hex) | Payout numerator per outcome slot. | #### `uma_adapter_question_flagged` `QuestionFlagged` on the UMAAdapter. A question flagged for review. ```json theme={null} { "type": "uma_adapter_question_flagged", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ------------------------------- | | `type` | string | `uma_adapter_question_flagged`. | | `questionID` | `bytes32` (hex) | Flagged question id. | #### `uma_adapter_question_paused` `QuestionPaused` on the UMAAdapter. A question paused part-way through. ```json theme={null} { "type": "uma_adapter_question_paused", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ------------------------------ | | `type` | string | `uma_adapter_question_paused`. | | `questionID` | `bytes32` (hex) | Paused question id. | #### `uma_adapter_question_unpaused` `QuestionUnpaused` on the UMAAdapter. A paused question resumed. ```json theme={null} { "type": "uma_adapter_question_unpaused", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | -------------------------------- | | `type` | string | `uma_adapter_question_unpaused`. | | `questionID` | `bytes32` (hex) | Resumed question id. | #### `uma_adapter_question_reset` `QuestionReset` on the UMAAdapter. A question reset back to its started state. ```json theme={null} { "type": "uma_adapter_question_reset", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ----------------------------- | | `type` | string | `uma_adapter_question_reset`. | | `questionID` | `bytes32` (hex) | Reset question id. | #### `uma_adapter_ancillary_data_updated` `AncillaryDataUpdated` on the UMAAdapter. The ancillary data for a question changed. ```json theme={null} { "type": "uma_adapter_ancillary_data_updated", "questionID": "0x…", "owner": "0x…", "update": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | -------------------------------------- | | `type` | string | `uma_adapter_ancillary_data_updated`. | | `questionID` | `bytes32` (hex) | Question whose ancillary data changed. | | `owner` | `address` (hex) | Owner that made the update. | | `update` | `bytes` (hex) | New ancillary data. | ### UMAOptimisticOracle events #### `uma_optimistic_question_initialized` `QuestionInitialized` on the UMAOptimisticOracle. An optimistic-oracle question started. ```json theme={null} { "type": "uma_optimistic_question_initialized", "questionID": "0x…", "ancillaryData": "0x…", "resolutionTime": "0x…", "rewardToken": "0x…", "reward": "0x…", "proposalBond": "0x…", "earlyResolutionEnabled": true } ``` | Field | Solidity type | Description | | ------------------------ | --------------- | ------------------------------------------- | | `type` | string | `uma_optimistic_question_initialized`. | | `questionID` | `bytes32` (hex) | Question id being initialized. | | `ancillaryData` | `bytes` (hex) | UMA ancillary data describing the question. | | `resolutionTime` | `uint256` (hex) | Unix time (seconds) resolution is expected. | | `rewardToken` | `address` (hex) | ERC-20 token the reward is paid in. | | `reward` | `uint256` (hex) | Reward offered to the proposer. | | `proposalBond` | `uint256` (hex) | Bond a proposer must post. | | `earlyResolutionEnabled` | `bool` | Whether early resolution is allowed. | #### `uma_optimistic_question_resolved` `QuestionResolved` on the UMAOptimisticOracle. A question reached its final result via the optimistic oracle. ```json theme={null} { "type": "uma_optimistic_question_resolved", "questionID": "0x…", "emergencyReport": false } ``` | Field | Solidity type | Description | | ----------------- | --------------- | --------------------------------------- | | `type` | string | `uma_optimistic_question_resolved`. | | `questionID` | `bytes32` (hex) | Resolved question id. | | `emergencyReport` | `bool` | `true` if resolved by emergency report. | #### `uma_optimistic_question_paused` `QuestionPaused` on the UMAOptimisticOracle. A question paused. ```json theme={null} { "type": "uma_optimistic_question_paused", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | --------------------------------- | | `type` | string | `uma_optimistic_question_paused`. | | `questionID` | `bytes32` (hex) | Paused question id. | #### `uma_optimistic_question_unpaused` `QuestionUnpaused` on the UMAOptimisticOracle. A paused question resumed. ```json theme={null} { "type": "uma_optimistic_question_unpaused", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ----------------------------------- | | `type` | string | `uma_optimistic_question_unpaused`. | | `questionID` | `bytes32` (hex) | Resumed question id. | #### `uma_optimistic_question_settled` `QuestionSettled` on the UMAOptimisticOracle. A question settled after the dispute window closed. ```json theme={null} { "type": "uma_optimistic_question_settled", "questionID": "0x…", "settledPrice": "1000000000000000000", "earlyResolution": false } ``` | Field | Solidity type | Description | | ----------------- | ----------------- | --------------------------------------- | | `type` | string | `uma_optimistic_question_settled`. | | `questionID` | `bytes32` (hex) | Settled question id. | | `settledPrice` | `int256` (string) | Signed price the question settled at. | | `earlyResolution` | `bool` | `true` if settled via early resolution. | #### `uma_optimistic_resolution_data_requested` `ResolutionDataRequested` on the UMAOptimisticOracle. A request for resolution data was sent to UMA. ```json theme={null} { "type": "uma_optimistic_resolution_data_requested", "requestor": "0x…", "requestTimestamp": "0x…", "questionID": "0x…", "identifier": "0x…", "ancillaryData": "0x…", "rewardToken": "0x…", "reward": "0x…", "proposalBond": "0x…", "earlyResolution": false } ``` | Field | Solidity type | Description | | ------------------ | --------------- | ------------------------------------------- | | `type` | string | `uma_optimistic_resolution_data_requested`. | | `requestor` | `address` (hex) | Address that requested resolution data. | | `requestTimestamp` | `uint256` (hex) | Unix time (seconds) of the request. | | `questionID` | `bytes32` (hex) | Question the request is for. | | `identifier` | `bytes32` (hex) | UMA price identifier. | | `ancillaryData` | `bytes` (hex) | UMA ancillary data for the request. | | `rewardToken` | `address` (hex) | ERC-20 token the reward is paid in. | | `reward` | `uint256` (hex) | Reward offered to the proposer. | | `proposalBond` | `uint256` (hex) | Bond a proposer must post. | | `earlyResolution` | `bool` | Whether early resolution is requested. | #### `uma_optimistic_question_updated` `QuestionUpdated` on the UMAOptimisticOracle. An optimistic-oracle question's parameters changed. ```json theme={null} { "type": "uma_optimistic_question_updated", "questionID": "0x…", "ancillaryData": "0x…", "resolutionTime": "0x…", "rewardToken": "0x…", "reward": "0x…", "proposalBond": "0x…", "earlyResolutionEnabled": true } ``` | Field | Solidity type | Description | | ------------------------ | --------------- | ------------------------------------ | | `type` | string | `uma_optimistic_question_updated`. | | `questionID` | `bytes32` (hex) | Updated question id. | | `ancillaryData` | `bytes` (hex) | New UMA ancillary data. | | `resolutionTime` | `uint256` (hex) | New expected resolution time. | | `rewardToken` | `address` (hex) | ERC-20 token the reward is paid in. | | `reward` | `uint256` (hex) | New reward amount. | | `proposalBond` | `uint256` (hex) | New proposal bond. | | `earlyResolutionEnabled` | `bool` | Whether early resolution is allowed. | #### `uma_optimistic_question_flagged_for_admin_resolution` `QuestionFlaggedForAdminResolution` on the UMAOptimisticOracle. A question escalated to admin resolution. ```json theme={null} { "type": "uma_optimistic_question_flagged_for_admin_resolution", "questionID": "0x…" } ``` | Field | Solidity type | Description | | ------------ | --------------- | ------------------------------------------------------- | | `type` | string | `uma_optimistic_question_flagged_for_admin_resolution`. | | `questionID` | `bytes32` (hex) | Question escalated to admin resolution. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the UMAAdapter and UMAOptimisticOracle contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "oracle", "confirmed": false } ``` # Channels Source: https://docs.radion.app/websockets/channels/overview Confirmed and mempool channels available on the Radion WebSocket. A channel is a named stream of Polymarket activity. We read it from the chain and decode it for you. [Subscribe](/websockets/subscribe) to a channel by its name. Some channels accept [filters](/websockets/filters); the two filter channels require one. There are three kinds of channel: * **Topic channels** carry a fixed set of decoded events. Every decoded event routes to exactly one topic channel, so you can always tell which channel an event arrives on. * **Filter channels** (`wallets`, `markets`) are cross-cutting. They carry the same events as the topic channels, but scoped to the wallets, markets, or tokens you name. * **CLOB channels** (`clob.*`) are a separate family. They carry the Polymarket order-book feed — book, prices, trades, midpoint — proxied over the same socket. See [CLOB channels](#clob-channels) below. ## Confirmed channels A confirmed channel sends you events after the transaction is in a block. This is final, on-chain truth. | Channel | Events it carries | Accepted filters | | ----------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------- | | [`trading`](/websockets/channels/trading) | Order fills, matches, cancels, preapprovals, and trading pauses. | `wallets`, `market_ids`, `token_ids`, `min_usd` | | [`fees`](/websockets/channels/fees) | Exchange fees charged on trades. | `wallets`, `token_ids` | | [`oracle`](/websockets/channels/oracle) | UMA Adapter and UMA Optimistic Oracle question events. | `market_ids` | | [`resolution`](/websockets/channels/resolution) | Condition resolutions, reported results, and resolution pauses. | `market_ids` | | [`lifecycle`](/websockets/channels/lifecycle) | Market, event, condition, and token preparation. | `market_ids`, `token_ids` | | [`positions`](/websockets/channels/positions) | CTF base-layer splits, merges, and payout redemptions. | `wallets`, `market_ids`, `token_ids` | | [`combos`](/websockets/channels/combos) | Module, neg-risk, combinatorial, bridge, and migration position events. | `wallets`, `market_ids`, `token_ids` | | [`transfers`](/websockets/channels/transfers) | ERC-1155 outcome-token transfers. | `wallets`, `token_ids` | | [`accounts`](/websockets/channels/accounts) | Proxy and deposit wallet creation. | `wallets` | | `wallets` *(filter channel)* | Any event that touches one or more wallet addresses. | `wallets` (required, min 1) | | `markets` *(filter channel)* | Any event for the given market ids or token ids. | `market_ids` or `token_ids` (min 1) | An empty filter (or one you leave out) means the whole channel. A filter a channel does not accept is ignored — it is not an error. See [Filters](/websockets/filters) for the combination rule. ## Pending feed Every channel has a pending feed. Set `confirmed: false` on the subscribe message to get pending transactions before they are in a block, on the same bare channel name. These are guesses, not final truth. See [Mempool](/websockets/mempool) for the frame shape and how to handle them. | Channel (with `confirmed: false`) | Description | | --------------------------------- | ---------------------------------------------------------------------------------------------------- | | `trading` | Pending transactions sent to Polymarket exchange contracts. | | `fees` | Pending exchange transactions that charge a fee. | | `oracle` | Pending transactions sent to UMA oracle contracts. | | `resolution` | Pending transactions sent to resolution and settlement paths. | | `lifecycle` | Pending transactions sent to market lifecycle contracts. | | `positions` | Pending transactions sent to CTF and collateral-adapter split, merge, and redemption paths. | | `combos` | Pending transactions sent to module, combinatorial, bridge, and migration contracts. | | `transfers` | Pending ERC-1155 transfer transactions. | | `accounts` | Pending wallet-factory transactions. | | `wallets` | Pending transactions that involve a `wallets` address (sender, recipient, or decoded from calldata). | | `markets` | Pending transactions that involve a `market_ids` or `token_ids` decoded from calldata. | Pending messages are guesses. A pending transaction can be dropped, replaced, reordered, or reverted. For final on-chain truth, use the confirmed feed (`confirmed: true`, the default). ## Filter availability This table shows which filters each channel accepts. A filter a channel does not accept is ignored: | Channel | `wallets` | `market_ids` | `token_ids` | `min_usd` | | ------------ | :-------: | :----------: | :---------: | :-------: | | `trading` | ✓ | ✓ | ✓ | ✓ | | `fees` | ✓ | — | ✓ | — | | `oracle` | — | ✓ | — | — | | `resolution` | — | ✓ | — | — | | `lifecycle` | — | ✓ | ✓ | — | | `positions` | ✓ | ✓ | ✓ | — | | `combos` | ✓ | ✓ | ✓ | — | | `transfers` | ✓ | — | ✓ | — | | `accounts` | ✓ | — | — | — | | `wallets` | required | — | — | — | | `markets` | — | required\* | required\* | — | \* The `markets` channel needs at least one of `market_ids` or `token_ids`. See [Filters](/websockets/filters) for the full reference, including how filters work on mempool. ## CLOB channels The `clob.*` channels are a separate family from the topic and filter channels above. They carry the Polymarket CLOB (order-book) feed — proxied over the same Radion WebSocket — instead of decoded on-chain events. | Channel | Sends | Accepted filters | | ------------------- | ---------------------------------------------- | ---------------------- | | `clob.book` | Full order-book snapshot (bid and ask levels). | `token_ids` (required) | | `clob.prices` | Price-change batches with new best bid/ask. | `token_ids` (required) | | `clob.last_trade` | Last trade price and size. | `token_ids` (required) | | `clob.midpoint` | Midpoint between best bid and ask. | `token_ids` (required) | | `clob.tick_size` | Tick-size changes. | `token_ids` (required) | | `clob.best_bid_ask` | Best bid and best ask. | `token_ids` (required) | They differ from the topic channels: each **requires** a `token_ids` filter, the `data` payload has **no** `type` discriminator, and they have **no** pending feed (`confirmed: false` does not apply). See [CLOB channels](/websockets/channels/clob) for the payload shapes. ## Filter channels `wallets` and `markets` are not new event types. They are **scoped views** of the same events the topic channels already send. If you subscribe to `wallets` with a `wallets` filter, you get the exact same payload shapes as `trading`, `positions`, and the rest, but only for events that touch your watched addresses. * **`wallets`** — sends any confirmed event where a watched address is a sender, recipient, or decoded participant. It **requires** at least one address, or the subscribe fails. For payload shapes, see the topic channel pages. * **`markets`** — sends events that match the `market_ids` or `token_ids` you pass. It **requires** at least one id. Same payload shapes as the topic channels. The `global` channel was removed. To watch everything, subscribe to each topic channel you care about. To scope across all topics at once, use `wallets` or `markets`. # positions Source: https://docs.radion.app/websockets/channels/positions Plain CTF base-layer position moves — splits, merges, and payout redemptions from the ConditionalTokens and collateral-adapter contracts. The `positions` channel sends every plain position move at the CTF base layer: collateral split into outcome positions, positions merged back, and payout redemptions. They come from the ConditionalTokens (CTF) contract and the CtfCollateralAdapter. We decode each one and send it after the transaction is confirmed on-chain. The `activity` channel was removed; its events split between this channel and `combos`. Module, neg-risk, and combinatorial position moves are on [`combos`](/websockets/channels/combos). ERC-1155 transfers are on [`transfers`](/websockets/channels/transfers). ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "positions" } ``` With no filters, you get every confirmed base-layer position event from all markets. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | -------------------------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Keeps events where a listed address is the initiator or holder. | | `market_ids` | `string[]` | No | Condition IDs as `bytes32` hex strings. Keeps events for those markets. | | `token_ids` | `string[]` | No | Token IDs as decimal strings. Keeps events for the listed position tokens. | `market_ids` and `token_ids` are one axis. `min_usd` is ignored here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends all six base-layer position events. See [Onchain Data](/concepts/onchain-data) for the part each event plays in the position and redemption lifecycle. | Event | `data.type` | Description | | --------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ | | `CtfPositionSplit` | `ctf_position_split` | Collateral split into conditional token positions at the CTF base layer. | | `CtfPositionsMerge` | `ctf_positions_merge` | Conditional token positions merged at the CTF base layer. | | `CtfPayoutRedemption` | `ctf_payout_redemption` | Conditional tokens redeemed into payout collateral at the CTF base layer. | | `CollateralPositionSplit` | `collateral_position_split` | Collateral split into outcome positions. Shows a position being created at the collateral-adapter layer. | | `CollateralPositionsMerged` | `collateral_positions_merged` | Outcome positions merged back at the collateral layer. Shows a position being closed before redemption. | | `PositionsRedeemed` | `positions_redeemed` | A redemption at the collateral-adapter level. Links resolved conditions to the collateral that was paid out. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "positions", "confirmed": true, "data": { "type": "positions_redeemed", "initiator": "0x…", "conditionId": "0x…", "…": "…" } } ``` The three `ctf_*` events come from the ConditionalTokens base layer and carry the full `collateralToken` / `parentCollectionId` context. The three collateral-adapter events are the higher-level wrapper and carry less. ### `ctf_position_split` `PositionSplit` on ConditionalTokens. Collateral (or a parent position) split into a set of outcome positions. ```json theme={null} { "type": "ctf_position_split", "stakeholder": "0x…", "collateralToken": "0x…", "parentCollectionId": "0x…", "conditionId": "0x…", "partition": ["0x…", "0x…"], "amount": "0x…" } ``` | Field | Solidity type | Description | | -------------------- | ----------------- | ---------------------------------------------------------- | | `type` | string | `ctf_position_split`. | | `stakeholder` | `address` (hex) | Address that split the position. | | `collateralToken` | `address` (hex) | ERC-20 collateral backing the positions. | | `parentCollectionId` | `bytes32` (hex) | Parent collection split from (zero for a top-level split). | | `conditionId` | `bytes32` (hex) | Condition the split is on. | | `partition` | `uint256[]` (hex) | Index sets describing the outcome slots created. | | `amount` | `uint256` (hex) | Amount of collateral or parent position split. | ### `ctf_positions_merge` `PositionsMerge` on ConditionalTokens. The inverse of a split — a set of outcome positions merged back into collateral or a parent position. ```json theme={null} { "type": "ctf_positions_merge", "stakeholder": "0x…", "collateralToken": "0x…", "parentCollectionId": "0x…", "conditionId": "0x…", "partition": ["0x…", "0x…"], "amount": "0x…" } ``` | Field | Solidity type | Description | | -------------------- | ----------------- | --------------------------------------------------- | | `type` | string | `ctf_positions_merge`. | | `stakeholder` | `address` (hex) | Address that merged the positions. | | `collateralToken` | `address` (hex) | ERC-20 collateral backing the positions. | | `parentCollectionId` | `bytes32` (hex) | Parent collection merged into (zero for top-level). | | `conditionId` | `bytes32` (hex) | Condition the merge is on. | | `partition` | `uint256[]` (hex) | Index sets describing the outcome slots merged. | | `amount` | `uint256` (hex) | Amount merged. | ### `ctf_payout_redemption` `PayoutRedemption` on ConditionalTokens. Resolved outcome tokens redeemed for their collateral payout. ```json theme={null} { "type": "ctf_payout_redemption", "redeemer": "0x…", "collateralToken": "0x…", "parentCollectionId": "0x…", "conditionId": "0x…", "indexSets": ["0x…", "0x…"], "payout": "0x…" } ``` | Field | Solidity type | Description | | -------------------- | ----------------- | -------------------------------------------- | | `type` | string | `ctf_payout_redemption`. | | `redeemer` | `address` (hex) | Address that redeemed the positions. | | `collateralToken` | `address` (hex) | ERC-20 collateral paid out. | | `parentCollectionId` | `bytes32` (hex) | Parent collection of the redeemed positions. | | `conditionId` | `bytes32` (hex) | Resolved condition being redeemed. | | `indexSets` | `uint256[]` (hex) | Index sets of the outcome slots redeemed. | | `payout` | `uint256` (hex) | Total collateral paid out. | ### `collateral_position_split` `PositionSplit` on the CtfCollateralAdapter (also NegRiskCtfCollateralAdapter). The higher-level split — collateral into outcome positions. ```json theme={null} { "type": "collateral_position_split", "initiator": "0x…", "conditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | -------------------------------- | | `type` | string | `collateral_position_split`. | | `initiator` | `address` (hex) | Address that split the position. | | `conditionId` | `bytes32` (hex) | Condition the split is on. | | `amount` | `uint256` (hex) | Amount of collateral split. | ### `collateral_positions_merged` `PositionsMerged` on the CtfCollateralAdapter. The higher-level merge — outcome positions back into collateral. ```json theme={null} { "type": "collateral_positions_merged", "initiator": "0x…", "conditionId": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ------------- | --------------- | ---------------------------------- | | `type` | string | `collateral_positions_merged`. | | `initiator` | `address` (hex) | Address that merged the positions. | | `conditionId` | `bytes32` (hex) | Condition the merge is on. | | `amount` | `uint256` (hex) | Amount merged. | ### `positions_redeemed` `PositionsRedeemed` on the CtfCollateralAdapter. A resolved condition paid out into collateral, with the per-outcome amounts redeemed. ```json theme={null} { "type": "positions_redeemed", "initiator": "0x…", "conditionId": "0x…", "amounts": ["0x…", "0x…"], "payout": "0x…" } ``` | Field | Solidity type | Description | | ------------- | ----------------- | ----------------------------------------------------- | | `type` | string | `positions_redeemed`. | | `initiator` | `address` (hex) | Address that started the redemption. | | `conditionId` | `bytes32` (hex) | Condition being redeemed. | | `amounts` | `uint256[]` (hex) | Amount redeemed for each outcome slot, in slot order. | | `payout` | `uint256` (hex) | Total collateral paid out. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the CTF and collateral-adapter contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "positions", "confirmed": false } ``` # resolution Source: https://docs.radion.app/websockets/channels/resolution Settlement outcomes from the Polymarket module and CTF contracts — condition resolutions, reported results, and resolution pauses. The `resolution` channel sends every settlement event: when a condition resolves, a result is reported, or resolution is paused. They come from the CTF base layer, the NegRisk adapter, and the BinaryModule and NegRiskModule contracts. We decode each one and send it after the transaction is confirmed on-chain. These events used to be split across the old `lifecycle` and `combos` channels. They now stream here as one settlement feed. Market **setup** events (not settlement) stay on [`lifecycle`](/websockets/channels/lifecycle). ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "resolution" } ``` With no filters, you get every confirmed resolution event from all markets. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | ----------------------------------------------------------------------- | | `market_ids` | `string[]` | No | Condition IDs as `bytes32` hex strings. Keeps events for those markets. | This channel accepts only `market_ids`. `wallets`, `token_ids`, and `min_usd` are ignored here. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends all eight settlement events. See [Onchain Data](/concepts/onchain-data) for the part each event plays in the resolution lifecycle. | Event | `data.type` | Description | | --------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------- | | `ConditionResolution` | `condition_resolution` | The final payout numerators saved for a CTF condition. Sets how a resolved condition splits collateral. | | `ConditionResolved` | `condition_resolved` | A condition resolved inside the module system. Carries the condition id and result numerators. | | `OutcomeReported` | `outcome_reported` | A reported neg-risk outcome. Gives resolution context for a question in a neg-risk market. | | `ResultReported` | `result_reported` | A result reported to a module. Carries the resolver address, condition id, and result numerators. | | `ResolutionPaused` | `resolution_paused` | Resolution paused for a module id. Means market resolution is blocked for now. | | `ResolutionUnpaused` | `resolution_unpaused` | Resolution unpaused for a module id. Means market resolution can run again. | | `ResolverPaused` | `resolver_paused` | A resolver paused at the module level. Carries the resolver address and timestamp. | | `ResolverUnpaused` | `resolver_unpaused` | A resolver unpaused at the module level. Carries the resolver address. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "resolution", "confirmed": true, "data": { "type": "condition_resolution", "conditionId": "0x…", "oracle": "0x…", "…": "…" } } ``` `conditionId` is `bytes32` on the CTF base-layer events (`condition_resolution`) but the compact `bytes31` module id on the module events (`condition_resolved`, `result_reported`). Both serialize as `0x…` hex; the byte length differs. ### `condition_resolution` `ConditionResolution` on ConditionalTokens. The final payout numerators saved for a resolved CTF condition. ```json theme={null} { "type": "condition_resolution", "conditionId": "0x…", "oracle": "0x…", "questionId": "0x…", "outcomeSlotCount": "0x2", "payoutNumerators": ["0x1", "0x0"] } ``` | Field | Solidity type | Description | | ------------------ | ----------------- | -------------------------------------------------------------- | | `type` | string | `condition_resolution`. | | `conditionId` | `bytes32` (hex) | Condition being resolved. | | `oracle` | `address` (hex) | Oracle that reported the outcome. | | `questionId` | `bytes32` (hex) | Question id linked to this condition. | | `outcomeSlotCount` | `uint256` (hex) | Number of outcome slots for this condition. | | `payoutNumerators` | `uint256[]` (hex) | Payout numerator per outcome slot. Sets how collateral splits. | ### `condition_resolved` `ConditionResolved` on the BinaryModule / NegRiskModule. A condition resolved inside the module system. ```json theme={null} { "type": "condition_resolved", "conditionId": "0x…", "result": ["0x1", "0x0"] } ``` | Field | Solidity type | Description | | ------------- | ----------------- | ---------------------------------- | | `type` | string | `condition_resolved`. | | `conditionId` | `bytes31` (hex) | Module condition id that resolved. | | `result` | `uint256[]` (hex) | Result numerator per outcome slot. | ### `outcome_reported` `OutcomeReported` on the NegRiskAdapter. A single neg-risk question's outcome reported. ```json theme={null} { "type": "outcome_reported", "marketId": "0x…", "questionId": "0x…", "outcome": true } ``` | Field | Solidity type | Description | | ------------ | --------------- | ---------------------------------------------- | | `type` | string | `outcome_reported`. | | `marketId` | `bytes32` (hex) | Neg-risk market the question belongs to. | | `questionId` | `bytes32` (hex) | Question that was reported. | | `outcome` | `bool` | Reported outcome (`true` = yes, `false` = no). | ### `result_reported` `ResultReported` on a module. A resolver pushed a result into a module condition. ```json theme={null} { "type": "result_reported", "resolver": "0x…", "conditionId": "0x…", "result": ["0x1", "0x0"] } ``` | Field | Solidity type | Description | | ------------- | ----------------- | ---------------------------------- | | `type` | string | `result_reported`. | | `resolver` | `address` (hex) | Resolver that reported the result. | | `conditionId` | `bytes31` (hex) | Module condition id. | | `result` | `uint256[]` (hex) | Result numerator per outcome slot. | ### `resolution_paused` `ResolutionPaused` on a module. Resolution blocked for a module id, with the pause timestamp. ```json theme={null} { "type": "resolution_paused", "id": "0x…", "timestamp": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ------------------------------------------ | | `type` | string | `resolution_paused`. | | `id` | `bytes31` (hex) | Module id whose resolution was paused. | | `timestamp` | `uint256` (hex) | Unix time (seconds) the pause took effect. | ### `resolution_unpaused` `ResolutionUnpaused` on a module. Resolution allowed again for a module id. ```json theme={null} { "type": "resolution_unpaused", "id": "0x…" } ``` | Field | Solidity type | Description | | ------ | --------------- | ----------------------------------- | | `type` | string | `resolution_unpaused`. | | `id` | `bytes31` (hex) | Module id whose resolution resumed. | ### `resolver_paused` `ResolverPaused` on a module. A resolver paused at the module level, with the pause timestamp. ```json theme={null} { "type": "resolver_paused", "resolver": "0x…", "timestamp": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ------------------------------------------ | | `type` | string | `resolver_paused`. | | `resolver` | `address` (hex) | Resolver that was paused. | | `timestamp` | `uint256` (hex) | Unix time (seconds) the pause took effect. | ### `resolver_unpaused` `ResolverUnpaused` on a module. A resolver resumed at the module level. ```json theme={null} { "type": "resolver_unpaused", "resolver": "0x…" } ``` | Field | Solidity type | Description | | ---------- | --------------- | ---------------------- | | `type` | string | `resolver_unpaused`. | | `resolver` | `address` (hex) | Resolver that resumed. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the CTF, NegRisk adapter, and module contracts, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "resolution", "confirmed": false } ``` # trading Source: https://docs.radion.app/websockets/channels/trading Confirmed order flow from the Polymarket exchange contracts — fills, matches, cancels, preapprovals, and trading pauses. The `trading` channel sends every order-flow event from the Polymarket exchange contracts (CTFExchange and NegRiskCTFExchange, v1 and v2). We decode each one and send it after the transaction is confirmed on-chain. Exchange fees are no longer on this channel. They moved to the [`fees`](/websockets/channels/fees) channel. ERC-1155 position transfers are on [`transfers`](/websockets/channels/transfers). ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "trading" } ``` With no filters, you get every confirmed order-flow event from all markets. Add filters to narrow to certain traders, markets, tokens, or trade sizes. ## Filters | Filter | Type | Required | Description | | ------------ | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Empty or left out means all traders. Otherwise, only events where a listed address is the maker or taker. | | `market_ids` | `string[]` | No | Condition IDs as `bytes32` hex strings. Keeps events for the listed markets. | | `token_ids` | `string[]` | No | Token IDs as decimal strings. Keeps events for the listed outcome tokens. | | `min_usd` | `integer` | No | Smallest trade size in whole USD. Empty or left out means all sizes. Only kept if a fill's USD size is at or above this threshold. | `market_ids` and `token_ids` are one axis — a market's condition id and its outcome token ids both identify it, so an event matches if it hits either list. All axes combine with AND: a wallet **and** a market **and** a size, if you set several. See [Filters](/websockets/filters) for the full combination rule. ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "trading", "filters": { "wallets": ["0x1234…abcd"], "min_usd": 100 } } ``` `min_usd` is a `trading`-only filter. The `large_trades` channel was removed — subscribe to `trading` with `min_usd` to get only large fills. ## Events This channel sends all nine exchange order-flow events. See [Onchain Data](/concepts/onchain-data) for the part each event plays in the full order lifecycle. | Event | `data.type` | Description | | ----------------------------- | ------------------------------- | ------------------------------------------------------------------------------------ | | `OrderFilledV1` | `order_filled_v1` | A fill on exchange v1. Carries maker, taker, asset ids, and amounts. | | `OrderFilledV2` | `order_filled_v2` | A fill on exchange v2. Also adds `side`, `tokenId`, `builder`, and `metadata`. | | `OrdersMatchedV1` | `orders_matched_v1` | The matched order pair around a v1 fill. Shows how taker intent met maker liquidity. | | `OrdersMatchedV2` | `orders_matched_v2` | The matched order pair around a v2 fill. Also includes `side` and `tokenId`. | | `OrderCancelled` | `order_cancelled` | An order cancelled by its hash, before it filled. | | `OrderPreapproved` | `order_preapproved` | A v2 order preapproved for later execution, by its hash. | | `OrderPreapprovalInvalidated` | `order_preapproval_invalidated` | A v2 order preapproval invalidated, by its hash. | | `TradingPaused` | `trading_paused` | An operator paused trading on an exchange. Carries the pauser address. | | `TradingUnpaused` | `trading_unpaused` | Trading resumed after a pause. Carries the pauser address. | Do you want a trader's full activity, not just order flow? That means transfers, redemptions, splits, and position moves. Use the [`wallets`](/websockets/channels/overview#filter-channels) filter channel instead. It sends every event type that involves the address. ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "trading", "confirmed": true, "data": { "type": "order_filled_v2", "orderHash": "0x…", "maker": "0x…", "…": "…" } } ``` ### `order_filled_v1` `OrderFilled` on CTFExchange v1 / NegRiskCTFExchange v1. The v1 signature identifies both sides of the trade with asset ids, not a single `tokenId`. ```json theme={null} { "type": "order_filled_v1", "orderHash": "0x…", "maker": "0x…", "taker": "0x…", "makerAssetId": "0x…", "takerAssetId": "0x…", "makerAmountFilled": "0x…", "takerAmountFilled": "0x…", "fee": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | --------------------------------------------------- | | `type` | string | `order_filled_v1`. | | `orderHash` | `bytes32` (hex) | Hash of the filled order. | | `maker` | `address` (hex) | Maker address. | | `taker` | `address` (hex) | Taker address. | | `makerAssetId` | `uint256` (hex) | Asset id the maker gave. `0` means USDC collateral. | | `takerAssetId` | `uint256` (hex) | Asset id the taker gave. `0` means USDC collateral. | | `makerAmountFilled` | `uint256` (hex) | Amount of the maker asset filled. | | `takerAmountFilled` | `uint256` (hex) | Amount of the taker asset filled. | | `fee` | `uint256` (hex) | Fee recorded on the fill. | ### `order_filled_v2` `OrderFilled` on CTFExchange v2 / NegRiskCTFExchange v2. The v2 signature replaces the asset-id pair with a `side` and a single `tokenId`, and adds `builder` and `metadata`. ```json theme={null} { "type": "order_filled_v2", "orderHash": "0x…", "maker": "0x…", "taker": "0x…", "side": 0, "tokenId": "0x…", "makerAmountFilled": "0x…", "takerAmountFilled": "0x…", "fee": "0x…", "builder": "0x…", "metadata": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | ---------------------------------- | | `type` | string | `order_filled_v2`. | | `orderHash` | `bytes32` (hex) | Hash of the filled order. | | `maker` | `address` (hex) | Maker address. | | `taker` | `address` (hex) | Taker address. | | `side` | `uint8` | `0` = buy, `1` = sell. | | `tokenId` | `uint256` (hex) | Outcome token traded. | | `makerAmountFilled` | `uint256` (hex) | Amount the maker filled. | | `takerAmountFilled` | `uint256` (hex) | Amount the taker filled. | | `fee` | `uint256` (hex) | Fee recorded on the fill. | | `builder` | `bytes32` (hex) | Builder tag attached to the order. | | `metadata` | `bytes32` (hex) | Order metadata. | ### `orders_matched_v1` `OrdersMatched` on the v1 exchanges. The match record around a v1 fill — the taker order hash and how much of each asset moved. ```json theme={null} { "type": "orders_matched_v1", "takerOrderHash": "0x…", "takerOrderMaker": "0x…", "makerAssetId": "0x…", "takerAssetId": "0x…", "makerAmountFilled": "0x…", "takerAmountFilled": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | --------------------------------------------- | | `type` | string | `orders_matched_v1`. | | `takerOrderHash` | `bytes32` (hex) | Hash of the taker order that drove the match. | | `takerOrderMaker` | `address` (hex) | Maker of the taker order. | | `makerAssetId` | `uint256` (hex) | Asset id given by the maker side. | | `takerAssetId` | `uint256` (hex) | Asset id given by the taker side. | | `makerAmountFilled` | `uint256` (hex) | Total maker asset filled across the match. | | `takerAmountFilled` | `uint256` (hex) | Total taker asset filled across the match. | ### `orders_matched_v2` `OrdersMatched` on the v2 exchanges. Like v1, but carries a `side` and single `tokenId` instead of the asset-id pair. ```json theme={null} { "type": "orders_matched_v2", "takerOrderHash": "0x…", "takerOrderMaker": "0x…", "side": 0, "tokenId": "0x…", "makerAmountFilled": "0x…", "takerAmountFilled": "0x…" } ``` | Field | Solidity type | Description | | ------------------- | --------------- | --------------------------------------------- | | `type` | string | `orders_matched_v2`. | | `takerOrderHash` | `bytes32` (hex) | Hash of the taker order that drove the match. | | `takerOrderMaker` | `address` (hex) | Maker of the taker order. | | `side` | `uint8` | `0` = buy, `1` = sell. | | `tokenId` | `uint256` (hex) | Outcome token matched. | | `makerAmountFilled` | `uint256` (hex) | Total maker amount filled across the match. | | `takerAmountFilled` | `uint256` (hex) | Total taker amount filled across the match. | ### `order_cancelled` `OrderCancelled`. An order cancelled by its hash before it filled. ```json theme={null} { "type": "order_cancelled", "orderHash": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ---------------------------- | | `type` | string | `order_cancelled`. | | `orderHash` | `bytes32` (hex) | Hash of the cancelled order. | ### `order_preapproved` `OrderPreapproved` (v2). An order preapproved for later execution, by its hash. ```json theme={null} { "type": "order_preapproved", "orderHash": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ------------------------------ | | `type` | string | `order_preapproved`. | | `orderHash` | `bytes32` (hex) | Hash of the preapproved order. | ### `order_preapproval_invalidated` `OrderPreapprovalInvalidated` (v2). A preapproval revoked, by the order hash. ```json theme={null} { "type": "order_preapproval_invalidated", "orderHash": "0x…" } ``` | Field | Solidity type | Description | | ----------- | --------------- | ---------------------------------------------------- | | `type` | string | `order_preapproval_invalidated`. | | `orderHash` | `bytes32` (hex) | Hash of the order whose preapproval was invalidated. | ### `trading_paused` `TradingPaused`. An operator paused trading on an exchange. ```json theme={null} { "type": "trading_paused", "pauser": "0x…" } ``` | Field | Solidity type | Description | | -------- | --------------- | ---------------------------- | | `type` | string | `trading_paused`. | | `pauser` | `address` (hex) | Address that paused trading. | ### `trading_unpaused` `TradingUnpaused`. Trading resumed after a pause. ```json theme={null} { "type": "trading_unpaused", "pauser": "0x…" } ``` | Field | Solidity type | Description | | -------- | --------------- | ----------------------------- | | `type` | string | `trading_unpaused`. | | `pauser` | `address` (hex) | Address that resumed trading. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending exchange transactions, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "trading", "confirmed": false } ``` # transfers Source: https://docs.radion.app/websockets/channels/transfers ERC-1155 outcome-token transfers from the Polymarket PositionManager contract. The `transfers` channel sends every ERC-1155 outcome-token transfer from the Polymarket PositionManager contract. Every time conditional-token positions move between wallets, one of these events fires. We decode each one and send it after the transaction is confirmed on-chain. These events used to stream on the old `combos` channel. They now have their own channel. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "transfers" } ``` With no filters, you get every confirmed transfer from all tokens. ## Filters | Filter | Type | Required | Description | | ----------- | ---------- | -------- | -------------------------------------------------------------------------------- | | `wallets` | `string[]` | No | Hex addresses (`0x…`). Keeps transfers where a listed address is `from` or `to`. | | `token_ids` | `string[]` | No | Token IDs as decimal strings. Keeps transfers of the listed position tokens. | This channel ignores `market_ids` and `min_usd`. See [Filters](/websockets/filters) for the combination rule. ## Events This channel sends both ERC-1155 transfer events. | Event | `data.type` | Description | | ---------------- | ----------------- | ----------------------------------------------------------------------------------------------- | | `TransferSingle` | `transfer_single` | One ERC-1155 conditional-token transfer. Tracks a single position token moving between wallets. | | `TransferBatch` | `transfer_batch` | Many ERC-1155 transfers in one transaction. Used when positions move together as a batch. | ## Payloads Every frame wraps the event in the standard envelope (`type`, `id`, `channel`, `confirmed`, `data`) shown on [Frames](/websockets/frames). Below, each block is the `data` object for one event — its `type` discriminator plus every field the ABI decodes. A full frame looks like: ```json theme={null} { "type": "event", "id": "s1", "channel": "transfers", "confirmed": true, "data": { "type": "transfer_single", "operator": "0x…", "from": "0x…", "…": "…" } } ``` ### `transfer_single` `TransferSingle` (ERC-1155). One conditional-token position moving between wallets. ```json theme={null} { "type": "transfer_single", "operator": "0x…", "from": "0x…", "to": "0x…", "id": "0x…", "amount": "0x…" } ``` | Field | Solidity type | Description | | ---------- | --------------- | ---------------------------------------------------------- | | `type` | string | `transfer_single`. | | `operator` | `address` (hex) | Address that made the transfer for the token owner. | | `from` | `address` (hex) | Sender of the position tokens (zero address for mints). | | `to` | `address` (hex) | Recipient of the position tokens (zero address for burns). | | `id` | `uint256` (hex) | ERC-1155 token id of the position moved. | | `amount` | `uint256` (hex) | Number of position tokens moved. | ### `transfer_batch` `TransferBatch` (ERC-1155). Many position tokens moving in one transaction. The parallel `ids` and `amounts` arrays line up index for index. ```json theme={null} { "type": "transfer_batch", "operator": "0x…", "from": "0x…", "to": "0x…", "ids": ["0x…", "0x…"], "amounts": ["0x…", "0x…"] } ``` | Field | Solidity type | Description | | ---------- | ----------------- | ---------------------------------------------------------- | | `type` | string | `transfer_batch`. | | `operator` | `address` (hex) | Address that made the transfer for the token owner. | | `from` | `address` (hex) | Sender of the position tokens (zero address for mints). | | `to` | `address` (hex) | Recipient of the position tokens (zero address for burns). | | `ids` | `uint256[]` (hex) | ERC-1155 token ids of the positions moved. | | `amounts` | `uint256[]` (hex) | Amount moved for each id, in the same order as `ids`. | ## Pending feed Set `confirmed: false` on your subscribe to get the pending feed of this channel — the same payload shape for pending transactions sent to the PositionManager contract, before they are in a block. Pending events are guesses. A transaction can be dropped, replaced, or reverted. See [Mempool](/websockets/mempool) for the full frame shape and how to handle it. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "transfers", "confirmed": false } ``` # Connection state Source: https://docs.radion.app/websockets/connection-state How Radion stays connected to the chain, heartbeats, and the health endpoint. ## Staying connected to the chain Radion does the hard part of staying connected to the blockchain for you. Behind the scenes, Radion keeps a live link to a Polygon node to read events as they happen. If that link ever drops — a node restart, a network blip, a provider hiccup — Radion notices and reconnects on its own. Once it is back, it re-subscribes to everything by itself. Your subscriptions stay valid the whole time. You do not need to resubscribe. During an outage the stream just goes quiet, then events come back when the connection recovers. ## Health endpoint You can check the connection status any time: ```http theme={null} GET /health ``` ```json theme={null} { "data": { "status": "ok", "at": "2026-06-21T12:00:00Z" } } ``` * `status: "ok"` — connected to the chain, events are flowing. * `status: "degraded"` — reconnecting, events are paused for a short time. `/health` always answers `200`. Read the `status` field for the real signal, not the HTTP code. ## Heartbeats Radion sends a protocol-level WebSocket ping about every 30 seconds. Your client should answer with a pong. Standard WebSocket libraries do this for you. If Radion sees no traffic from your client for about 75 seconds, it closes the connection as dead. This is not the same as the app-level [`ping` message](/websockets/subscribe#ping) you can send to get a `pong` frame back. This is about Radion's connection to the blockchain. Your own connection to Radion is a separate thing. Your client should still reconnect if its socket closes or you get a [`lagged` error](/websockets/frames#error-frames), then resubscribe to your channels. # Examples Source: https://docs.radion.app/websockets/examples Runnable Node.js + TypeScript examples for building on the Radion WebSocket. The **[radion-examples](https://github.com/radion-app/radion-examples)** repo has copy-paste, ready-to-run examples for every common WebSocket use case. Each one is a single [Node.js](https://nodejs.org) + TypeScript file built on the [`@radion-app/sdk`](https://github.com/radion-app/radion-typescript) package. You run it with [`tsx`](https://tsx.is), so there is no build step. The SDK handles the `X-API-Key` upgrade, reconnect, heartbeat, and resubscribe for you. ## Quickstart ```bash theme={null} git clone https://github.com/radion-app/radion-examples cd radion-examples cp .env.example .env # set RADION_API_KEY=sk_... pnpm install # @radion-app/sdk + tsx + dev types pnpm run wallet-alerts 0xYOUR_WALLET ``` Need a key? Make one at [radion.app](https://radion.app), then see [Authentication](/websockets/authentication). Every example reads it from `RADION_API_KEY`. Never hard-code it or put it in the URL. ## What's included | Example | Channel | Run | | ------------------------ | ---------------------------------------- | --------------------------------- | | Mirror a wallet's trades | `trading` (wallets) | `pnpm run copytrade 0xWALLET` | | Wallet alerts | `wallets` | `pnpm run wallet-alerts 0xWALLET` | | Whale trade feed | `trading` (min\_usd) | `pnpm run whales 10000` | | Single-market monitor | `markets` | `pnpm run market --token 0xTOKEN` | | Mempool early alerts | `trading` (confirmed: false) + `trading` | `pnpm run mempool` | | Resolution watcher | `oracle` + `resolution` | `pnpm run resolutions` | | Resilient client | `trading` | `pnpm run resilient` | ## The SDK realtime client Every example uses the realtime client from [`@radion-app/sdk`](https://github.com/radion-app/radion-typescript). It handles the production parts once for you: * opens the socket with the `X-API-Key` header, * reconnects with exponential back-off, * sends your subscriptions again after each reconnect, * sends an app-level [`ping`](/websockets/subscribe#ping), * reconnects on [`lagged`](/websockets/frames#error-frames) and stops on `key_revoked`. ```ts theme={null} import { Radion } from "@radion-app/sdk"; const radion = new Radion({ apiKey: process.env.RADION_API_KEY }); radion.realtime.onAnyChannel((e) => console.log(e.data)); radion.realtime.subscribe({ id: "s1", channel: "trading", filters: { wallets: ["0x…"] }, }); await radion.realtime.connect(); ``` See the [repo README](https://github.com/radion-app/radion-examples) for the full walkthrough, and the [channel catalogue](/websockets/channels/overview) for every payload shape. # Filters Source: https://docs.radion.app/websockets/filters Narrow a WebSocket channel to specific wallets, markets, tokens, or trade size. Add a `filters` object to the [subscribe](/websockets/subscribe) message. An empty filter, or one you leave out, means the whole channel. A filter a channel does not accept is ignored — it has no effect and is not an error. ## The four filters | Filter | Type | Accepted by | Description | | ------------ | ---------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `wallets` | `string[]` | `trading`, `fees`, `positions`, `combos`, `transfers`, `accounts`, `wallets` | Hex addresses (`0x…`). Keeps events that touch a listed address (maker, taker, sender, recipient, or a decoded participant). Required on the `wallets` channel (at least 1). | | `market_ids` | `string[]` | `trading`, `oracle`, `resolution`, `lifecycle`, `positions`, `combos`, `markets` | Condition/market IDs as hex strings. Keeps events for the listed markets. | | `token_ids` | `string[]` | `trading`, `fees`, `lifecycle`, `positions`, `combos`, `transfers`, `markets`, `clob.*` | Token IDs as decimal strings. Keeps events for the listed outcome tokens. **Required** on every [`clob.*`](/websockets/channels/clob) channel (at least 1). | | `min_usd` | `integer` | `trading` only | Smallest trade size in whole USD. Keeps fills at or above this size. Empty means all sizes. | See the [channel pages](/websockets/channels/overview) for the exact filters each channel accepts. The `wallets` and `markets` channels **require** their filter, or the subscribe fails. The [`clob.*`](/websockets/channels/clob) channels likewise **require** `token_ids`. ## How filters combine Filters group into three axes: * **Wallet axis** — `wallets`. * **Market axis** — `market_ids` and `token_ids` together. A market's condition id and its outcome token ids both name the same market, so they are one axis. * **Size axis** — `min_usd` (`trading` only). The rule is **AND across axes, OR within an axis**: * Within one axis, an event matches if it hits **any** value. Two `wallets` means "wallet A **or** wallet B". A `market_ids` and a `token_ids` together mean "this market **or** this token". * Across axes, an event must match **every** axis you set. A wallet **and** a market **and** a size. * An axis with no filter set adds no constraint (empty filter = whole channel). **Worked example.** Subscribe to `trading` with `wallets: [a]`, `market_ids: [m]`, and `min_usd: 100`. You get wallet `a`'s trades in market `m` at or above \$100 — all three axes must match. ## Confirmed vs pending matching On the confirmed feed (`confirmed: true`, the default), filters run against the fields of each decoded event log. On the pending feed (`confirmed: false`), the same filters run against fields read from each pending transaction's **calldata** instead of event logs. A filter only works where the field is a real calldata argument, because the blockchain limits what you can read before a transaction runs. See [Mempool → Filter reach](/websockets/mempool#filter-reach) for the full list. `min_usd` means a different thing on each feed. On the confirmed feed it measures the **filled** amount. On the pending feed it measures the pending order's **intended fill** (`call.notional_usd`), which may only partly fill or never land at all. # Frames Source: https://docs.radion.app/websockets/frames Event, control, and error frame shapes sent over the Radion WebSocket. Every message the server sends is a JSON object with a `type` field. Confirmed-event payloads are shown here. Pending-transaction payloads are on the [Mempool](/websockets/mempool) page. Every event frame carries a `confirmed` boolean on the envelope — `true` for on-chain confirmed events, `false` for pending (mempool) transactions — and both use the bare channel name. ## Event frame ```json theme={null} { "type": "event", "id": "s1", "channel": "trading", "confirmed": true, "data": { "type": "order_filled_v2", "orderHash": "0x…", "maker": "0x…", "taker": "0x…", "side": 0, "tokenId": "0x…", "makerAmountFilled": "0x…", "takerAmountFilled": "0x…", "fee": "0x…", "builder": "0x…", "metadata": "0x…" } } ``` * `id` — the subscription `id` you gave. * `channel` — the channel that matched (the bare name, the same for confirmed and pending). * `confirmed` — `true` for on-chain confirmed events, `false` for pending transactions. Route pending vs confirmed by this flag and your subscription `id`. * `data.type` — tells you which event it is (snake\_case). The other keys are the event's fields, decoded straight from the on-chain event. The example above is one `trading` event. `data.type` and the field set change per event. Each channel page lists **every** event it carries with its own payload example and full field table — see the **Payloads** section on [`trading`](/websockets/channels/trading#payloads), [`fees`](/websockets/channels/fees#payloads), [`oracle`](/websockets/channels/oracle#payloads), [`resolution`](/websockets/channels/resolution#payloads), [`lifecycle`](/websockets/channels/lifecycle#payloads), [`positions`](/websockets/channels/positions#payloads), [`combos`](/websockets/channels/combos#payloads), [`transfers`](/websockets/channels/transfers#payloads), and [`accounts`](/websockets/channels/accounts#payloads). The [`clob.*`](/websockets/channels/clob) channels are the exception: their `data` has **no** `type` field, because each clob channel has one fixed payload shape. ## Control frames These confirm the messages you send: ```json theme={null} { "type": "subscribed", "id": "s1", "channel": "trading" } { "type": "unsubscribed", "id": "s1" } { "type": "pong" } ``` ## Error frames Errors carry a fixed `code` and a `message` you can read. | `code` | When | Connection | | --------------------- | --------------------------------------------------------------------- | ---------- | | `invalid_message` | The message was not valid JSON. | Stays open | | `unknown_channel` | Unknown channel name, or a required filter is missing or wrong. | Stays open | | `subscription_limit` | The subscribe would go over your plan's subscriptions per connection. | Stays open | | `lagged` | The client fell behind the live stream, so some events were dropped. | Stays open | | `key_revoked` | The API key was revoked or is no longer valid. | Closes | | `revalidation_failed` | Radion could not re-check the API key after several tries. | Closes | A `lagged` error also tells you how many events were skipped: ```json theme={null} { "type": "error", "code": "lagged", "skipped": 42, "message": "Receiver fell behind; 42 events dropped. Reconnect to resume." } ``` See [Limitations → Backpressure](/websockets/limitations#backpressure) for why lag happens and how to handle it. # Limitations Source: https://docs.radion.app/websockets/limitations What the Radion WebSocket streams do and do not guarantee. ## Public onchain data only Radion streams decoded onchain events and public mempool transactions. There are no account-only order or fill streams. Radion cannot see private orderbooks or your account state. To track your own confirmed fills, subscribe to the [`trading`](/websockets/channels/trading) and [`wallets`](/websockets/channels/overview) channels, filtered to your addresses. ## Mempool is speculative Pending transactions on the [pending feed](/websockets/mempool) — any channel you subscribe to with `confirmed: false` — can be dropped, replaced, reordered, or reverted before they confirm. Treat them as hints, not facts, and check them against the confirmed feed by transaction hash. Mempool filters can only match fields that show up as calldata arguments — see [Filter reach](/websockets/mempool#filter-reach). ## Backpressure Radion keeps a fixed window of recent events for each stream. If your client reads slower than events arrive and falls behind that window, the oldest events are dropped and you get a [`lagged` error frame](/websockets/frames#error-frames) with a `skipped` count. The connection stays open, but the gap is not sent again. To recover, reconnect and resubscribe. To avoid lag, read fast, narrow your subscriptions with [filters](/websockets/filters), and do not block the socket read loop with heavy work for each message. ## No event replay Streams are live only. There is no backfill and no historical replay over the WebSocket. You get events from the moment you subscribe. For historical data, use the [REST API](/api/overview). # Mempool Source: https://docs.radion.app/websockets/mempool Stream speculative pending Polymarket transactions before they are confirmed onchain. Every channel has a [pending feed](/websockets/channels/overview#mempool-channels). Set `confirmed: false` on the subscribe message to get pending Polygon transactions to Polymarket contracts, before they land in a block. Use it for early visibility — alerts before confirmation, low-latency feeds, and checking pending against confirmed events. Mempool messages are guesses, not facts. A pending transaction can be dropped, replaced, reordered, or reverted. Use confirmed channels like `trading` as the real, final onchain truth. ## Mempool frame ```json theme={null} { "type": "event", "id": "pending-trades", "channel": "trading", "confirmed": false, "data": { "seen_at_ms": 1782027489000, "transaction_hash": "0x...", "from": "0x...", "to": "0x...", "contract_kinds": ["exchange"], "method_selector": "0x...", "call": { "method": "fillOrder", "market_ids": [], "token_ids": ["713210..."], "wallets": ["0x...", "0x..."], "notional_usd": 1000, "orders": [ { "maker": "0x...", "taker": "0x...", "token_id": "713210...", "side": "buy", "maker_amount": "200000000", "taker_amount": "100000000" } ] }, "input": "0x...", "value": "0" } } ``` `confirmed` sits on the envelope (next to `type`, `id`, `channel`), and is `false` on pending frames. The `channel` is the bare name (`trading`), the same as the confirmed feed — tell them apart by `confirmed`, and route by your subscription `id`. | Field | Description | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | `seen_at_ms` | When Radion first saw the pending transaction (Unix ms). | | `transaction_hash` | The pending transaction hash. Use it to match the pending one to the confirmed event. | | `from` / `to` | The sender and the Polymarket contract that was called. | | `contract_kinds` | The contract type of `to`: `exchange`, `oracle`, `lifecycle`, `ctf`, `combos`, `wallet_factory`, or `unknown`. | | `method_selector` | The 4-byte function selector, or `null` if the calldata is shorter than 4 bytes. | | `call` | Decoded fields (see below), or `null` if the calldata is not a known Polymarket call. | | `input` | The raw calldata. Always there, so you can decode more yourself. | | `value` | The native POL value sent with the transaction. | ### The `call` object Radion reads each pending transaction's calldata and turns it into a `call` object. It holds the fields the channel filters use, plus the decoded orders: | Field | Meaning | | -------------- | ----------------------------------------------------------------------- | | `method` | The decoded function name (e.g. `fillOrder`, `redeemPositions`). | | `market_ids` | Condition/market ids found in the arguments. | | `token_ids` | Trade token ids and position ids found in the arguments. | | `wallets` | Addresses found in the arguments. | | `notional_usd` | Intended fill notional in USD (exchange trades only); `null` otherwise. | | `orders` | Per-order detail for exchange trades; empty for non-trade calls. | Each entry in `orders` carries `maker`, `taker` (`null` for v2 exchange orders), `token_id`, `side` (`"buy"` or `"sell"`), `maker_amount`, and `taker_amount`. Amounts are decimal strings in base units, the same format as `token_ids`. There is no derived price yet — compute it from the amounts if you need one. `call` is `null` when the selector is not a known Polymarket state-changing call. Transactions whose calldata cannot be decoded still stream on their contract-type channel with `confirmed: false`. They just have no `call` to filter on. ## Filter reach Mempool [filters](/websockets/filters) run against the `call` fields above. A filter only works where the field is a real calldata argument, because the blockchain limits what you can read before a transaction runs: * **`market_ids`** — matches trades (v2), splits, merges, redemptions, and conversions that pass a `conditionId`/`marketId` argument. It cannot match ids the contract builds at run time (`prepareCondition`/`prepareMarket`), combos modules (which use a different `bytes31`/`bytes29` id), or oracle calls (which carry only a `questionId`). * **`token_ids`** — matches the token ids in trade orders and the position ids in redemptions, bridge, and combos calls. * **`min_usd`** — exchange trades only. Same filter name on both feeds, but they measure different things: confirmed measures the **filled** amount; pending measures `call.notional_usd`, the **intended fill** of the pending order, which may only partly fill or never land. * **`wallets`** — the sender and recipient (`from`/`to`), plus any wallet read from the calldata. ## Reconcile with confirmed streams Use the pending and confirmed feeds together: Set `confirmed: false` on `trading` (or any channel) when you want to see things early, before block confirmation. Subscribe to the same channel (default `confirmed: true`) to get the final decoded event after the transaction lands in a block. Give it a different `id`. Match `data.transaction_hash` from the pending frame with the confirmed event's transaction hash, when your client keeps both streams. # WebSockets Source: https://docs.radion.app/websockets/overview Subscribe to live Polymarket onchain events — trading, positions, market lifecycle, oracle, and resolution. Radion WebSockets send you Polymarket events live. You get each event the moment it is confirmed on the blockchain. You can also get pending transactions from the Polygon mempool, before they land in a block. One open connection can hold many subscriptions at once, and you can mix confirmed and pending subscriptions. So you get only the data you want, and you never have to poll. ## Use cases Follow a wallet and copy its moves in real time. See its trades the moment they land onchain. Get a signal whenever a wallet you watch buys, sells, or moves money. See big trades. Stream only fills above a USD amount you pick. Watch open interest, liquidity, and price ticks for every market. No need to poll the REST API. Listen for oracle events to know the second a market resolves. Get pending transactions from the Polygon mempool, before they are in a block. ## Endpoint ```text theme={null} wss://api.radion.app/ws ``` Put your API key in the `X-API-Key` header of the WebSocket upgrade request. See [Authentication](/websockets/authentication) for the details and safety tips. One connection can hold many subscriptions, and you can mix confirmed and pending subscriptions. How many connections and subscriptions you may open depends on your [plan](/websockets/rate-limits). ## Quickstart ```js theme={null} import WebSocket from "ws"; const ws = new WebSocket("wss://api.radion.app/ws", { headers: { "X-API-Key": process.env.RADION_API_KEY }, }); ws.on("open", () => { ws.send( JSON.stringify({ action: "subscribe", id: "s1", channel: "trading" }) ); }); ws.on("message", (raw) => { const frame = JSON.parse(raw.toString()); console.log(frame.type, frame.channel, frame.data); }); ``` 1. **Connect** — open the socket with your `X-API-Key` header. 2. **Subscribe** — send a `subscribe` message with the channel name (and filters if you want). 3. **Receive** — read the JSON frames that come back. Each one has a `type`, a `channel`, and a `data` payload. See [Subscribe](/websockets/subscribe) for every message shape, and [Frames](/websockets/frames) for the full frame reference. ## Channels at a glance | Channel | Purpose | | ----------------------------------------------- | ----------------------------------------------------------------------- | | [`trading`](/websockets/channels/trading) | Order fills, matches, cancels, preapprovals, and trading pauses. | | [`fees`](/websockets/channels/fees) | Exchange fees charged on trades. | | [`oracle`](/websockets/channels/oracle) | UMA Adapter and UMA Optimistic Oracle question events. | | [`resolution`](/websockets/channels/resolution) | Condition resolutions, reported results, and resolution pauses. | | [`lifecycle`](/websockets/channels/lifecycle) | Market, event, condition, and token preparation. | | [`positions`](/websockets/channels/positions) | CTF base-layer splits, merges, and payout redemptions. | | [`combos`](/websockets/channels/combos) | Module, neg-risk, combinatorial, bridge, and migration position events. | | [`transfers`](/websockets/channels/transfers) | ERC-1155 outcome-token transfers. | | [`accounts`](/websockets/channels/accounts) | Proxy and deposit wallet creation. | | [`wallets`](/websockets/channels/overview) | Events that touch one or more wallet addresses. | | [`markets`](/websockets/channels/overview) | Events that touch market ids or token ids. | Use [filters](/websockets/filters) to narrow a channel down to certain wallets, markets, tokens, or trade sizes. Watching big trades? Subscribe to `trading` with a `min_usd` filter. ## Connection lifecycle 1. **Open** — start the WebSocket connection with your API key in the upgrade header. 2. **Authenticate** — Radion checks the key and accepts the connection. A bad key returns HTTP `401`. 3. **Subscribe** — send one or more `subscribe` messages to start getting frames on the channels you name. 4. **Receive frames** — handle the JSON event and control frames until you are done or an error happens. 5. **Reconnect** — if the connection drops, connect again with exponential back-off, then send your subscriptions again. See [Connection state](/websockets/connection-state) for heartbeat and reconnect tips. ## Further reading Ready-to-run Node.js + TypeScript examples for every channel. Put your API key in the upgrade request. Subscribe, unsubscribe, and ping messages. Full channel list with typed schemas. Narrow a channel to wallets, markets, tokens, or size. Event, control, and error frame shapes. Pending transactions before they are in a block. Reconnect, heartbeats, and the health endpoint. Connection and subscription limits per plan. What the WebSocket streams do and do not promise. # Rate limits Source: https://docs.radion.app/websockets/rate-limits WebSocket connection and subscription limits per Radion plan. ## Plan limits | Plan | Concurrent connections | Subscriptions per connection | | ---------- | ---------------------- | ---------------------------- | | Free | 1 | 2 | | Starter | 3 | 5 | | Pro | 10 | 20 | | Enterprise | Unlimited | Unlimited | Your plan sets how many WebSocket connections you can hold at once, and how many channels you can subscribe to on each one. The plan is tied to your API key. The key you [authenticate](/websockets/authentication) with sets the limits for that connection. * Going over the **connection** limit rejects the upgrade with HTTP `429`. * Going over the **subscriptions per connection** limit returns a [`subscription_limit` error frame](/websockets/frames#error-frames). The connection stays open and your other subscriptions are not touched. Plan changes apply to live connections, with no reconnect needed. If you upgrade, the higher limits start within a few seconds. If you downgrade, new subscriptions follow the lower limit. If the API key is revoked, its open connections close right away. WebSocket connections do **not** count against the per-second or monthly request limits that apply to REST calls. Opening a connection, the messages you send, and the events streamed back to you do not use any request quota. Your only limits are the **concurrent connection** and **subscriptions per connection** numbers above. You still need a valid API key to connect. # Subscribe Source: https://docs.radion.app/websockets/subscribe Subscribe, unsubscribe, and ping over the Radion WebSocket. Send JSON messages after the socket opens. Each message has an `action`. You set the `id` yourself. Radion echoes it back in confirmations and event frames, so you can match incoming data to the subscription that made it. ## Subscribe ```json theme={null} { "action": "subscribe", "id": "s1", "channel": "trading" } ``` To narrow a channel, add a `filters` object: ```json theme={null} { "action": "subscribe", "id": "wallet-activity", "channel": "wallets", "filters": { "wallets": ["0x0000000000000000000000000000000000000000"] } } ``` If it works, the server sends back a [`subscribed` control frame](/websockets/frames#control-frames). If it fails, it sends an [`error` frame](/websockets/frames#error-frames). That can happen if the channel is unknown, a required filter is missing, or you are over your [subscription limit](/websockets/rate-limits). If you subscribe again with an `id` you already use, the new subscription replaces the old one on that `id`. ## Confirmed vs pending By default a subscription streams the confirmed on-chain feed — `confirmed` is `true`. Set `confirmed: false` to stream the pending feed of the same channel instead: pending transactions before they land in a block. ```json theme={null} { "action": "subscribe", "id": "pending", "channel": "trading", "confirmed": false } ``` Both feeds use the bare channel name. Each event frame carries a `confirmed` boolean, so you tell pending from confirmed by that flag and your subscription `id`, not by the channel name. See [Mempool](/websockets/mempool) for the pending frame shape. ## Unsubscribe Unsubscribe with the same `id`: ```json theme={null} { "action": "unsubscribe", "id": "wallet-activity" } ``` The server sends back an `unsubscribed` control frame. If the `id` is unknown, nothing changes, but the server still acknowledges it. ## Ping Send an app-level ping to check the socket is alive: ```json theme={null} { "action": "ping" } ``` The server sends back `{ "type": "pong" }`. This is not the same as the protocol-level WebSocket ping/pong that the server uses for [heartbeats](/websockets/connection-state#heartbeats).