Predictefy Docs
Browse documentation

Scan for qualified cross-venue opportunities

Poll fetchArbitrage for gate-checked results, read every rejection reason, and revalidate before acting.

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

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

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.

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://data.predictefy.com';

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

{
  "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

  • 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.

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.

Cost

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:

IntervalQueries/dayCredits/dayCredits/30 days
5 min2884,320129,600
60 s1,44021,600648,000
10 s8,640129,6003,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.