On this page
Settlement Tracking
A future release will turn the previously-empty consensusScore into a real signal by
ingesting resolutions from each provider via three different transports —
because each provider exposes settlement at a different layer of the
stack.
Why three sources?
| Provider | Settlement happens | Best ingestion transport |
|---|---|---|
| Polymarket | Polygon chain (UMA OO + ConditionalTokens) | Polygon RPC event subscription — source of truth, captures dispute lifecycle |
| Kalshi | Off-chain (CFTC-regulated DCM clearing) | REST polling — no chain footprint to listen to |
| Hyperliquid HIP-4 | HyperCore L1 (HL's custom chain, not EVM) | HL info poll — closest equivalent to a chain event |
Polling Polymarket's REST API would work — but you'd be reading their reflection of settlement. Listening to the actual Polygon contracts is both lower-latency and captures information REST hides (dispute proposals, DVM voting, etc.).
Architecture
text┌──────────────────────────────────────────────────────────────────────────┐
│ SettlementOrchestrator │
│ starts/stops the three sources, tolerates per-source failure │
└──────────▲────────────────────▲────────────────────▲────────────────────┘
│ │ │
┌───────┴────────┐ ┌────────┴─────────┐ ┌───────┴──────────────────┐
│ Polymarket │ │ Kalshi │ │ Hyperliquid HIP-4 │
│ chain listener │ │ REST poller │ │ info poller │
│ │ │ │ │ │
│ viem + Polygon │ │ markets?status= │ │ /info { type:outcomeMeta }│
│ ↓ events: │ │ settled │ │ ↓ status==='resolved' │
│ • CTF.Condition│ │ ↓ result: │ │ │
│ Resolution │ │ yes/no │ │ │
│ • UMA OO.{ │ │ │ │ │
│ Propose, │ │ │ │ │
│ Dispute, │ │ │ │ │
│ Settle} │ │ │ │ │
│ ↓ also pubs │ │ │ │ │
│ dispute │ │ │ │ │
│ stream │ │ │ │ │
└───────┬────────┘ └────────┬─────────┘ └───────┬──────────────────┘
│ │ │
└────────┬───────────┴────────────────────┘
▼
┌─────────────────────────────┐
│ SettlementService │
│ unified record() interface │
└────────────┬────────────────┘
▼
┌─────────────────────────────┐
│ EventStore │
│ Postgres (DATABASE_URL) │
│ or in-memory fallback │
└─────────────────────────────┘Polymarket: on-chain listener
The chain listener subscribes to two contracts on Polygon:
| Contract | Address | Event |
|---|---|---|
| Conditional Tokens | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 | ConditionResolution(bytes32 conditionId, …, uint256[] payoutNumerators) |
| UMA Optimistic Oracle V2 | 0xeE3Afe347D5C74317041E2618C49534dAf887c24 | ProposePrice / DisputePrice / Settle |
The conditionId from ConditionResolution maps directly to our
market_links.provider_market_id for Polymarket — no decoding required.
The payoutNumerators array tells us polarity:
[yes, no] | Polarity |
|---|---|
[1, 0] | YES wins |
[0, 1] | NO wins |
[0.5, 0.5] | UMA judged invalid |
UMA dispute stream
A bonus side-effect: every UMA ProposePrice / DisputePrice / Settle
log gets republished to:
textartery:stream:settlement_dispute:polymarketSubscribers use this to detect markets entering UMA dispute (which freezes trading and can take 7+ days to resolve via DVM voting). This information is not present in Polymarket's REST API.
Kalshi: REST polling
Kalshi has no chain footprint. Every 5 minutes the poller queries each crypto series for newly-settled markets:
bashGET /trade-api/v2/markets?series_ticker=KXBTCD&status=settled
→ [
{ ticker: 'KXBTCD-26MAY0917-T68000', status: 'settled', result: 'yes', close_time: '...' },
...
]market_links reverse-lookup gives us the universalId; result maps
directly to polarity. The resolutions table is keyed (provider, provider_market_id) so re-polling is idempotent.
Hyperliquid HIP-4: info polling
HL HIP-4 settlement isn't EVM. Every minute the listener calls:
bashPOST /info { type: 'outcomeMeta' }
→ { outcomes: [{ outcome: 10, status: 'resolved', winningSide: 1 }, ...] }winningSide: 1 → YES, 0 → NO. The outcome integer + the
hip4AssetId(outcome, side) formula gives us the YES asset id, which is
the provider_market_id we registered earlier.
Storage
All three sources funnel into SettlementService.record(), which writes
to the pluggable EventStore:
DATABASE_URLset →PostgresEventStore(migrations applied at boot)DATABASE_URLunset →InMemoryEventStore(dev fallback, lost on restart)
Schema:
sqlCREATE TABLE resolutions (
universal_id TEXT NOT NULL REFERENCES artery_events(universal_id),
provider TEXT NOT NULL,
provider_market_id TEXT NOT NULL,
polarity TEXT NOT NULL, -- 'yes'|'no'|'invalid'|'unresolved'
resolved_at TIMESTAMPTZ NOT NULL,
raw JSONB,
PRIMARY KEY (provider, provider_market_id)
);Resolution records survive process restart, so consensus scores accumulate across deploys — which is what makes the score useful.
Local dev
Bring up Postgres and run with the chain listener disabled:
bashdocker compose up -d postgres
DATABASE_URL='postgresql://artery:artery@localhost:5433/artery' \
pnpm --filter @artery/api devTo enable the Polygon listener, add an RPC URL — Alchemy / QuickNode / Infura free tier all work:
bashPOLYGON_RPC_URL='https://polygon-mainnet.g.alchemy.com/v2/<key>' \
DATABASE_URL='...' \
pnpm --filter @artery/api devWithout POLYGON_RPC_URL the chain listener logs a "disabled" notice and
exits cleanly — Kalshi + HL HIP-4 still work.
Disable flags
| Flag | Effect |
|---|---|
ART_SETTLEMENT_OFF=true | Stop all three settlement sources |
ART_EVENT_STORE=memory | Force in-memory store even if DATABASE_URL set |
ART_RECONCILE_OFF=true | Stop reconcile loop (registry is read-only) |
ART_SPREAD_STREAM_OFF=true | Stop the spread + arbitrage publisher |
What you get
bash# Resolution history for one event
curl https://api.artery.questflow.ai/v1/events/<id>/settlement \
-H "Authorization: Bearer $TOKEN"json{
"universalId": "evt_...",
"records": [
{
"provider": "polymarket",
"polarity": "yes",
"resolvedAt": "2026-05-09T12:00:00.000Z",
"raw": {
"source": "on-chain.ctf",
"txHash": "0xabc...",
"blockNumber": 56123456
}
},
{
"provider": "kalshi",
"polarity": "yes",
"resolvedAt": "2026-05-09T17:00:00.000Z",
"raw": { "source": "kalshi-rest", "expiration_value": "..." }
}
],
"consensusScore": 1.0
}A consensusScore < 1.0 is an active risk signal — the linked markets
disagreed on resolution, meaning future events of the same series carry
structural settlement risk. Arbitrageurs should size accordingly.
See also
- Event correlation — how events get clustered
- Arbitrage signals — where consensus score augments the edge calculation