> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revalonlabs.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetch and Filter Arbitrage Opportunities

> Query live arbitrage opportunities across prediction market platforms using the external API, with filters for score, classification, risk gate, and category.

The arbitrage opportunities endpoint is the core data feed for API key customers. It returns a scored and classified list of price discrepancies detected across Arbitrage integrated platforms — Polymarket, Limitless, Predict.fun, Manifold, PredictIt, Azuro, and Drift — so your bot can evaluate and act on them programmatically. Every opportunity includes fee-adjusted profit estimates, lifecycle status, and a multi-factor quality score to help you separate signal from noise.

## Endpoint

```text theme={null}
GET https://api.predexy.com/api/v1/external/arbitrage/opportunities
```

Authentication: `X-API-Key` header with your `pdx_` key. This endpoint requires the `read:arbitrage` permission.

## Query parameters

| Parameter        | Type            | Default | Description                                                |
| ---------------- | --------------- | ------- | ---------------------------------------------------------- |
| `min_score`      | integer (0–100) | 40      | Minimum arbitrage score to include.                        |
| `classification` | string          | —       | Filter by tier: `actionable`, `informational`, or `noise`. |
| `category`       | string          | —       | Market category, e.g. `crypto`, `politics`.                |
| `status`         | string          | —       | Lifecycle state: `active`, `stale`, or `resolved`.         |
| `guarantee`      | string          | —       | Policy guarantee class: `STRICT` or `QUASI`.               |
| `risk_gate`      | string          | —       | Runtime risk gate: `allowed`, `degraded`, or `blocked`.    |
| `limit`          | integer (1–200) | 50      | Maximum number of opportunities to return.                 |

### Understanding classification

Arbitrage scoring engine assigns each opportunity a score from 0 to 100 based on three factors:

* **Execution quality** — spread magnitude, net profit after fees, fee efficiency
* **Market quality** — liquidity depth, 24-hour trading volume
* **Temporal quality** — time remaining before market expiry (longer is better)

That score maps to a classification tier:

| Classification  | Score range | Additional criteria                    |
| --------------- | ----------- | -------------------------------------- |
| `actionable`    | ≥ 70        | Positive net spread, liquidity > \$500 |
| `informational` | 40–69       | Or low liquidity at any score          |
| `noise`         | \< 40       | —                                      |

### Understanding risk gate

The `risk_gate_state` field reflects the current runtime governance state:

* `allowed` — no restrictions; normal execution
* `degraded` — execution may be affected by platform or global controls
* `blocked` — execution is blocked by policy

<Tip>
  Combine `classification=actionable` and `risk_gate=allowed` to get the best opportunities with no execution restrictions. Add `status=active` to exclude stale or resolved entries.
</Tip>

## Example request

The following fetches up to 25 actionable, active opportunities with a minimum score of 70, filtered to risk gate `allowed`:

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.predexy.com/api/v1/external/arbitrage/opportunities\
  ?classification=actionable\
  &status=active\
  &min_score=70\
  &risk_gate=allowed\
  &limit=25" \
    -H "X-API-Key: pdx_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
  ```

  ```python Python theme={null}
  import httpx

  API_KEY = "pdx_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
  BASE_URL = "https://api.predexy.com"

  params = {
      "classification": "actionable",
      "status": "active",
      "min_score": 70,
      "risk_gate": "allowed",
      "limit": 25,
  }

  response = httpx.get(
      f"{BASE_URL}/api/v1/external/arbitrage/opportunities",
      headers={"X-API-Key": API_KEY},
      params=params,
  )
  response.raise_for_status()
  payload = response.json()

  opportunities = payload["data"]["opportunities"]
  stats = payload["data"]["stats"]

  print(f"Actionable opportunities: {stats['actionable_count']}")
  for opp in opportunities:
      print(
          f"{opp['question_title']} | "
          f"Score: {opp['arbitrage_score']} | "
          f"Net spread: {opp['net_spread']:.3f} | "
          f"Profit/100: ${opp['estimated_profit']:.2f}"
      )
  ```
</CodeGroup>

## Sample response

```json theme={null}
{
  "data": {
    "opportunities": [
      {
        "id": "arb_poly_limit_abc123",
        "canonical_question_id": "550e8400-e29b-41d4-a716-446655440000",
        "question_title": "Will BTC reach $100k by 2026?",
        "question_slug": "will-btc-reach-100k-by-2026",
        "category": "crypto",
        "type": "direct",
        "buy_platform": {
          "id": "a1b2c3d4-...",
          "slug": "limitless",
          "name": "Limitless"
        },
        "sell_platform": {
          "id": "e5f6g7h8-...",
          "slug": "polymarket",
          "name": "Polymarket"
        },
        "buy_price": 0.42,
        "sell_price": 0.48,
        "spread": 0.06,
        "spread_bps": 600,
        "net_spread": 0.02,
        "estimated_profit": 2.00,
        "arbitrage_score": 78,
        "classification": "actionable",
        "status": "active",
        "liquidity": 15000.00,
        "volume24h": 52000.00,
        "risk_gate_state": "allowed",
        "guarantee": "STRICT",
        "detected_at": "2026-04-26T10:15:00Z",
        "fees": {
          "buy_fee": 0.02,
          "sell_fee": 0.02,
          "total_fees": 0.04
        }
      }
    ],
    "stats": {
      "total_opportunities": 47,
      "actionable_count": 12,
      "informational_count": 23,
      "avg_spread": 0.034,
      "max_profit": 8.50,
      "platform_pairs": {
        "polymarket↔limitless": 15,
        "polymarket↔predictfun": 8
      },
      "scanned_questions": 312,
      "duration_ms": 1250,
      "scanned_at": "2026-04-26T10:15:05Z"
    }
  },
  "meta": {
    "count": 1,
    "offset": 0,
    "limit": 25,
    "source": "database"
  }
}
```

## Reading the response

### Opportunity fields

Each object in `opportunities` represents a detected price discrepancy. Key fields:

| Field                            | Description                                                                                                             |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`                             | Unique opportunity identifier                                                                                           |
| `question_title`                 | Human-readable question text                                                                                            |
| `type`                           | `direct` (same outcome, different prices) or `dutch_book` (buy Yes on one platform + No on another for \< \$1 combined) |
| `buy_platform` / `sell_platform` | Platform objects with `id`, `slug`, and `name`                                                                          |
| `buy_price` / `sell_price`       | Prices as probabilities (0–1)                                                                                           |
| `net_spread`                     | Spread remaining after deducting platform fees                                                                          |
| `estimated_profit`               | Estimated profit per \$100 deployed                                                                                     |
| `arbitrage_score`                | Quality score (0–100)                                                                                                   |
| `classification`                 | `actionable`, `informational`, or `noise`                                                                               |
| `status`                         | `active`, `stale`, or `resolved`                                                                                        |
| `risk_gate_state`                | `allowed`, `degraded`, or `blocked`                                                                                     |
| `guarantee`                      | `STRICT` (policy-approved) or `QUASI` (weaker guarantee)                                                                |
| `fees`                           | Breakdown of `buy_fee`, `sell_fee`, and `total_fees` as fractions                                                       |

### Stats object

The `stats` object summarizes the most recent scan across all opportunities (not just the filtered subset you received):

| Field                 | Description                                                      |
| --------------------- | ---------------------------------------------------------------- |
| `total_opportunities` | Total detected in this scan                                      |
| `actionable_count`    | How many scored ≥ 70 with positive profit and adequate liquidity |
| `informational_count` | How many scored 40–69 or had low liquidity                       |
| `avg_spread`          | Average raw spread across all opportunities                      |
| `max_profit`          | Highest estimated profit per \$100 in this scan                  |
| `scanned_questions`   | Total canonical questions evaluated                              |
| `platform_pairs`      | Per-pair opportunity counts                                      |

Use `stats` to understand current market conditions before acting on individual opportunities. A low `actionable_count` or low `avg_spread` signals a quiet market.

### meta.source

The `meta.source` field tells you where the data came from:

| Value      | Meaning                                     |
| ---------- | ------------------------------------------- |
| `database` | Live data served directly from the database |
| `cache`    | Served from a short-lived cache layer       |
| `empty`    | No opportunities matched; response is empty |

## Polling for opportunities

Most trading bots poll this endpoint on a schedule. The background scanner runs continuously, so polling every 30–60 seconds gives you fresh data without burning your rate limit.

```python theme={null}
import httpx
import time

API_KEY = "pdx_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
BASE_URL = "https://api.predexy.com"
POLL_INTERVAL_SECONDS = 30

def fetch_opportunities():
    response = httpx.get(
        f"{BASE_URL}/api/v1/external/arbitrage/opportunities",
        headers={"X-API-Key": API_KEY},
        params={
            "classification": "actionable",
            "status": "active",
            "min_score": 70,
            "risk_gate": "allowed",
            "limit": 50,
        },
        timeout=10,
    )

    if response.status_code == 429:
        reset = int(response.headers.get("X-RateLimit-Reset", time.time() + 60))
        wait = max(0, reset - time.time())
        print(f"Rate limited. Waiting {wait:.0f}s.")
        time.sleep(wait)
        return []

    response.raise_for_status()
    payload = response.json()
    return payload["data"]["opportunities"]

while True:
    opportunities = fetch_opportunities()
    print(f"Found {len(opportunities)} actionable opportunities")
    for opp in opportunities:
        # your execution logic here
        pass
    time.sleep(POLL_INTERVAL_SECONDS)
```

<Note>
  Check `X-RateLimit-Remaining` on each response. If it drops near zero, slow your polling cadence or upgrade your key's rate limit in the Developer Console.
</Note>
