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, records why
non-qualifying candidates were rejected — so you can tell “no edge today” from “my size is too
large” — and revalidates anything it is about to act on.
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.
Anything short of that stays 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 a match score, never a confidence. Two markets can read alike and settle
differently, which is exactly why the resolution gates exist.
Request
Section titled “Request”curl -s "$PREDICTEFY_API_URL/api/router/fetchArbitrage?executableOnly=true&contracts=1000" \ -H "Authorization: Bearer pk_live_YOUR_KEY"contracts is 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.
Drop executableOnly=true to also receive non-qualifying candidates with their reasons. That is
useful while tuning and more expensive to run continuously.
const BASE = 'https://reads-production.up.railway.app';
async function scan({ contracts = 1000, minNetEdge = 0.01 } = {}) { const url = new URL('/api/router/fetchArbitrage', BASE); url.searchParams.set('executableOnly', 'true'); url.searchParams.set('contracts', String(contracts));
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 []; throw new Error(`${body.error.code}: ${body.error.message}`); }
// netEdge is nullable — an unpriced pair is not a zero-edge pair. return body.data.filter((row) => row.netEdge !== null && row.netEdge >= minNetEdge);}Response
Section titled “Response”{ "success": true, "data": [ { "clusterId": "clr_9f2a7c41", "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", "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-12T14:22:08.412Z" } ]}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”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