Resolutions feed
Cursor-paged bulk read of on-chain condition resolutions, for mechanical settlement.
GET /v1/resolutions is the pull-side companion to the
market.resolved webhook. It exists for one job: letting a bot
settle resolved positions mechanically, with no dependence on having been
online when the resolution happened.
If your bot learns about resolutions only from webhooks, it will eventually miss one — and then it carries that position forever, on mismarked equity, with the capital stuck in a slot that never frees. Persist a cursor and sweep this feed.
Quick start
curl -s "https://api.polyrank.app/v1/resolutions?limit=200&binary_only=true" \
-H "authorization: Bearer $POLYRANK_API_KEY" | jq{
"rows": [
{
"condition_id": "0x5f65…",
"question_id": "0x9c01…",
"oracle": "0xa1b2…",
"outcome_slot_count": 2,
"payout_numerators": ["1", "0"],
"winner_outcome": "YES",
"winner_index": 0,
"is_binary": true,
"resolved_block": 78431022,
"resolved_time": "2026-07-21T18:02:55Z",
"source": "ctf",
"tx_hash": "0xab12…",
"log_index": 41,
"updated_at": "2026-07-21T18:03:10Z"
}
],
"next_cursor": "Nzg0MzEwMjI6NDE",
"has_more": true,
"contract": { "ordering": "…", "backfill_caveat": "…", "recommended_pattern": "…" }
}Parameters
| Param | Default | Meaning |
|---|---|---|
cursor | — | Opaque cursor from a previous response's next_cursor. |
since_updated_at | — | ISO-8601. Pages by ingestion time instead. Mutually exclusive with cursor. |
limit | 200 | 1–1000. |
binary_only | false | Restrict to 2-outcome conditions. |
Paging contract
Ordering is a keyset over (resolved_block, log_index) ascending — the on-chain
order, which is total and immutable. Paging is strictly forward and, over a stable
dataset, never skips or repeats a row. Page while has_more is true.
next_cursor is null only when a page came back empty, which means you are
caught up. Keep your previous cursor; do not persist the null.
The one thing that can bite you
The resolutions table also receives backfilled and re-org-corrected rows. Such a row can land with a block number older than a cursor you have already passed, so a pure cursor consumer will never see it.
That is why since_updated_at exists: it pages by ingestion time and does surface
those rows, at the cost of re-delivering rows you already have — harmless, because
settlement is idempotent.
Recommended pattern
// steady state: drain forward
let cursor = await store.get('resolutions_cursor');
for (;;) {
const url = new URL('https://api.polyrank.app/v1/resolutions');
url.searchParams.set('limit', '500');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await get(url);
if (page.rows.length === 0) break; // caught up — keep the old cursor
for (const r of page.rows) await settleIdempotently(r);
cursor = page.next_cursor;
await store.set('resolutions_cursor', cursor);
if (!page.has_more) break;
}
// hourly, additionally: absorb backfilled corrections
const since = new Date(Date.now() - 3 * 86400e3).toISOString();
const sweep = await get(`…/v1/resolutions?since_updated_at=${since}&limit=1000`);
for (const r of sweep.rows) await settleIdempotently(r);Pair this with the market.resolved webhook for low latency. The webhook makes
you fast; this feed is what guarantees you never miss one.
Field notes
winner_outcomeisYES/NOfor binary conditions, may beINVALID, and isnull(withwinner_index: -1) on ties and multi-outcome payouts. Settle onpayout_numeratorswhenwinner_indexis-1.payout_numeratorsare returned as strings — they are rawUInt256values as emitted on-chain and do not fit a JSON number.sourceisctfornegrisk. One scan covers both binary and neg-risk sub-conditions.