GuidesCookbook
Scan for qualified cross-venue opportunities
The same question trades on more than one venue, and the two prices are rarely identical. Most of those gaps are not tradeable. This recipe keeps only the ones that survive every gate.
What you will build
Section titled “What you will build”A polling worker that calls fetchArbitrage with executableOnly=true and revalidates anything it
is about to act on. When you need to diagnose an empty result, make a separate call without
executableOnly=true; only that unfiltered response includes non-qualifying candidates and their
rejection reasons.
Prerequisites: a Builder plan or above, and an API key from the dashboard.
The gates
Section titled “The gates”fetchArbitrage is the only verb that applies the word arbitrage to anything, and it does so
only when every gate passes: live non-synthetic asks on both legs, both markets open, real depth
at the size you asked for, verified per-venue fees, compatible resolution rules, and a net edge
that survives all of it.
Returned rows that fall short of that stay labelled an indicative price discrepancy. label carries exactly
two values — arbitrage and indicative price discrepancy — and executable is the boolean
form of the same judgement. There is no partial credit.
Two fields describe resolution, and they are not the same claim:
resolution.compatibleis a cheap fingerprint veto. Itsreasonis one ofthreshold_conflict,stage_conflict,source_conflict, or empty. Compatible does not mean verified equivalent.resolutionEquivalenceis the stronger statement —verifiedonly when a high-confidence persisted verdict matches both legs’ current resolution-rule content hashes.
similarity is candidate similarity from matching, not matchScore or judge confidence. Two
markets can read alike and settle differently, which is why the resolution gates exist.
Match score
Section titled “Match score”matchScore compares resolution rules on a scale from 0 to 100. A score of 100 requires a
fresh, high-confidence equivalent verdict under the current judge identity and rule hashes.
It does not establish executable prices, available depth, or a profitable trade.
matchBand | Score | Meaning |
|---|---|---|
verified | 100 | Rules verified equivalent; every execution gate still applies. |
minor | 85 to 99 | Visible as indicative, unverified and non-executable. |
material | 60 to 84 | Visible with the listed differences, unverified and non-executable. |
hidden | Below 60, or a veto | Excluded from the feed; qualification can still explain it. |
unscored | null | No current verdict; visible unless the close-date veto applies. |
The fixed deductions below come from
MATCH_SCORE_DEDUCTIONS and CONFIDENCE_PENALTY in packages/arbitrage/src/match-score.ts.
The model reports differences; code computes the number.
| Difference kind | Points deducted |
|---|---|
event_identity | 50 |
deadline_handling | 20 |
resolution_authority | 15 |
cancellation_handling | 10 |
successor_handling | 10 |
other | 5 |
confidence when medium | 5 |
confidence when low | 10 |
Each judge difference kind is charged once, with its explanations joined. confidence is a
scoring-only kind. An equivalent verdict at medium or low confidence scores 95 or 90 and stays
unverified. matchDifferences lists every charged { kind, detail, points } entry, including
confidence. For every visible numeric score, the points sum to 100 - matchScore.
A polarity inversion hides the pair. Close dates more than 21 days apart also hide it, even if
there is no verdict yet. A numeric veto is accounted for with 100 points (polarity, or
deadline_handling with detail close_time_mismatch); this is a hide rule, not a graded
fine-print difference. Without a close-date veto, insufficient evidence or a malformed verdict
has no numeric score.
Hidden numeric scores use max(0, 100 - sum(points)).
The meaning of resolutionEquivalence: "verified", executable, and the arbitrage label is
unchanged. A score below 100 never enables execution. The qualification endpoint exposes the
same explanation as checks.resolutionEquivalence.matchDeductions; its differenceKinds
contains judge kinds only, while differences remains a string list for compatibility.
The match score is a rules comparison produced by an automated judge from the two venues’ published resolution rules; it is not a probability that the markets settle identically, and trading a pair below 100 is your own risk.
Request
Section titled “Request”curl -s "$PREDICTEFY_API_URL/api/router/fetchArbitrage?executableOnly=true&contracts=1000&limit=500" \ -H "Authorization: Bearer pk_live_YOUR_KEY"contractsis the size the assessment is made at — depth and fees are judged for that fill, not for one contract. Change it and the answer legitimately changes.limitsupports up to 500 rows per page, paginated viacursor(page.nextCursor).executableOnly=truereturns only rows passing every executable gate. Drop it to also receive visible non-qualifying candidates with rejection reasons; hidden match bands remain excluded.venues(optional) keeps a row when either leg touches the listed venues (for example,venues=polymarket,kalshi). It does not require both legs to stay inside the list.minEdge(optional) filters rows by minimum net edge.
const BASE = 'https://data.predictefy.com';
async function scan({ contracts = 1000, minNetEdge = 0.01, venues } = {}) { let cursor; const results = [];
do { const url = new URL('/api/router/fetchArbitrage', BASE); url.searchParams.set('executableOnly', 'true'); url.searchParams.set('contracts', String(contracts)); url.searchParams.set('limit', '500'); if (venues) url.searchParams.set('venues', venues.join(',')); if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.PREDICTEFY_API_KEY}` }, }); const body = await res.json();
if (!body.success) { // PLAN_REQUIRED on Free keys; RATE_LIMITED and INSUFFICIENT_CREDITS are also expected. if (body.error.retryable) return results; throw new Error(`${body.error.code}: ${body.error.message}`); }
// netEdge is nullable — an unpriced pair is not a zero-edge pair. results.push(...body.data.filter((row) => row.netEdge !== null && row.netEdge >= minNetEdge)); cursor = body.page?.hasMore ? body.page.nextCursor : undefined; } while (cursor);
return results;}Response
Section titled “Response”{ "success": true, "data": [ { "clusterId": "clr_9f2a7c41:kalshi:polymarket", "question": "Will the Fed cut rates at the January 2026 meeting?", "similarity": 0.94, "contracts": 1000, "legs": { "buyYes": { "venue": "kalshi", "canonicalMarketId": "kalshi:FED-26JAN-CUT", "side": "yes", "executable": true, "reasons": [], "vwap": 0.412, "cost": 412.0, "fee": 3.7, "feeBasis": "general", "filled": 1000, "fullyFilled": true }, "buyNo": { "venue": "polymarket", "canonicalMarketId": "polymarket:0x7d3f...c19a", "side": "no", "executable": true, "reasons": [], "vwap": 0.559, "cost": 559.0, "fee": 0.0, "feeBasis": "general", "filled": 1000, "fullyFilled": true } }, "resolutionEquivalence": "verified", "matchScore": 100, "matchBand": "verified", "matchDifferences": [], "resolution": { "compatible": true, "reason": "", "auditReasons": [] }, "settlementFee": 0.0, "totalCost": 974.7, "payout": 1000.0, "netEdge": 25.3, "roi": 0.026, "executable": true, "reasons": [], "label": "arbitrage", "asOf": "2026-08-31T14:22:08.412Z" } ], "meta": { "live": true, "contracts": 1000, "provenance": { "source": "predictefy-live" }, "asOf": "2026-08-31T14:22:08.412Z", "seq": 1042, "source": "published" }, "page": { "limit": 500, "offset": 0, "total": 1, "hasMore": false, "nextCursor": null }}Every leg carries its own executable and reasons, so a pair can fail on one side only. Read
the leg-level reasons when the pair-level reasons array does not explain enough.
fullyFilled is the field to branch on for sizing: filled is what the walked asks could
actually absorb, and it is less than or equal to what you requested.
Reading the numbers honestly
Section titled “Reading the numbers honestly”meta.sourcediscloses the data origin.'published'means the response was served from the live published surface (fresh within the publisher’s self-declared cadence (about 30s; 3s floor));'computed-fallback'indicates bounded on-demand computation over at most 10 candidate clusters when the published surface is unavailable. One candidate cluster can emit multiple ordered pair rows.meta.asOfandmeta.seqrecord the timestamp and sequence number.netEdge,roi,totalCost,vwap,cost,feeandsettlementFeeare all nullable. A null is “not priced”, not “zero”. Filtering withrow.netEdge >= xsilently drops nulls in some languages and admits them in others — test for null explicitly.feeis a verified taker fee or nothing. Where a venue’s schedule is not verified, the leg carries anoteexplaining why no model was applied, and the edge is unknown rather than optimistic.page.totalmay benull, which means not counted, not zero. Paginate onhasMoreandnextCursor.
Before you act on a result
Section titled “Before you act on a result”Good market data on a venue does not mean you can trade there. GET /v1/exec/venues is the
authoritative list of execution lanes — see Trading & execution. Several
venues serve real books with no hosted trading lane at all.
Venues with reconstructed books cannot qualify by design: a synthetic book is a faithful representation of price and is not executable depth. Venue coverage records which venues have a real book.
An arbitrage query is the most expensive read on the platform at 15 credits; a cross-venue price comparison is 10 and an order-book snapshot is 5. Continuous polling adds up:
| Interval | Queries/day | Credits/day | Credits/30 days |
|---|---|---|---|
| 5 min | 288 | 4,320 | 129,600 |
| 60 s | 1,440 | 21,600 | 648,000 |
| 10 s | 8,640 | 129,600 | 3,888,000 |
Budget before you poll, and add revalidation on top — 5 credits per leg per check. Current weights and plan allowances are in Credits & billing.
Related
Section titled “Related”- Cross-venue data — how matching works, and why a gap is not an edge
- Market relationships — related markets and hedge candidates
- Prediction markets — why venues disagree in the first place