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 Predexy’s 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
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
Predexy’s 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
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.
Example request
The following fetches up to 25 actionable, active opportunities with a minimum score of 70, filtered to risk gate allowed:
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"
Sample response
{
"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.
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.
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)
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.