On this page
Event Correlation
Artery's biggest value-add over per-provider SDKs is a single API that treats the same real-world question as one event — even though Polymarket, Kalshi, and Hyperliquid HIP-4 use completely different identifiers and metadata schemas.
The problem
The same question shows up everywhere differently:
| Provider | Identifier | Where threshold lives |
|---|---|---|
| Polymarket | 0xa8aceb0fb04828602057be289979652104e8f7b7ebda4489c53a97c60b9063d9 (256-bit hash) | Inside question text |
| Kalshi | KXBTCD-26MAY0917-T69999.99 (hierarchical ticker) | Suffix after T + floor_strike field |
| Hyperliquid HIP-4 | outcome: 10 (integer) | description KV string targetPrice:79583 |
There is no external authority that maps these. Artery builds the mapping itself.
Structured parsers (current)
For crypto price-binary markets, all three providers expose enough structure to parse without NLP:
textHIP-4 description: class:priceBinary|underlying:BTC|expiry:20260509-0600|targetPrice:79583
↓ parse
{ underlying: 'BTC', threshold: 79583, op: 'gte', expiresAt: '2026-05-09T06:00:00Z' }
Kalshi ticker: KXBTCD-26MAY0917-T69999.99
↓ parse
{ underlying: 'BTC', threshold: 69999.99, op: 'gte', expiresAt: '2026-05-09T17:00:00Z' }
Polymarket question: "Will the price of Bitcoin be above $70,000 on May 9?"
↓ regex + endDateIso
{ underlying: 'BTC', threshold: 70000, op: 'gte', expiresAt: '2026-05-09T00:00:00Z' }Canonical clustering key
Each parsed market is hashed into a canonical key; markets sharing a key
join the same ArteryEvent:
textcanonical_key = `${underlying}|${op}|${round(threshold)}|${YYYY-MM-DD}`
= "BTC|gte|70000|2026-05-09"Day-level (rather than full-timestamp) precision lets Polymarket "by May 9" cluster with Kalshi "26MAY0917" — they resolve on the same trading day.
What gets clustered today
Live numbers from a recent run:
| Provider source | Markets parsed | Source |
|---|---|---|
| Polymarket | 80 | regex_title |
| Kalshi | 577 | structured_ticker |
| Hyperliquid HIP-4 | 1 | structured_description |
| Universal events created | 504 | |
| Multi-provider clusters | 17 (all BTC ≥ $70K-$88K on 2026-05-09 across Polymarket × Kalshi) |
The Polymarket vs Kalshi pair is the most active cluster because both venues list dense daily price-binary grids. HIP-4 is sparse (1 market today) and its strikes (e.g. $79,583) rarely match other venues' round-number strikes.
Endpoints
| Endpoint | What |
|---|---|
GET /v1/events | List + filter universal events |
GET /v1/events/:id | One event with all linked markets + metadata |
GET /v1/events/:id/spread | Cross-platform price snapshot — yesBid/yesAsk/yesMid per leg |
GET /v1/events/:id/arbitrage | Best buy-leg / sell-leg + net edge after fees (no threshold) |
GET /v1/events/:id/settlement | Per-provider resolution history + consensus score |
POST /v1/events/reconcile | Force a reconcile pass (admin scope) |
POST /v1/events/spread-stream/tick | Force a spread-stream tick (admin scope) |
bashcurl 'https://api.artery.questflow.ai/v1/events?underlying=BTC&min_providers=2&limit=5' \
-H "Authorization: Bearer $TOKEN"json{
"events": [
{
"universalId": "evt_94c9a200-8152-43b1-8b10-46214bea101a",
"canonicalName": "BTC ≥ $70,000 by 2026-05-09",
"kind": "price_binary",
"underlying": "BTC",
"threshold": 70000,
"op": "gte",
"resolutionDate": "2026-05-09T00:00:00Z",
"links": [
{
"provider": "polymarket",
"providerMarketId": "0xa8aceb0fb04828602057be...",
"source": "regex_title",
"metadata": {
"yesTokenId": "55007456209846753968...",
"noTokenId": "10093425111459431137..."
}
},
{
"provider": "kalshi",
"providerMarketId": "KXBTCD-26MAY0917-T69999.99",
"source": "structured_ticker"
}
]
}
]
}Filtering
GET /v1/events supports the following query params:
| Param | Type | Effect |
|---|---|---|
underlying | string | BTC, ETH, SOL, etc. — case-insensitive |
resolution_after | ISO 8601 | Only events resolving after this time |
resolution_before | ISO 8601 | Only events resolving before this time |
min_providers | int | Only multi-provider clusters when ≥2 |
limit | int | Cap result size |
Recent additions
| Feature | Status | Notes |
|---|---|---|
price_range event kind | ✅ | Kalshi range markets with both cap_strike + floor_strike produce kind: 'price_range' events |
Spread streaming artery:stream:event:<id> | ✅ | 5s tick, change-debounced 0.5¢ |
Arbitrage signal stream artery:stream:arbitrage:<id> | ✅ | Net-edge after fees ≥ 50 bps default |
GET /v1/events/:id/arbitrage | ✅ | One-shot computation with no threshold |
GET /v1/events/:id/settlement | ✅ | Per-provider resolution history + consensus score |
| Per-provider fee schedule | ✅ | ART_FEES_<PROVIDER>_TAKER_BPS env overrides |
| Settlement consensus score | ✅ | In-memory; Postgres persistence on roadmap |
What this is not (yet)
- Election / outcome / sports markets are not clustered yet — they need entity-NER + temporal matching (roadmap)
- Approximate threshold matching — Polymarket "above $70k" and Kalshi
T69999.99happen to round to the same $70K bucket, butT69500would NOT cluster with$70K(roadmap) - Embedding-based clustering for long-tail markets (roadmap)
- Postgres persistence of registry + settlement history (roadmap)
How parsers fail (and that's OK)
Parsers return null for anything they can't handle. The reconcile loop
silently drops nulls and only registers what it understands. So:
- Sports markets, custom event markets, jokes, and anything non-crypto
→ not in
/v1/events - Range markets (Kalshi has
cap_strike+floor_strike) → not yet parsed class:otherHIP-4 outcomes → not parsed
This keeps the registry tight and high-confidence. Recent versions progressively relaxes parsers as we add NER + embedding fallbacks.
Reconcile schedule
The reconcile job runs:
- Once on app bootstrap (1.5s after listen)
- Every 5 minutes thereafter
- On-demand via
POST /v1/events/reconcile(admin scope)
Disable with ART_RECONCILE_OFF=true (useful for tests where upstream
calls are noisy).
See also
- Cross-platform spread recipe — using
/v1/events/:id/spreadto find arbitrage edges - Native vs Normalized — wire shape