Skip to content
Get an API key

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.

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.

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.compatible is a cheap fingerprint veto. Its reason is one of threshold_conflict, stage_conflict, source_conflict, or empty. Compatible does not mean verified equivalent.
  • resolutionEquivalence is the stronger statement — verified only 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.

Terminal window
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);
}
{
"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.

  • netEdge, roi, totalCost, vwap, cost, fee and settlementFee are all nullable. A null is “not priced”, not “zero”. Filtering with row.netEdge >= x silently drops nulls in some languages and admits them in others — test for null explicitly.
  • fee is a verified taker fee or nothing. Where a venue’s schedule is not verified, the leg carries a note explaining why no model was applied, and the edge is unknown rather than optimistic.
  • page.total may be null, which means not counted, not zero. Paginate on hasMore and nextCursor.

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.