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 winning outcome index derived from it, 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

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"
One envelope field will mislead you and it is the first thing to know: freshness.newest_settlement_at currently reports a date in October 2020 and is not a feed-recency signal (see deviation 1 below). 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"

Walk the whole store forward 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.

# first page
curl -sS -H "X-API-Key: $CONVEXLY_API_KEY" \
  "https://www.convexly.app/api/engine/public/resolved-markets?order=block_asc&since_block=6205069&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&limit=200&cursor=$NEXT"

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 and cannot be replayed under a different order.
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.

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 null on effectively every row, by design. A Polygon log carries no timestamp of its own, and resolving one costs an extra RPC per distinct block, which the sweep does not spend. Measured 2026-08-25 16:08 UTC: 3 of 2,838,273 rows carry a settlement time. Derive 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

There is no uptime commitment on this feed, and there will not be one. The path is Vercel to Railway to Supabase, three providers whose targets Convexly does not control, and the operator is one person on active military duty. A number that implied a night rota would be a fabrication. What exists instead is a measured freshness objective, republished with its measurement, and failure semantics designed so a broken read is 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. Back off on your own schedule: the engine's Retry-After header does not currently survive the proxy in front of it (deviation 4).

502 The store could not be read. Never an empty page: an unreadable store and a lane holding nothing are different answers. Retry.

There is no CORS or OPTIONS handling on this path, so the feed is not callable from a browser client today. That is an absence rather than a decision.

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, dated, and re-checked before every revision of this page. The consumer-facing ones, as of 2026-08-27:

1. freshness.newest_settlement_at reports a date in October 2020 on every response; it derives from a column populated on 3 of 2,838,273 rows (2026-08-25 16:08 UTC). Nothing else derives from it.

2. row.settlement_timestamp is null on 2,838,270 of 2,838,273 rows, by the same design decision. Derive 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. Retry-After does not survive the proxy in front of the engine.

5. No CORS or OPTIONS on the path.

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

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