> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# 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](https://portal.predictefy.com/keys).

:::caution[The Free plan excludes this feed]
Calling `fetchArbitrage` on a Free key returns `PLAN_REQUIRED`. See
[Credits & billing](/guides/credits/) for what each plan includes.
:::

## 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

```sh
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.

```js
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

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

:::danger[Revalidate immediately before execution]
Every field is a snapshot carrying `asOf`. A pair that qualified forty seconds ago may be gone,
partly filled, or closed. Re-read the books and re-check market status at the moment of
execution, every time.
:::

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](/guides/trading/). 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](/reference/venues/) 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:

| 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](/guides/credits/).

## Related

- [Cross-venue data](/guides/cross-venue/) — how matching works, and why a gap is not an edge
- [Market relationships](/guides/market-relationships/) — related markets and hedge candidates
- [Prediction markets](/guides/prediction-markets/) — why venues disagree in the first place
