AArtery
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?

ProviderSettlement happensBest ingestion transport
PolymarketPolygon chain (UMA OO + ConditionalTokens)Polygon RPC event subscription — source of truth, captures dispute lifecycle
KalshiOff-chain (CFTC-regulated DCM clearing)REST polling — no chain footprint to listen to
Hyperliquid HIP-4HyperCore 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:

ContractAddressEvent
Conditional Tokens0x4D97DCd97eC945f40cF65F87097ACe5EA0476045ConditionResolution(bytes32 conditionId, …, uint256[] payoutNumerators)
UMA Optimistic Oracle V20xeE3Afe347D5C74317041E2618C49534dAf887c24ProposePrice / 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:polymarket

Subscribers 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_URL setPostgresEventStore (migrations applied at boot)
  • DATABASE_URL unsetInMemoryEventStore (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 dev

To 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 dev

Without POLYGON_RPC_URL the chain listener logs a "disabled" notice and exits cleanly — Kalshi + HL HIP-4 still work.

Disable flags

FlagEffect
ART_SETTLEMENT_OFF=trueStop all three settlement sources
ART_EVENT_STORE=memoryForce in-memory store even if DATABASE_URL set
ART_RECONCILE_OFF=trueStop reconcile loop (registry is read-only)
ART_SPREAD_STREAM_OFF=trueStop 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

Edit this page on GitHubLast updated
Settlement Tracking · Artery API Docs