Polymarket Market Monitor avatar

Polymarket Market Monitor

Pricing

from $2.10 / 1,000 market snapshot delivereds

Go to Apify Store
Polymarket Market Monitor

Polymarket Market Monitor

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

Pricing

from $2.10 / 1,000 market snapshot delivereds

Rating

0.0

(0)

Developer

Tim Zinin

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Categories

Share

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.

Polymarket Market Monitor icon

Polymarket Market Monitor: filter → Gamma evidence → market decisions

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 planPer delivered market snapshotPer 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

FieldType and boundMeaning
categoryall, sports, politics; default allBrowse classification. Sports uses documented sports market types; politics sends the verified public tag ID 2.
keywordoptional text, 200 charactersCase-insensitive match against source question, slug, or description.
marketIdsoptional unique numeric IDs, maximum 50Exact public Gamma market reads. Combined with other filters.
closingWithinDaysoptional integer 0–30End date must be at or after observation time and no later than this many days after it. Missing/invalid dates do not match.
maxItems1–1,000; default 100Maximum useful rows delivered and charged.
previousSnapshotoptional object, 512 KiB, 1,000 rowsPrior 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

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

ACCEPTED CLOUD CANARY example A — exact first market row

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

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

LOCAL LIVE example B — exact first market row

{
"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:

{
"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.

{
"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 fieldContract
recordTypepolymarket_market for useful rows; diagnostic for free status rows.
marketIdGamma market identifier as a string.
question, slugSource market text and optional source slug.
outcomesEvery source outcome accepted for this row: {name, price}. Prices are source values, not probabilities guaranteed by this Actor.
volume, liquidityNumeric source values or null when unavailable.
endDateValid source timestamp or null; not a derived resolution time.
statusactive, closed, or unknown, only from source fields.
sourceURLPolymarket event or market URL when a source slug is available.
observedAtActor observation timestamp.
sourcePublisher, exact retrieval endpoint/access, and page-level hash/bytes/time evidence for that row.
sourceDetailsDiagnostic-only bounded source failure context: safe route, attempts, and optional HTTP status.
changesComparison state and price/volume/liquidity/status differences when a compatible snapshot exists.
billingRequest 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 outcomeUse it forDo not conclude
completedDelivered rows and completed collection coverage.Future availability or settlement.
partialValidated delivered rows plus explicit coverage limits/failure.That absent markets are closed or nonexistent.
emptyA completed scope with no matching delivered market.That a broader Polymarket category is empty.
budget_stoppedFree diagnostic and count state.That any source market was read.
failedError 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

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.

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

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.

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.