# Polymarket Market Monitor (`zinin/polymarket-market-monitor`) Actor

Get current Polymarket market prices, outcomes, volume, liquidity and status. Filter your watchlist and compare with a previous snapshot for research and newsroom updates.

- **URL**: https://apify.com/zinin/polymarket-market-monitor.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** News, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 market snapshot delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Polymarket Market Monitor

Get current Polymarket market prices, every available outcome, volume, liquidity, end date and status; filter a watchlist and compare a new observation with your prior same-scope snapshot.

<p align="center"><img src="https://api.apify.com/v2/key-value-stores/JhI2CK7mTAqEHgqTu/records/polymarket-market-monitor-7d69006a572d0c04-icon.png?signature=eiT2JjHGWfxHEm9zBsBQ" width="96" alt="Polymarket Market Monitor icon"></p>

![Polymarket Market Monitor: filter → Gamma evidence → market decisions](https://api.apify.com/v2/key-value-stores/JhI2CK7mTAqEHgqTu/records/polymarket-market-monitor-adc69ba95fe3531b-readme-hero.webp?signature=14VXHD5wdNiizzvJEjSoE)

Polymarket Market Monitor is for research, newsroom, product and operations teams that need a bounded export of **current market observations**. It reads public Polymarket Gamma responses, writes one useful Dataset row per delivered market, and can compare those rows with a snapshot you provide from a prior run. It does not place orders, read trades, guarantee probabilities, predict a resolution, or infer that a market closed because it is absent from an incomplete response.

> **Live evidence.** Example A below is an accepted cloud canary; example B and the direct-ID control are accepted local live runs. All used the live Gamma source on 2026-09-10. They are observations, not a promise that the same markets, prices, volume, or coverage will be present later.

### 1. What you get

Each useful `polymarket_market` Dataset row represents one market actually delivered by the Actor. It contains the market ID and question, all outcomes with their source-reported prices, source volume and liquidity when available, `endDate`, source-reported `status`, a human source URL, and `observedAt`. `changes` describes a comparison only when a compatible prior snapshot was supplied.

The `OUTPUT` Key-Value Store record contains counts, delivery state, full source coverage, and a reusable `snapshot`. The snapshot contains only comparison fields for rows delivered in that run. It is deliberately smaller than the Dataset: download Dataset for questions, links and full row data; keep `OUTPUT.snapshot` for the next same-scope comparison.

`diagnostic` Dataset rows are free. After valid startup they report budget stops, source failure, source incompleteness, or no matching markets. They are not market data and never request a `result-found` event. Invalid startup input fails before useful delivery and may not create a Dataset diagnostic.

Every useful row keeps all source outcomes together. A binary Yes/No market is one row, and a market with more named outcomes is still one row. The Actor validates that outcome names and prices have the same length, keeps outcome identity by name, and accepts only finite prices from 0 through 1. It does not choose a “winning” side, turn a price into a guaranteed probability, or split outcomes into separately billed records.

Volume and liquidity are nullable because the source may omit them. `status` is derived only from the source's explicit `closed` and `active` fields. `endDate` is the parseable source value or `null` for a direct lookup; browse discovery requires a known end date that is not before the observation. Each row also carries exact page evidence: request URL, byte count, SHA-256, and retrieval time.

The comparison baseline is inside `OUTPUT.snapshot`, not a separate uncharged catalog of questions and links. It contains market ID, named outcome prices, volume, liquidity, and status only for acknowledged useful deliveries. Questions and source URLs remain in the paid Dataset. This separation keeps the snapshot below its 512 KiB input boundary and prevents it from becoming a second results export.

### 2. Who uses it

Researchers can capture a bounded list of current market prices with evidence of when and where the list was read. Newsrooms can monitor a political or sports slice, retain a source link for each row, and distinguish a partial scan from an empty result. Product teams can pass explicit market IDs for a watchlist of up to 50 markets. Analysts can run a second same-scope observation and route only rows whose price, volume, liquidity, or source status changed.

The Actor is not betting advice, an order-book feed, an execution tool, a settlement service, or a complete-market census. A price is the value supplied by Gamma at the observation time. Treat it as an observation that needs its own editorial or analytical interpretation.

**News and research desks** can preserve a time-stamped source observation behind a developing story. The question, all named outcomes, source URL, observation time, and page digest help an editor understand what was actually collected. Coverage flags prevent a capped browse from being described as the full market universe.

**Product and data teams** can normalize Gamma responses into a stable row contract. Market IDs provide a durable join key; nested outcomes preserve nonbinary markets; nullable numeric fields remain explicit. Teams can append rows to their own warehouse without asking the Actor to hold shared state.

**Watchlist operators** can pass up to 50 exact IDs. This path is useful when the identity matters more than current discovery and can return a source-reported old or closed market. It also makes failed IDs explicit in coverage instead of silently replacing them with unrelated current markets.

**Automation and agent builders** can branch on `OUTPUT.outcome`, completeness, and stop reason before taking downstream action. The Actor does not create a schedule, send a webhook, publish a story, place an order, or write to a CRM. Those actions remain under the buyer's credentials and rules.

### 3. How to run

1. Select `all`, `sports`, or `politics`.
2. Optionally add a keyword, up to 50 explicit `marketIds`, or an end-date window.
3. Set `maxItems` and a Max total charge that covers only the useful rows you want.
4. Run and download Dataset rows.
5. Read `OUTPUT`. If it is `partial`, use its coverage before treating the list as a sample.
6. For a later comparison, copy `OUTPUT.snapshot` into `previousSnapshot` with exactly the same stable filters.

For a watchlist, `marketIds` takes the documented direct Gamma market route rather than scanning the keyset. A direct response may explicitly report `closed`; that is different from drawing any conclusion from a missing market in a browse response.

Browse and direct-ID modes answer different questions. Browse asks for current discovery and always sends `closed=false` plus a frozen `end_date_min` equal to the run observation time. If `closingWithinDays` is set, it also sends one frozen `end_date_max`. Sports appends the explicit Gamma sports market types; politics sends the currently verified public politics tag ID `2`. Pagination follows `after_cursor` until the keyset ends or a documented bound stops it.

Direct-ID mode requests `https://gamma-api.polymarket.com/markets/{id}` for each named ID with concurrency four and a 90-second shared deadline. It retains the exact endpoint and marks access as `public_direct_market`. Other filters still apply, but the direct path allows a past end date because the buyer explicitly requested that market's current source record.

For comparison, copy `OUTPUT.snapshot` without edits. Keep `category`, normalized `keyword`, sorted `marketIds`, and `closingWithinDays` unchanged. `maxItems` may change because it controls delivery size and is not part of comparison scope. A market absent from the prior delivered snapshot becomes `marketIsNew: true`; that wording means new to the compared delivered set, not newly created on Polymarket.

### 4. Pricing

The pricing noun is **market snapshot delivered**. One useful delivered market snapshot requests one `result-found` event. The base price is **$0.003 per market snapshot**, or $3.00 per 1,000 useful rows. Bronze is $0.00270, Silver is $0.00240, and Gold, Platinum, and Diamond are $0.00210 per useful row.

There is no Actor start fee and no default Dataset-item fee. Diagnostics, invalid-input failures, source failures, and valid empty results are free. A repeated market is still a useful current observation and is billable when delivered; comparison does not make a useful row free. Use `maxItems` and Max total charge as independent bounds.

| Apify plan | Per delivered market snapshot | Per 1,000 delivered snapshots |
|---|---:|---:|
| Base / Free | $0.00300 | $3.00 |
| Bronze | $0.00270 | $2.70 |
| Silver | $0.00240 | $2.40 |
| Gold | $0.00210 | $2.10 |
| Platinum | $0.00210 | $2.10 |
| Diamond | $0.00210 | $2.10 |

The platform event name is `result-found`; its product meaning is one **market snapshot delivered**. A 100-row partial browse can request 100 result events because all 100 rows are useful observations even though the Actor has not claimed complete discovery. Conversely, a run with 100 source records that all fail validation requests no result events.

Set two independent bounds. `maxItems` limits delivered market rows. Apify Max total charge limits dollars. The Actor verifies a platform budget before source access and before each paid write. A verified zero budget stops before Gamma requests. A later budget stop preserves only acknowledged delivered rows in the snapshot and reports the stop explicitly.

#### Cost planning examples

At base price, a cap of 10 useful rows costs at most $0.030 in result events; 100 costs at most $0.300; 1,000 costs at most $3.00. Those figures exclude no start fee and no Dataset-item fee because both are configured at zero. They are upper bounds only for rows actually delivered; diagnostics and empty results request no result event. A partial 100-row sports browse still has 100 useful delivered snapshots and can therefore request 100 result events on the platform.

For a newsroom check, start at 10 rows and inspect `coverage.stopReason`. For a recurring workflow that needs 100 sports observations, set both `maxItems:100` and a Max total charge that covers 100 result events. If only ten rows are needed for a particular editorial question, set both bounds to ten. Do not rely on a source empty response to control spend; use explicit input and platform charge limits.

### 5. Input contract

| Field | Type and bound | Meaning |
|---|---|---|
| `category` | `all`, `sports`, `politics`; default `all` | Browse classification. Sports uses documented sports market types; politics sends the verified public tag ID `2`. |
| `keyword` | optional text, 200 characters | Case-insensitive match against source question, slug, or description. |
| `marketIds` | optional unique numeric IDs, maximum 50 | Exact public Gamma market reads. Combined with other filters. |
| `closingWithinDays` | optional integer 0–30 | End date must be at or after observation time and no later than this many days after it. Missing/invalid dates do not match. |
| `maxItems` | 1–1,000; default 100 | Maximum useful rows delivered and charged. |
| `previousSnapshot` | optional object, 512 KiB, 1,000 rows | Prior `OUTPUT.snapshot` with exactly matching category, keyword, IDs and closing window. |

Stable scope excludes `maxItems` and observation time, so you can increase a cap on a later run without making the snapshot incompatible. Changing category, keyword, explicit IDs, or closing window makes it incompatible. The Actor rejects that input before source reads or paid output with `INVALID_SNAPSHOT_SCOPE`.

Input is a closed object. Unknown root keys, accessors, proxies, control characters, nonfinite numbers, duplicate IDs, zero-like IDs, excessive arrays, and out-of-range integers are rejected. The keyword is trimmed and lowercased before scope comparison. Market IDs are sorted, so caller order does not create a different scope.

`category` is an explicit source filter. Sports uses Gamma's sports market-type fields for moneyline, spreads, and totals. Politics sends `tag_id=2`, which a bounded live check mapped to Gamma's public `politics` tag on 2026-09-10. The Actor also requires an explicit politics value in the returned market category, event category, tag, or fee-type fields. It does not classify a market as political or sporting by guessing from question words. If Gamma changes the tag assignment or omits those fields, rows can be excluded; treat that as a source-contract boundary.

`closingWithinDays` applies to browse discovery from the exact observation instant. Zero means an end date equal to that instant; seven means no later than exactly seven 24-hour periods afterward. Both request and local validation require a known date at or after the observation. Explicit IDs bypass only the general past-date exclusion; if you also supply `closingWithinDays`, its requested interval still applies.

`previousSnapshot` is at most 512 KiB serialized and 1,000 markets. Every snapshot market has a unique positive numeric-string ID, at most 100 unique named outcomes with prices from 0 through 1, nonnegative finite or null volume/liquidity, and `active`, `closed`, or `unknown` status. Arbitrary question text, links, coverage objects, and billing claims are rejected from the comparison state.

### 6. Real happy, partial, and failure output

Example A is an **ACCEPTED CLOUD CANARY** and example B is **ACCEPTED LOCAL LIVE evidence**, both collected from the public Gamma API on 2026-09-10 after the future-end-date correction. Their markets, prices, volumes, liquidity, and availability can change.

#### ACCEPTED CLOUD CANARY example A — active sports discovery input

```json
{
  "category": "sports",
  "maxItems": 100
}
```

#### ACCEPTED CLOUD CANARY example A — exact first market row

```json
{
  "billing": {
    "platformStartChargeMayApply": true,
    "resultEventRequested": true,
    "settlement": "unconfirmed_at_write"
  },
  "changes": {
    "changed": null,
    "comparisonStatus": "no_previous_snapshot",
    "marketIsNew": null
  },
  "endDate": "2026-09-24T01:30:00Z",
  "liquidity": 1519.3447,
  "marketId": "1607632",
  "observedAt": "2026-09-10T19:16:56.252Z",
  "outcomes": [
    {
      "name": "Yes",
      "price": 0.465
    },
    {
      "name": "No",
      "price": 0.535
    }
  ],
  "question": "Will Seattle Sounders FC win on 2026-04-12?",
  "recordType": "polymarket_market",
  "schemaVersion": "1.0",
  "slug": "mls-sea-rsl-2026-04-12-sea",
  "source": {
    "access": "public_keyset",
    "coverage": {
      "access": "public_keyset",
      "endpoint": "https://gamma-api.polymarket.com/markets/keyset",
      "observedAt": "2026-09-10T19:16:56.252Z",
      "page": {
        "bytes": 647114,
        "retrievedAt": "2026-09-10T19:16:56.520Z",
        "sha256": "676863c53b0f4bb239b65fd18eae1f8b91fa6084d144f1d2c9d44e87bc3eea74",
        "url": "https://gamma-api.polymarket.com/markets/keyset?closed=false&limit=100&end_date_min=2026-09-10T19%3A16%3A56.252Z&sports_market_types=moneyline&sports_market_types=spreads&sports_market_types=totals"
      }
    },
    "endpoint": "https://gamma-api.polymarket.com/markets/keyset",
    "publisher": "Polymarket Gamma API"
  },
  "sourceURL": "https://polymarket.com/event/mls-sea-rsl-2026-04-12",
  "status": "active",
  "volume": 1571.989888
}
```

This run returned 100 useful rows plus one free `SOURCE_INCOMPLETE` diagnostic. Cloud run `O8wAehc37bl1Muscz` used private build `0.1.2` (`pFevVYeVhaRQduuzn`) and finished in 8.722 seconds. It reported `outcome: "partial"`, `stopReason: "max_items"`, `pagesRead: 1`, and `nextCursorAvailable: true`. Final settlement confirmed 100 `result-found` events, no start charge event, and no Dataset auto-charge event. Root acceptance matched all 100 source identities to the native run and found zero past end dates. This owner canary was validation traffic, not customer revenue.

Its exact source URL froze `end_date_min=2026-09-10T19:16:56.252Z` and sent the three repeated sports market types. Every delivered browse row had a known `endDate` at or after that instant, and all 100 had positive liquidity in this observation. One hundred rows is a buyer cap, not proof that the Actor enumerated every future sports market.

This first source record exposes an important upstream boundary. Its question and slug say `2026-04-12`, while Gamma's structured `endDate` and raw `gameStartTime` were `2026-09-24T01:30:00Z`; the raw record also reported `acceptingOrders: true` and a September 10 update. The Actor preserves the question verbatim and applies discovery date filtering to Gamma's structured `endDate`. It does not parse a date from question text, rewrite source wording, or claim it validated consistency between a question, fixture date, `gameStartTime`, and `endDate`. Consumers that need fixture consistency must add an independent domain rule.

#### LOCAL LIVE example B — seven-day closing discovery input

```json
{
  "category": "all",
  "closingWithinDays": 7,
  "maxItems": 100
}
```

#### LOCAL LIVE example B — exact first market row

```json
{
  "schemaVersion": "1.0",
  "recordType": "polymarket_market",
  "marketId": "811719",
  "question": "Will the Swedish Social Democratic Party (S) win the most seats in the 2026 Swedish parliamentary election?",
  "slug": "will-the-swedish-social-democratic-party-s-win-the-most-seats-in-the-2026-swedish-parliamentary-election",
  "outcomes": [
    {
      "name": "Yes",
      "price": 0.9925
    },
    {
      "name": "No",
      "price": 0.0075
    }
  ],
  "volume": 246706.98283000002,
  "liquidity": 100098.21545,
  "endDate": "2026-09-13T00:00:00Z",
  "status": "active",
  "sourceURL": "https://polymarket.com/event/sweden-parliamentary-election-winner",
  "observedAt": "2026-09-10T19:11:45.874Z",
  "source": {
    "publisher": "Polymarket Gamma API",
    "endpoint": "https://gamma-api.polymarket.com/markets/keyset",
    "access": "public_keyset",
    "coverage": {
      "observedAt": "2026-09-10T19:11:45.874Z",
      "endpoint": "https://gamma-api.polymarket.com/markets/keyset",
      "access": "public_keyset",
      "page": {
        "url": "https://gamma-api.polymarket.com/markets/keyset?closed=false&limit=100&end_date_min=2026-09-10T19%3A11%3A45.874Z&end_date_max=2026-09-17T19%3A11%3A45.874Z",
        "bytes": 599730,
        "sha256": "64182b0100fb554227d889415af6f351884aee88f4cc2d74620c31437e061161",
        "retrievedAt": "2026-09-10T19:11:46.243Z"
      }
    }
  },
  "changes": {
    "comparisonStatus": "no_previous_snapshot",
    "marketIsNew": null,
    "changed": null
  },
  "billing": {
    "resultEventRequested": false,
    "settlement": "no_result_event_requested",
    "platformStartChargeMayApply": false
  }
}
```

This run also returned 100 useful rows plus one free `SOURCE_INCOMPLETE` diagnostic. Its keyset URL froze both boundaries: `end_date_min=2026-09-10T19:11:45.874Z` and `end_date_max=2026-09-17T19:11:45.874Z`. The first row ended on 2026-09-13, inside that interval. The result was `partial` because the 100-row buyer cap was reached while another cursor remained, not because the source request failed.

#### Direct-ID historical behavior

An additional accepted local live check used this input:

```json
{
  "category": "all",
  "marketIds": [
    "1149073"
  ],
  "maxItems": 10
}
```

It returned market `1149073` from the exact `/markets/1149073` route with `source.access: "public_direct_market"`, `endDate: "2026-01-23T13:00:00Z"`, and a complete direct-ID coverage receipt. That old end date is intentionally allowed for an explicit watchlist lookup. It is excluded from browse discovery, where the product contract is current markets with known future end dates. Direct IDs preserve source-reported closed or past history because the buyer named the identity; they do not make it part of current discovery.

#### Partial, empty, and failed outcomes

`partial` means useful rows can exist while the requested collection is not complete. `max_items`, `page_cap`, total timeout, a later-page failure, or a failed subset of direct IDs can all produce partial coverage. The Actor emits `SOURCE_INCOMPLETE` when a configured browse bound stops before the keyset ends and `SOURCE_PARTIAL` when validated rows coexist with failed source reads. Missing markets in either result are not interpreted as closed.

`empty` is reserved for a completed source operation with no matching rows. A browse that reaches the end of the keyset with no rows can be a valid empty result; so can a complete explicit-ID selection whose returned markets do not match the other requested filters. `NO_MATCHING_MARKETS` is free. An empty result with incomplete coverage remains partial.

`failed` means the Actor could not produce a trustworthy browse collection result or could not safely finish processing. Invalid input and incompatible snapshots stop before source reads and paid delivery. A first-page source error, wrong content type, redirect, oversized response, or malformed JSON is not converted to a successful empty result. A problem after validated browse pages, including an invalid cursor or duplicate source identity, becomes partial. Direct-ID lookup failures also produce explicit partial coverage, even when zero requested IDs were validated. When delivery is initialized and remains certain, `OUTPUT` records the stable failure code and a free diagnostic explains the source condition. HTTP source failures also retain a bounded `sourceFailureDetails` object with the safe Gamma route, final attempt number, and HTTP status.

```json
{
  "schemaVersion": "1.0",
  "outcome": "partial",
  "deliveredRows": 100,
  "selectedRows": 100,
  "collectionComplete": false,
  "deliveryComplete": false,
  "sourceFailure": null,
  "itemLimitReached": false,
  "maxItems": 100,
  "coverage": {
    "endpoint": "https://gamma-api.polymarket.com/markets/keyset",
    "pagesRead": 1,
    "complete": false,
    "stopReason": "max_items",
    "nextCursorAvailable": true
  }
}
```

The abbreviated summary above shows the routing fields; `OUTPUT` also contains billing counters, page evidence, and the complete compact snapshot. Always use the stored `OUTPUT` rather than reconstructing outcome from Dataset row count.

#### Failure and zero-row handling

`INVALID_INPUT` covers unsupported categories, non-string keyword values, duplicate or excessive IDs, invalid closing windows and invalid maxItems. `INVALID_PREVIOUS_SNAPSHOT`, `PREVIOUS_SNAPSHOT_TOO_LARGE`, and `INVALID_SNAPSHOT_SCOPE` stop comparison before market collection. Correct the data contract and submit a new run; do not expect a useful Dataset export from rejected startup input.

`SOURCE_FAILED` means no source page could be validated for a browse request. Its free diagnostic uses `sourceDetails`; failed OUTPUT uses `sourceFailureDetails`. Both contain only `endpoint`, `attempts`, and an optional numeric HTTP `status`. The endpoint is reduced to `https://gamma-api.polymarket.com/markets/keyset` or `https://gamma-api.polymarket.com/markets/{id}`: no query, response body, token, or buyer input is copied into failure metadata. An HTTP 403 reports `attempts:1` because it is not retried; HTTP 429 and server errors can report up to three total attempts.

`SOURCE_PARTIAL` means some reads failed after at least one validated page or direct ID response. Its free diagnostic carries the same bounded `sourceDetails`, and `OUTPUT.coverage.failureDetails` preserves browse failure context. Direct-ID coverage lists failed identities under `failedMarketIds`; each HTTP failure has a `source` object with the safe route, attempts, and status. `SOURCE_INCOMPLETE` means the configured max-items/page/time bound ended a browse before the keyset ended. A valid completed zero result yields `empty` with `NO_MATCHING_MARKETS`; it differs from partial coverage with zero delivered rows, which remains `partial` because absence is not a market-state claim.

### 7. Field dictionary

| Dataset field | Contract |
|---|---|
| `recordType` | `polymarket_market` for useful rows; `diagnostic` for free status rows. |
| `marketId` | Gamma market identifier as a string. |
| `question`, `slug` | Source market text and optional source slug. |
| `outcomes` | Every source outcome accepted for this row: `{name, price}`. Prices are source values, not probabilities guaranteed by this Actor. |
| `volume`, `liquidity` | Numeric source values or `null` when unavailable. |
| `endDate` | Valid source timestamp or `null`; not a derived resolution time. |
| `status` | `active`, `closed`, or `unknown`, only from source fields. |
| `sourceURL` | Polymarket event or market URL when a source slug is available. |
| `observedAt` | Actor observation timestamp. |
| `source` | Publisher, exact retrieval endpoint/access, and page-level hash/bytes/time evidence for that row. |
| `sourceDetails` | Diagnostic-only bounded source failure context: safe route, attempts, and optional HTTP status. |
| `changes` | Comparison state and price/volume/liquidity/status differences when a compatible snapshot exists. |
| `billing` | Request intent and settlement state at write time; check OUTPUT counters for confirmed events. |

#### Nested field contract

`outcomes[]` contains one object per source outcome. `outcomes[].name` is the source outcome identity; `outcomes[].price` is the reported current price. Outcome order is not used as comparison identity. A prior snapshot preserves name/price pairs, so a reordered source array does not fabricate a price change.

`changes.comparisonStatus` is `no_previous_snapshot` on a first run and `compared` when the supplied same-scope snapshot contains a compatible state. `marketIsNew:true` means the market was not among prior delivered snapshot rows. It does not mean the market is newly created at Polymarket. `outcomePriceChanges[]` supplies outcome name, previous price and current price only when the named outcome state differs. `volume` and `liquidity` changes include previous/current values and a numeric delta only when both values exist. `status` changes are only source-status changes.

`source.coverage` inside a market row is page-specific evidence: the endpoint/access mode and the exact response page URL, byte count, SHA-256 and retrieval timestamp that produced that row. OUTPUT `coverage` is run-wide evidence: endpoint, pages read, complete flag, stop reason, cursor availability and all page evidence. Do not duplicate run-wide coverage into a downstream row table unless you need that denormalization.

`billing.resultEventRequested` is true only for a paid platform useful-row write. `billing.settlement` is intentionally an at-write statement, not a receipt. Reconcile confirmed event counters in OUTPUT/run metadata before accounting. Locally, the SDK reports `local_no_charging`; local examples therefore show zero confirmed events even though their market rows model useful output.

#### OUTPUT and coverage fields

`outcome` is `completed`, `empty`, `partial`, `budget_stopped`, or `failed`. `selectedRows` counts normalized matches presented to delivery; `deliveredRows` counts useful rows whose Dataset write was acknowledged. `datasetWritesAcknowledged` can also include free diagnostics. `confirmedResultEvents` is the run-level confirmed billing counter. `deliveryUncertain` prevents a success claim after an ambiguous paid operation.

`collectionComplete` copies the source coverage conclusion. `deliveryComplete` requires all selected rows to be delivered and requires complete collection without a source failure. `sourceFailure` names a partial source code or is null. `sourceFailureDetails` carries the bounded route/attempt/status context for a partial or fatal source failure and is null or absent when there was none. `itemLimitReached` concerns selected-versus-delivered rows, while `coverage.stopReason: "max_items"` records discovery stopped at the buyer's source/result bound.

Browse coverage includes observation time, endpoint, pages read, completion, stop reason, cursor availability, and bounded page evidence. Direct-ID coverage additionally names `mode: "direct_market_ids"` and `failedMarketIds`. A direct set is complete only when every requested lookup and normalization succeeds. Snapshot scope is stored alongside snapshot rows so a later run can reject a different population before collection.

### 8. Evidence and boundaries

Browse uses `https://gamma-api.polymarket.com/markets/keyset?closed=false` with `after_cursor`. It reads at most 20 pages, at most 100 source markets per page, and has a shared 90-second collection budget. A page body is capped at 8 MiB and a request at 15 seconds. Retryable transport and transient HTTP conditions are retried at most twice; 403 and malformed responses are not retried.

`OUTPUT.coverage` is the run-level truth: `end_of_keyset` means the source stream ended; `max_items`, `page_cap`, or a source failure mean the browse coverage is incomplete. Useful rows from validated pages may still be delivered with a free diagnostic, but the outcome is `partial`. Do not treat a partial output as all active Polymarket markets in a category.

The corrected 2026-09-10 live evidence confirms the date-filter contract. The accepted cloud sports canary returned 100 future-ended markets from one capped keyset page. The request carried `end_date_min` equal to the observation and three explicit sports types. Seven-day local discovery returned 100 markets whose known end dates fell between the same frozen lower bound and a seven-day upper bound. Both runs were partial because another cursor remained. Date-filter correctness does not imply that separate source text and structured scheduling fields agree.

Politics discovery sent verified tag ID `2`, read two pages, and returned 100 future-ended markets before the item cap. A separate bounded live check on 2026-09-10 confirmed that Gamma's public politics slug mapped to ID `2`, and a tag-2 keyset response carried the explicit `politics_fees` source value required by the local defense. The Actor does not need an auxiliary tag lookup during a run. The direct-ID control requested a market with a January 2026 end date from the exact market endpoint and returned it as a complete named-watchlist observation. This contrast is intentional: current browse discovery excludes stale or missing end dates, while direct identity lookup preserves source history.

The client rejects redirects and wrong content types, streams each response with an 8 MiB cap, limits a request to 15 seconds, and retries transient network failures, timeouts, HTTP 429, and server errors at most twice. The overall collection is bounded to 90 seconds. A malformed first page is a failure; a later malformed page makes the validated prefix partial.

No output is a complete order book, trade history, resolution record, or list of every market on Polymarket. Gamma fields and tag assignments can change, and a price can move after `observedAt`. Page SHA-256 records bytes seen by the run; it is evidence for the observation, not a permanent archive or publisher signature.

### 9. Decision routing

| OUTPUT outcome | Use it for | Do not conclude |
|---|---|---|
| `completed` | Delivered rows and completed collection coverage. | Future availability or settlement. |
| `partial` | Validated delivered rows plus explicit coverage limits/failure. | That absent markets are closed or nonexistent. |
| `empty` | A completed scope with no matching delivered market. | That a broader Polymarket category is empty. |
| `budget_stopped` | Free diagnostic and count state. | That any source market was read. |
| `failed` | Error diagnosis and safe retry planning. | That an unacknowledged write did not occur. |

For a complete or empty result, preserve `OUTPUT` with the Dataset. For a partial result, retain rows as point observations while labeling population coverage incomplete. For delivery uncertainty, reconcile the existing run and Dataset before retrying; the durable claim rejects replay when a processing claim or prior confirmed result exists.

Require `changes.comparisonStatus == "compared"` before interpreting `changed`. `marketIsNew: true` means absent from the prior delivered snapshot. A price change is identified by outcome name, so reordered arrays do not fabricate changes. Volume and liquidity deltas can be null when either side is unavailable. A status change reflects only two explicit delivered source states.

A newsroom threshold might route a named outcome movement of 0.05 or more to review, while a data-quality route might flag liquidity changing from a number to null. Those thresholds belong downstream. Include market ID, question, observation context, source URL, and coverage in the handoff rather than sending an unexplained number.

### 10. Commercial playbooks

**News desk.** Run the political task with `maxItems:100`; archive Dataset, OUTPUT, and the source hashes. Use `partial` coverage in your handoff. On a later same-scope run, filter `changes.changed:true` in your warehouse before assigning a reporter.

**Sports research.** Run the sports task. Use market question, named outcomes, end date, and source URL as research inputs. Do not translate prices into a claim of event likelihood without your own methodology.

**Closing watchlist.** Run the seven-day task. A market must have a source end date inside the future window; already-ended dates do not match. Re-run only as often as your workflow needs and retain the previous snapshot.

**Append-only research table.** Store `(marketId, observedAt)` as an observation identity and preserve nested outcomes as JSON. Keep each run's `OUTPUT` in a companion run table. This creates longer history under your control without shared Actor state and lets analysts exclude incomplete runs from population counts.

**Seven-day closing desk.** Run `polymarket-markets-closing-this-week`, retain each stated `endDate`, and label the export partial whenever another cursor remains. The corrected query enforces known future dates at Gamma and locally; `endDate: null` is never treated as “closing soon.”

**Source-health dashboard.** Aggregate page bytes, attempts, retrieval time, errors, page count, and stop reason. Alert on content-type failures, cursor loops, duplicate market IDs, or a sharp change in valid-row yield. Treat that as an engineering signal, not proof of why the publisher changed its response.

**Editorial change queue.** Supply the prior same-scope snapshot, keep rows with `comparisonStatus: "compared"`, and sort changed named outcomes by absolute difference. An editor should open `sourceURL` and review current context before publication. The Actor does not generate a story, certainty claim, or recommendation.

#### Workflow details

For a sports research queue, save each run’s OUTPUT with the Dataset export. Compare only matching market IDs across two run exports; use `changes` to prioritize review, then open `sourceURL` for source context. For a political newsroom queue, route a changed named outcome price or a material volume change to an editor, retaining `observedAt` and page hash in the handoff. For a 7-day closing queue, route only rows with a valid end date in the requested window; `endDate:null` is not a near-close signal.

No automatic owner schedule, webhook or CRM action is created by this Actor. Your integration owns cadence, downstream authentication, deduplication, retention and editorial decisions. Keep a bounded polling interval, stop after a terminal run status, and treat an ambiguous client timeout as a reconciliation task rather than an instruction to replay paid work.

When exporting CSV, serialize `outcomes` and `changes` or normalize them to child tables keyed by market ID and observation time. Never retain only the first two outcomes: Gamma markets can have more than two, and the delivered row is the complete market snapshot.

For an agent workflow, prescribe a strict order: submit bounded input, wait for one terminal run, read `OUTPUT`, stop or disclose partial coverage, retrieve useful Dataset rows, and retain the snapshot only for an identical scope. Do not let an agent treat an HTTP timeout as permission to create a duplicate paid run.

### 11. Integration recipes

![Schedule or API → Actor → Dataset/OUTPUT → newsroom, CRM, or research queue](https://api.apify.com/v2/key-value-stores/JhI2CK7mTAqEHgqTu/records/polymarket-market-monitor-651f9f1782647e56-readme-workflow.webp?signature=C3KerFZ0nYM6NmeO15Nb)

Start a run through the Apify API with an authorization header and your Actor identifier. `waitForFinish` is capped at 60 seconds by the API. If the response still describes a running job, poll that existing run ID with bounded GET requests. Never retry the POST after an ambiguous client response until you have reconciled whether the first run exists. Fetch Dataset and `OUTPUT` separately after a successful terminal state. Save `OUTPUT.snapshot` unchanged and send it as `previousSnapshot` only with the same stable scope.

#### Practical API recipes

#### JavaScript: bounded run, polling, Dataset and OUTPUT

The run response supplies IDs for the run, default Dataset and default Key-Value Store. Keep them together. Dataset is the market export; OUTPUT is the run contract and the source of the next snapshot. This example deliberately has a finite poll count and does not submit a second run if a request times out.

```js
const token = process.env.APIFY_TOKEN;
const actorId = process.env.ACTOR_ID;
if (!token || !actorId) throw new Error('APIFY_TOKEN and ACTOR_ID are required');
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
const terminal = new Set(['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT']);
const deadline = Date.now() + 240_000;

async function checkedJson(url, { timeoutMs = 25_000, ...options } = {}) {
  const response = await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });
  if (!response.ok) throw new Error(`${options.method ?? 'GET'} ${url}: ${response.status}`);
  const value = await response.json();
  if (value === null || typeof value !== 'object') throw new Error(`Invalid JSON shape from ${url}`);
  return value;
}
function remainingTimeout(limit = 25_000) {
  const remaining = deadline - Date.now();
  if (remaining <= 0) throw new Error('Overall 240-second deadline exceeded');
  return Math.min(limit, remaining);
}

// Submit exactly once. If the response is ambiguous, reconcile before another POST.
const started = await checkedJson(`https://api.apify.com/v2/acts/${actorId}/runs?waitForFinish=60&maxTotalChargeUsd=0.30`, {
  method: 'POST', headers, timeoutMs: 75_000,
  body: JSON.stringify({ category: 'sports', maxItems: 100 }),
});
let run = started.data;
if (!run || typeof run !== 'object' || typeof run.id !== 'string' || typeof run.status !== 'string') {
  throw new Error('Invalid run response');
}
while (!terminal.has(run.status)) {
  const delay = Math.min(3000, deadline - Date.now());
  if (delay <= 0) throw new Error(`Run ${run.id} did not finish before the deadline`);
  await new Promise(resolve => setTimeout(resolve, delay));
  const reply = await checkedJson(`https://api.apify.com/v2/actor-runs/${run.id}`, { headers, timeoutMs: remainingTimeout() });
  if (!reply.data || typeof reply.data !== 'object' || reply.data.id !== run.id || typeof reply.data.status !== 'string') {
    throw new Error('Invalid run status response');
  }
  run = reply.data;
}
if (run.status !== 'SUCCEEDED') throw new Error(`Run ended as ${run.status}`);
if (typeof run.defaultDatasetId !== 'string' || typeof run.defaultKeyValueStoreId !== 'string') throw new Error('Run storage IDs are missing');
const rows = await checkedJson(`https://api.apify.com/v2/datasets/${run.defaultDatasetId}/items?clean=true`, { headers, timeoutMs: remainingTimeout() });
if (!Array.isArray(rows)) throw new Error('Dataset response is not an array');
const output = await checkedJson(`https://api.apify.com/v2/key-value-stores/${run.defaultKeyValueStoreId}/records/OUTPUT`, { headers, timeoutMs: remainingTimeout() });
if (Array.isArray(output) || typeof output.outcome !== 'string' || !output.snapshot || typeof output.snapshot !== 'object') {
  throw new Error('OUTPUT response has an invalid shape');
}
if (output.outcome === 'partial') console.log('Use rows as an observed sample only:', output.coverage);
```

#### Python: persist a compatible snapshot

```python
import json
import os
import pathlib
import urllib.request

token = os.environ["APIFY_TOKEN"]
run_id = os.environ["APIFY_RUN_ID"]
headers = {"Authorization": f"Bearer {token}"}

def get_json(url):
    request = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(request, timeout=25) as response:
        if response.status != 200:
            raise RuntimeError(f"GET {url}: {response.status}")
        value = json.load(response)
        if not isinstance(value, dict):
            raise RuntimeError(f"GET {url}: invalid JSON shape")
        return value

run_reply = get_json(f"https://api.apify.com/v2/actor-runs/{run_id}")
run = run_reply.get("data")
if not isinstance(run, dict) or run.get("id") != run_id or not isinstance(run.get("status"), str):
    raise RuntimeError("invalid run response")
if run["status"] != "SUCCEEDED":
    raise RuntimeError(f"run is not successful: {run['status']}")
if not isinstance(run.get("defaultKeyValueStoreId"), str):
    raise RuntimeError("run Key-Value Store ID is missing")
output_url = f"https://api.apify.com/v2/key-value-stores/{run['defaultKeyValueStoreId']}/records/OUTPUT"
output = get_json(output_url)
if output.get("outcome") not in {"completed", "partial"} or not isinstance(output.get("snapshot"), dict):
    raise RuntimeError(f"unusable OUTPUT: {output.get('outcome')}")
snapshot = output.get("snapshot")
## Store this object exactly. Do not add notes inside it; keep notes beside it.
pathlib.Path("polymarket-snapshot.json").write_text(json.dumps(snapshot, indent=2) + "\n")
next_input = {**snapshot["scope"], "maxItems": 100, "previousSnapshot": snapshot}
assert snapshot["scope"] == {key: next_input[key] for key in snapshot["scope"]}
```

`previousSnapshot` is a data contract, not a cache key. Do not mix a sports snapshot with politics, add an ID to only one side, or reuse a closing-window snapshot with an unbounded browse. The Actor rejects these combinations instead of silently comparing different populations.

#### CSV export without losing diagnostics

CSV is useful for rows but cannot faithfully hold nested outcomes and changes without serialization. Export useful rows and diagnostics separately. Keep the original JSON archive for evidence.

```js
const useful = rows.filter(r => r.recordType === 'polymarket_market');
const diagnostics = rows.filter(r => r.recordType === 'diagnostic');
const csv = useful.map(r => ({
  marketId: r.marketId, question: r.question, outcomes: JSON.stringify(r.outcomes),
  volume: r.volume, liquidity: r.liquidity, endDate: r.endDate, status: r.status,
  observedAt: r.observedAt, sourceURL: r.sourceURL, changes: JSON.stringify(r.changes),
}));
```

### 12. Operating guide

Start with `maxItems:10` to inspect the row contract. Move to 100 only when the bounded cost and partial-coverage semantics fit your workflow. Keep Dataset as the record of delivered market rows. Keep OUTPUT as the record of collection/delivery state. Store snapshots with the date, scope, and run ID in your own system. No owner-authored automatic schedule is enabled by this Actor; create scheduling only within your own authorized Apify workflow.

Before each run, decide whether you need discovery or exact IDs. Use discovery for a bounded current sample with known future end dates. Use IDs when your own system already knows the markets and may need old or closed source records. Keep category, keyword, and closing filters only when you intend them to narrow those IDs.

After each run, check terminal status, then `OUTPUT.outcome`, collection completeness, delivery completeness, source failure, item limit, and delivery uncertainty. Separate free diagnostics from useful rows. Archive the run ID and page evidence with any external report. Do not overwrite a previous complete export with a partial run unless your application explicitly records that loss of coverage.

Before a comparison, copy the entire prior `OUTPUT.snapshot`. Confirm the stable scope is identical. Keep notes, run IDs, and editorial context beside the snapshot rather than inserting extra fields. If the horizon or category changes, establish a fresh baseline.

When budget stops, retain acknowledged rows and their snapshot as a partial observation. Increase Max total charge only after calculating expected delivered rows. When source collection is incomplete, retry only if the operational value justifies another paid observation and the previous run's delivery is reconciled.

For recurring operation, the buyer owns cadence and credentials. Prices, volume, and liquidity can change quickly, so choose a schedule based on the decision being supported rather than assuming a universal refresh rate. Every newly delivered repeat observation is billable even when `changes.changed` is false.

### 13. FAQ

**Why is a repeat billable?** It is a newly delivered current market snapshot, even if no field changed.

**Why did I receive `partial` with useful rows?** The Actor validated and delivered those rows, but the bounded keyset browse ended at an item/page/time limit or a source problem. Read coverage before using absence as information.

**Can I track a closed market?** Use up to 50 explicit IDs. The direct source response can explicitly report `closed`; a browse request deliberately asks for non-closed markets.

**Where is the snapshot?** `OUTPUT.snapshot`. It includes only acknowledged delivered rows and cannot represent unseen source markets.

**Why does an input fail before output?** Invalid fields, oversize snapshots, duplicate IDs, malformed snapshot rows, or a scope mismatch are rejected to avoid ambiguous comparisons and charges.

**Why do browse rows need a future `endDate`?** Browse is the current discovery product. It sends and enforces a lower bound equal to observation time, so missing and already-ended dates cannot fill the result cap.

**Can direct IDs return an already-ended market?** Yes. An explicit identity request uses the direct market endpoint and can preserve the source's current record for old or closed markets. That does not make the row part of current browse discovery.

**What does `marketIsNew` mean?** The market ID was absent from the prior delivered snapshot. It does not prove the market was newly created on Polymarket.

**Are prices guaranteed probabilities?** No. They are source-reported values between zero and one at observation time. Interpretation, normalization, and forecasting belong to your methodology.

**Can one row have more than Yes and No?** Yes. All valid named source outcomes are retained together, up to the bounded contract maximum.

**Why is a 100-row task often partial?** The keyset can contain more matching markets than the buyer cap. If another cursor remains when 100 rows are reached, the Actor truthfully reports `max_items` and incomplete coverage.

**Can I change `maxItems` when comparing?** Yes. It is not part of stable scope. Category, normalized keyword, sorted IDs, and closing horizon must remain the same.

**Does the Actor store a private shared watchlist?** No. You submit IDs or filters each time. The only comparison state is the snapshot you explicitly provide.

**Does it place trades or call a wallet?** No. The Actor reads public Gamma data and writes Dataset/KVS results. It has limited permissions and no trade execution workflow.

### 14. Sources and rights

The source is Polymarket’s public Gamma API. This Actor records publisher and endpoint evidence but does not grant rights in Polymarket data, brands, or market content. Confirm that your intended storage, reporting, redistribution, and commercial use comply with the source’s current terms and your applicable policies. The Actor has limited permissions and needs no buyer secret for public source reads.

Browse uses the public Gamma keyset endpoint. Explicit IDs use the public Gamma market endpoint. Politics category selection sends the verified public politics tag ID `2` and then enforces explicit source category fields locally; sports category selection uses explicit sports market types. `sourceURL` points to a Polymarket event when the response provides an event slug, otherwise to the market slug when available.

Polymarket and related names, marks, questions, and content belong to their respective owners. This Actor is an independent transformation and monitoring tool and is not affiliated with, sponsored by, or endorsed by Polymarket. Product artwork must not be read as official branding.

Source availability, fields, tags, prices, volumes, liquidity, status, and end dates can change. Buyers are responsible for evaluating their intended storage, analysis, publication, and redistribution under current source terms and applicable law. The Actor does not bypass authentication, access controls, robots restrictions, or paywalls.

Retain `observedAt`, market ID, `sourceURL`, and page evidence when using a row in research. A page digest identifies response bytes observed by the Actor; it does not prove future availability or market resolution. Confirm sensitive or consequential claims from appropriate primary evidence before acting.

# Actor input Schema

## `category` (type: `string`):

all is unfiltered. sports and politics use explicit category/tag or fee-type fields supplied by the keyset response.

## `keyword` (type: `string`):

Optional case-insensitive match against source question, slug, or description.

## `marketIds` (type: `array`):

Optional watchlist of up to 50 market IDs. These use documented direct Gamma reads and are combined with other filters.

## `closingWithinDays` (type: `integer`):

Optional upper bound from the source observation time. Markets without a valid source end date do not match.

## `maxItems` (type: `integer`):

Maximum billable market rows. The source pagination remains bounded; also set Max total charge.

## `previousSnapshot` (type: `object`):

Optional snapshot from this actor's prior OUTPUT record. Its stable filters must exactly match this input; incompatible snapshots are rejected before source reads or charges.

## Actor input object example

```json
{
  "category": "all",
  "maxItems": 100
}
```

# Actor output Schema

## `results` (type: `string`):

One billable polymarket\_market row per delivered market. Diagnostic rows are unbilled.

## `summary` (type: `string`):

Contains source coverage, delivery status, and a snapshot made only from delivered rows.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "category": "all",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/polymarket-market-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "category": "all",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/polymarket-market-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "category": "all",
  "maxItems": 100
}' |
apify call zinin/polymarket-market-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/polymarket-market-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/PLjfa905GiUCbpvhY/builds/kS9AZYcN3f7mxaJEl/openapi.json
