Skip to content
Convexly

Resolved-markets feed

schema 2.0.0

A key-gated, queryable feed of settled conditions read from the Polygon blockchain: one row per ConditionResolution event this project has captured on the shared ConditionalTokens contract, keyed by condition_id, carrying the on-chain payout vector, the payout state and, when exclusive, winning outcome index, the reporting oracle address, the question_id to join your own market metadata on, and the settlement block.

Two properties do the most work. First, it is a contract-wide read, not a venue read: every row comes from one shared contract, 0x4d97dcd97ec945f40cf65f87097ace5ea0476045, any address may report a payout vector to it, and the store held 77 distinct reporting addresses when that was measured on 2026-08-25. The feed publishes oracle and oracle_class on every row because attributing a settlement to any particular venue is a judgement about the reporting address, and the feed will not make it for you. Second, the default page is chain-only: without include_metadata=true the request addresses no venue-derived table at all.

The coverage receipt for the store behind this feed is reproducible against any Polygon RPC endpoint you choose, with a dependency-free checker. Artifacts and the checker are listed on the data page.

Access

Base URL: https://www.convexly.app/api/engine/public/resolved-markets

Authentication: X-API-Key header on every request. Generate keys from Settings → API Keys. Access is plan-gated: the feed is included from the Researcher tier upward under a daily row quota, and Trader carries the higher quota. See pricing for the current tiers.

Keys on this feed run inside a chain-only transport scope: a request cannot reach a venue-operated host from this path. The data is key-authenticated everywhere; there is no unauthenticated route to any of it.

Quick start

For a current Polymarket resolution lookup, its native resolution API already provides lifecycle, payout and transaction fields. Use this feed to replay captured CTF settlements by block range, inspect explicit condition-id misses and read recorded oracle lineage with its coverage receipt. A missing capture does not mean an unresolved market.

Get a page and read the envelope before the rows: universe, coverage and freshness tell you what span the answer is drawn from and how far behind the chain it is.

curl -sS -H "X-API-Key: $CONVEXLY_API_KEY" \
  "https://www.convexly.app/api/engine/public/resolved-markets?limit=5"
freshness.newest_settlement_at only reflects rows with a captured block timestamp. It is not a feed-recency signal and may lag the settlement sweep. Read freshness.age_seconds beside age_measured_from, and coverage.swept_through; those are the fields that answer how current the feed is.

Look up conditions you already care about. Ids the feed does not hold come back by name in requested_not_found:

curl -sS -H "X-API-Key: $CONVEXLY_API_KEY" \
  "https://www.convexly.app/api/engine/public/resolved-markets?condition_id=0xAAA...&condition_id=0xBBB...&limit=200"

Add include_lineage=true for the captured UMA lineage rollup, and read lineage_coverage before interpreting missing history. Payout numerators refer to outcome slots, not Yes/No labels or currency amounts. Multiple positive slots are a split payout, even when the amounts differ.

Replay a fixed block range with cursor, not offset: an offset's cost grows with depth on a store this size, a cursor is a range boundary the index starts at, so its cost does not. Resend with the cursor from the last response until has_more is false.

Save coverage.swept_through from an initial response as UNTIL, then start a new walk with that until_block. Keep every filter and the order unchanged. Cursors enforce order, not filter identity. Pages are not snapshot-isolated: a fixed block ceiling excludes new head blocks, but later repairs and enrichment can still change earlier rows.

# first page, after saving coverage.swept_through as UNTIL
curl -sS -H "X-API-Key: $CONVEXLY_API_KEY" \
  "https://www.convexly.app/api/engine/public/resolved-markets?order=block_asc&since_block=6205069&until_block=$UNTIL&limit=200"
# then, until has_more is false
curl -sS -H "X-API-Key: $CONVEXLY_API_KEY" \
  "https://www.convexly.app/api/engine/public/resolved-markets?order=block_asc&since_block=6205069&until_block=$UNTIL&limit=200&cursor=$NEXT"

If has_more is true but next_cursor is null, continuation is unavailable. Preserve your checkpoint; this is not the end of the dataset.

record_updated_at tracks settlement-row touches, not optional text or lineage changes. New timestamp fills advance it; earlier fills may not have done so. Re-fetch enrichment separately for correction-sensitive use. updated_at_asc is not a complete change feed.

Request parameters

ParameterTypeNotes
condition_idrepeatable stringExact ids, 0x plus 64 hex, at most 100 per call. Ids the feed does not hold come back by name in requested_not_found rather than being dropped.
question_idrepeatable stringExact questionIds, at most 100 per call. Does not produce requested_not_found entries: one questionId can prepare several conditions, so absence is not well defined for it.
since_blockinteger >= 0Inclusive lower bound on settlement_block.
until_blockinteger >= 0Inclusive upper bound on settlement_block.
oraclestringExact reporting address, 0x plus 40 hex, case-insensitive.
limitinteger 1..200Page size. Default 50.
offsetinteger 0..10000Bounded deliberately: its cost grows with depth. Use cursor for anything deeper.
cursoropaque stringnext_cursor from a previous response. Cannot be combined with a non-zero offset or a different order. Keep filters unchanged; filter identity is not enforced by the cursor.
orderenumblock_desc (default, newest settlement first), block_asc (forward replay; the bulk path), updated_at_asc (walks the write stamp; its cost is not measured, so use it only for short incremental polls and prefer a block order for bulk).
include_metadatabooleanAttach stored question and rule text. Off by default; the default page addresses no venue-derived table at all.
include_lineagebooleanAttach the captured UMA lineage rollup and lineage_coverage. Off by default. Missing lineage is not proof that no dispute occurred.

Malformed values are refused with a 400 rather than dropped, so a typo cannot come back as a confident empty page.

The coverage block, and what each guarantee does not cover

Every response carries a coverage block: floor_block, swept_through, head_lag_blocks, rewind_count, no_authorised_rewinds, source. Each field is stated here with its own boundary, because every one of them has a natural over-reading.

Absence is not a negative result. rows_matched counts rows this lane has captured, never markets that settled. Nothing in this feed supports the inference that a missing market did not settle.

The sweep walks contiguously forward from a declared floor. floor_block (6,205,069) is the lowest block the sweep may ever be positioned at, and the cursor only ever advances to a block it has read. The sub-floor claim carries a measured receipt: two independent rescans on unaffiliated RPC endpoints each walked block 0 to 6,205,068 in 621 windows and found every window empty with zero errored reads. The scan artifacts and the standalone checker are linked from the data page.

no_authorised_rewinds is not a contiguity claim. It reports that rewind_count is zero, meaning no span was re-read under a recorded authorisation. It does not say every block in the span produced a row, and it says nothing about blocks below the floor. It is named for what it proves.

capture_completeness describes a block span and nothing else. Every row currently reports windowed, meaning the read that produced it examined the span from capture_from_block to capture_to_block. The stronger value in the schema has no writer, deliberately: coverage is a property of the sweep, so the lane-level statement lives in coverage and never in a per-row stamp.

oracle says who reported, never who was right. The event carries nothing about proposal, dispute, bond, or how a dispute ended. oracle_class is a statement about the address, never about the settlement: unclassified is the default and covers the majority of addresses, and eoa means an externally owned account reported the payout directly, which is a structural fact worth knowing and not an allegation.

settlement_timestamp is optional. A Polygon log carries no timestamp of its own. Separate block reads can populate this field independently of the settlement sweep. Measured 2026-08-25 16:08 UTC: 3 of 2,838,273 rows carried a settlement time at that instant. This historical census is not current timestamp coverage. Timestamp backfilling was added after this census. This page does not provide a current whole-store timestamp coverage measurement; check each returned row for a timestamp. Derive a missing settlement time from settlement_block against a Polygon node you control.

One row per condition is a measurement, not yet a constraint. Measured 2026-08-25 16:08 UTC: 2,838,273 rows, 2,838,273 distinct condition ids, zero nulls, held by writer behaviour rather than a unique index. If your pipeline keys on condition_id, key defensively.

The census above is not the receipt, and they do not agree. The published coverage manifest is an earlier read of the same store: 2,835,501 ConditionResolution events swept through block 92,639,309, generated 2026-08-25 15:45 UTC. The sweep runs every three hours, so the store gained 2,772 rows in the interval between the two reads. The manifest is the hashed, re-derivable figure and is the one to quote back at us; the census is the later count and the only source for the per-column coverage on this page.

Question text and the four metadata states

question_text and resolution_rule_text are venue-published strings, not chain reads, and they are off by default. Measured 2026-08-25 16:08 UTC: 15,217 of 2,838,273 rows carry a non-empty question text. Do not build on this field being present. Every row reports a metadata_state:

suppressed include_metadata was not set, so the registry was never consulted and the two text fields carry no information either way. This is the default and is kept apart from absent on purpose.

absent The registry was consulted and nothing joins. A statement about this project's registry, not about the market.

partial The registry holds one of the two text fields for this condition, not both.

held Both text fields joined. metadata_source names the collector that stored them, so chain-derived fields and venue-derived text stay separable without asking.

Freshness and service posture

No uptime percentage is promised for this feed. Published freshness objectives and their measurements describe the service. Error responses make a broken read distinguishable from an empty one: an unreadable store returns 502 and never an empty page.

The freshness objective: a settlement becomes readable within one sweep interval after it is head_lag_blocks deep (300 blocks, about 7.5 minutes at current Polygon block time). The sweep runs every three hours; target p50 under 3 hours and p95 under 5 hours, measured monthly. Nothing is owed if the objective is missed; what is owed is that the measurement is published and honest.

Read freshness rather than trusting any table. Every response carries age_seconds plus age_measured_from, which says whether that age is the age of the job or the age of the data. One caveat that favours you: stale_after_seconds is 172,800 (48 hours), roughly eleven times the measured p95, so state: "current" is a weak signal. If you need a sharp staleness signal, apply your own rule to age_seconds; six hours is a reasonable line.

Corrections: Convexly publishes corrections to its own figures unprompted: any published figure about this feed that a later measurement contradicts is recorded, key holders whose served rows were affected are notified within 2 business days of confirmation, and a published correction naming the wrong figure, the corrected figure, and the method follows within 5. Support objective: a first substantive reply within one business day, at support@convexly.app. Include the schema_version, the exact request URL, and if you can the freshness.last_run_at value from the response you are asking about.

Deprecation commitment: any breaking change will be announced ahead of time with a minimum of fourteen days between announcement and sunset. The response-header mechanics for that announcement (RFC 9745 Deprecation, RFC 8594 Sunset) are a commitment being built to, not yet emitted on this path; until they are, notice is delivered directly to each key holder.

Errors

400 Malformed filter value, more than 100 ids in one call, an offset past the bound, or a cursor combined with a non-zero offset or replayed under a different order. Fix the request; retrying will not help.

401 Missing, invalid or revoked key.

403 The key owner's plan does not include the feed. The tier gate, not a data problem.

429 Rate limit or daily row quota. Respect Retry-After when supplied; otherwise use bounded backoff. The proxy forwards the engine's retry and rate-limit headers.

502 The store could not be read or its data failed contract validation. Never an empty page. Retry transient failures with bounded backoff; a contract error needs investigation. For a lineage failure, retry without lineage for settlement-only data.

This path has no cross-origin CORS or OPTIONS support. Integrate from your server and keep API keys out of public browser clients.

Versioning

schema_version is the pin. It bumps when a field is removed, renamed, or changes meaning. Additive fields do not bump it, so parse permissively: an unknown field is expected and is not an error. Version 2.0.0 (2026-08-25) was a major bump for three meaning changes: the default became chain-only (text fields null unless include_metadata is set), metadata_state gained the fourth value suppressed, and rows_matched narrowed to condition-id lookups carrying no other filter and no cursor. feed_id is stable and independent of the URL.

Known deviations from the contract

A contract that describes intent rather than behaviour is worse than no contract, so the gaps between the documentation and the code are published with their measurement dates. Timestamp semantics below were reviewed on 2026-09-21; the earlier census remains a historical snapshot.

1. freshness.newest_settlement_at is the newest populated settlement timestamp, not the newest captured settlement. Partial timestamp coverage makes it unsuitable for measuring feed recency. Use the sweep freshness and coverage fields instead.

2. row.settlement_timestamp was null on 2,838,270 of 2,838,273 rows at the 2026-08-25 16:08 UTC census. This is a historical measurement, not current coverage. Derive missing values from settlement_block.

3. order=updated_at_asc has an unmeasured cost against an 8 second statement budget. Prefer a block order for bulk.

4. Earlier proxy versions dropped Retry-After. Forwarding is now implemented and tested (checked 2026-09-20).

5. No cross-origin CORS or OPTIONS support. Use a server-side integration.

6. One row per condition is held by writer behaviour, not by a unique index.

7. With include_lineage=true, the NegRisk operator hop refuses ambiguous, malformed or incompletely read bindings with HTTP 502. It does not select a conflicting target. Retry without lineage for settlement-only data, and treat the missing lineage as unknown.

The full dated deviations table, with status per row, ships with the versioned consumer contract this page renders; ask through the support address above for the current revision if you need the rows this page does not carry.

Back to the API reference · Coverage receipt package · Pricing

Feed id: convexly.polymarket.resolved-markets