Skip to main content

EDGE Python SDK

Official Python SDK for the EDGE by Predigy prediction market API.

EDGE is a prediction market pricing engine that integrates with sportsbook platforms. This SDK provides a fully-typed async client for all API operations.


Installation

pip install predigy-edge-sdk

Note: The PyPI package name is predigy-edge-sdk, but the import name is still edge_sdk (from edge_sdk import EdgeClient). Do not run pip install edge-sdk — that is an unrelated third-party package.

Current version: 4.3.0 Requirements: Python 3.10+ (dependencies: httpx>=0.25.0, pydantic>=2.0.0)


Quick Start

import asyncio
from edge_sdk import EdgeClient

async def main():
    async with EdgeClient(
        base_url="https://edge-production-7b77.up.railway.app",
        api_key="your-api-key",
    ) as client:
        # List open markets
        result = await client.list_markets(status="OPEN")
        for market in result.markets:
            print(f"{market.title}: YES={market.prices.yes:.1%}, NO={market.prices.no:.1%}")

        # Get a quote before trading
        quote = await client.get_quote("mkt_abc123", side="YES", amount=50.0)
        print(f"Cost: ${quote.total_cost:.2f} for {quote.contracts:.1f} contracts")

        # Execute the trade
        trade = await client.execute_trade("mkt_abc123", side="YES", amount=50.0)
        print(f"Trade {trade.trade_id} executed! New balance: ${trade.new_balance:.2f}")

asyncio.run(main())

Authentication

Every request requires an API key passed in the X-API-Key header. The SDK handles this automatically:

client = EdgeClient(
    base_url="https://edge-production-7b77.up.railway.app",
    api_key="your-api-key",
)

You receive your API key when your operator account is created by the Predigy team.

Key scopes: keys carry a scope — READ_ONLY < TRADE < ADMIN. A READ_ONLY key can list markets, quote, and pull compliance reports but cannot trade; TRADE adds trade/sell and retail flows; ADMIN is required for market creation, settlement, feeds, LP, and config writes. Requests below your key's scope return 403.

Rate limits: the default limit is 60 requests/min per operator key, with higher per-route limits on hot paths (300/min trade+sell, 600/min quote, 120/min reads). Exceeding a limit returns 429 with a Retry-After header (see Error Handling).


API Reference

Markets

# List markets with optional filters
markets = await client.list_markets(status="OPEN", category="NBA", limit=10)

# Get a single market by external ID
market = await client.get_market("mkt_abc123")
from datetime import datetime, timedelta, timezone

# Create a market. State the risk in DOLLARS — `max_exposure` is the most this
# market is modelled to lose, and EDGE derives the engine's liquidity
# parameter from it and the price the market opens at.
market = await client.create_market(
    title="Lakers vs Celtics — Lakers Win",
    category="NBA",
    description="Will the Lakers win tonight's game?",
    max_exposure=10_000.00,
    initial_price_yes=0.55,
    # Any FUTURE UTC timestamp — the server refuses one in the past.
    trading_closes_at=(
        datetime.now(timezone.utc) + timedelta(hours=4)
    ).isoformat(),
)

⚠️ trading_closes_at is required in practice. accept_in_play_trades defaults to True server-side, and an in-play market must declare when trading closes — so supply trading_closes_at, or pass accept_in_play_trades=False for a pre-event-only market. (Before 3.0.0 the SDK exposed neither parameter and create_market() could not succeed at all.)

⚠️ max_exposure is a MODELLED maximum under EDGE's trading controls, not a contractual guarantee. It is mutually exclusive with b_base. Not every amount is expressible, and which ones are depends on the opening price ($207.95–$20.8M at 50¢; $1,173.61–$117.4M at 2¢); an out-of-range amount is refused with a 422 naming the nearest achievable figure, never silently adjusted.

Quotes and Trades

# Get a price quote (does not execute a trade)
quote = await client.get_quote("mkt_abc123", side="YES", amount=100.0)
print(f"Contracts: {quote.contracts}")
print(f"Avg price: ${quote.avg_fill_price:.4f}")
print(f"Fee: ${quote.fee:.2f} ({quote.fee_rate:.2%})")
print(f"Total cost: ${quote.total_cost:.2f}")

# Execute a trade
trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)

# Execute with slippage protection
trade = await client.execute_trade(
    "mkt_abc123", side="YES", amount=100.0,
    max_avg_price=0.60,  # Reject if avg price exceeds $0.60
)

# Sell (cash out) contracts from an existing position
sell = await client.sell_position("mkt_abc123", side="YES", contracts=50.0)
print(f"Net payout: ${sell.net_payout:.2f}")

Idempotency: The API accepts an optional Idempotency-Key header on trade and sell requests — retrying with the same key safely replays the original response instead of executing twice. The SDK does not yet send this header; if you need replay-safe retries today, call the trade/sell endpoints via raw HTTP and set the header yourself.

Portfolio

portfolio = await client.get_portfolio()
print(f"Balance: ${portfolio.balance:.2f}")
print(f"Unrealized P&L: ${portfolio.total_unrealized_pnl:.2f}")

for pos in portfolio.positions:
    print(f"  {pos.market_title} ({pos.side}): {pos.contracts} contracts, P&L: ${pos.unrealized_pnl:.2f}")

Admin Operations

# Get platform statistics
stats = await client.get_stats()
print(f"Total markets: {stats.total_markets}")
print(f"Total volume: ${stats.total_volume:,.2f}")

# Settle a market
result = await client.settle_market("mkt_abc123", outcome="YES")

# Reset sandbox data (demo/sandbox environments only — returns 403 in production)
await client.reset_sandbox()

Sub-Clients (SDK 2.x)

SDK 2.x adds grouped sub-clients alongside the flat 1.x methods (nothing was removed — 2.x is non-breaking):

Sub-client Surface
client.retail Cashier / retail ticket flows
client.parlay Parlay market creation and leg resolution
client.compliance 16 MICS compliance reports + Balance Bonus rebate reads
client.lp Liquidity Provider management

Retail (client.retail)

from edge_sdk.types import MintTicketRequest, RetailTradeRequest, RetailCashoutRequest

# Mint a retail ticket at a cashier terminal
ticket = await client.retail.mint_ticket(MintTicketRequest(...))

# Trade against the ticket (debits ticket balance)
result = await client.retail.execute_retail_trade("ticket_ref", RetailTradeRequest(...))

# Cash out a ticket's position on one market+side
await client.retail.cashout_ticket("ticket_ref", RetailCashoutRequest(...))

# Redeem a winning ticket / check status
await client.retail.redeem_ticket("ticket_ref")
status = await client.retail.get_ticket_status("ticket_ref")

Parlay (client.parlay)

from edge_sdk.types import ParlayCreateRequest, ParlayLegResolveRequest

# Create a 2-3 leg parlay market (admin)
market = await client.parlay.create_parlay(ParlayCreateRequest(...))

# Resolve one leg
await client.parlay.resolve_parlay_leg("mkt_abc123", ParlayLegResolveRequest(...))

Compliance (client.compliance)

16 MICS report methods plus the Balance Bonus rebate reads (shown in the next section). All return the raw report dict. Most daily reports take a date string (plus report-specific filters), but the range-based reports — get_past_post_report, get_large_wagers, get_structuring_alerts, get_operator_config_history — take optional date_from/date_to strings, and get_sport_statistics takes year/month:

report = await client.compliance.get_exception_report(date="2026-07-06")
transactions = await client.compliance.get_daily_transactions(date="2026-07-06")
large = await client.compliance.get_large_wagers(date_from="2026-07-01", date_to="2026-07-06")

Full method list: get_exception_report, get_daily_transactions, get_daily_results, get_daily_wagering_detail, get_daily_wagering_summary, get_past_post_report, get_large_wagers, get_structuring_alerts, get_futures_reconciliation, get_accrual_recap, get_sport_statistics, get_customer_detail, get_customer_summary, get_cutoff_enforcement_log, get_operator_config_history, get_shift_close_report.

Liquidity Providers (client.lp)

⚠️ designate_lp / revoke_lp were removed — designating a Liquidity Provider requires Predigy's own credential, so those methods could only return 403 for an operator. Request an LP designation through Predigy. See the CHANGELOG for the full reasoning.

from edge_sdk.types import LPConfigUpdateRequest

lps = await client.lp.list_lps()
await client.lp.update_lp_config("user_ext_id", LPConfigUpdateRequest(...))
dashboard = await client.lp.get_dashboard("user_ext_id")
activity = await client.lp.get_activity("user_ext_id", limit=50)
analytics = await client.lp.get_analytics()

Balance Bonus (Admin / Compliance)

The counter-flow surge rebate ("Balance Bonus") read + config surface (SDK 2.2.0). Reads require a READ_ONLY key; config writes require an ADMIN key. The backend owns all validation, clamping, and audit — these methods are thin HTTP wrappers.

# Read the rebate ledger (locked credits the operator owes)
ledger = await client.compliance.list_rebate_ledger(
    market_id="mkt_abc123",
    vested=True,
    limit=100,
)

# Per-market rebate period summary
period = await client.compliance.get_rebate_period("mkt_abc123")

# Read the Balance Bonus config (tiers + resolved values + source + clamps)
config = await client.get_balance_bonus_config(market_id="mkt_abc123")

# Tune one market's config (None resets a knob to the inherited value)
await client.update_balance_bonus_config(
    scope="market",
    market_id="mkt_abc123",
    config={"enabled": True, "headline_cap": 0.12},
)

Risk controls & fees (Admin)

Your own risk dials and your own trading fees (SDK 2.3.0). Reads need READ_ONLY; writes need ADMIN.

# Read your fee terms. bounds + platform defaults are SERVED, so render your
# inputs from this payload rather than hardcoding limits.
fees = await client.get_fees()
# {"base_fee_rate": 0.02, "max_fee_rate": None, "bounds": {...}, ...}

# Omitting a key leaves that fee alone; None CLEARS it.
await client.update_fees({"base_fee_rate": "0.02"})  # max_fee_rate untouched
await client.update_fees({"base_fee_rate": None})    # back to the 1.75% default

# Decimal is accepted on both doors, converted to the type each one wants:
# fees -> a decimal STRING (exact; the server parses it as a Decimal)
from decimal import Decimal
await client.update_fees({"base_fee_rate": Decimal("0.0175")})

# Risk dials. A None value deletes a key and reverts to the engine default.
# risk controls -> a FLOAT (the server's validator 422s a numeric string)
await client.update_risk_controls_config({"circuit_breaker": {"caution": Decimal("0.3")}})

⚠️ The two doors take different types on the wire, and that is deliberate. update_fees sends a Decimal as a string because the server parses it back to a Decimal — exact, where float would round-trip through binary. update_risk_controls_config sends a float because the server's risk-control validator accepts only int/float and rejects a numeric string with a 422. You pass a Decimal to either; the SDK picks the right wire type.

⚠️ A fee change takes effect immediately, including on markets already open. There is no per-market fee snapshot, so the next trade on every open market is priced at the new rate.

⚠️ Raising or clearing max_fee_rate CAN raise what your traders pay. It is a ceiling, not a second fee — but if the current ceiling is holding fees down, lifting it releases those charges.

Your fee is your revenue; there is no platform minimum, so 0 is legal on both fields.

Webhooks

# Register a webhook endpoint
webhook = await client.create_webhook(
    url="https://your-app.com/webhook",
    events=["trade.executed", "market.settled"],
    description="Production trade notifications",
)
print(f"Webhook ID: {webhook.webhook.external_id}")
print(f"Secret: {webhook.secret}")  # Store this — shown only once!

# List webhooks
webhooks = await client.list_webhooks()

# Delete a webhook
await client.delete_webhook("whk_abc123")

Health Check

health = await client.health_check()
print(health)  # {"status": "healthy"}

Error Handling

The SDK raises typed exceptions for different error scenarios:

from edge_sdk.exceptions import (
    EdgeAPIError,       # Base class for all API errors
    EdgeAuthError,      # 401 — Invalid or missing API key
    EdgeRateLimitError, # 429 — Too many requests
    EdgeValidationError,# 422 — Invalid request data
)

try:
    trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)
except EdgeAuthError as e:
    print(f"Authentication failed: {e.detail}")
except EdgeRateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
except EdgeValidationError as e:
    print(f"Invalid request: {e.detail}")
except EdgeAPIError as e:
    print(f"API error {e.status_code}: {e.detail}")
    print(f"Request ID: {e.request_id}")  # Useful for support

All exceptions include a request_id field that you can reference when contacting support.


Webhook Verification

When receiving webhook deliveries, verify the HMAC-SHA256 signature to ensure the payload is authentic:

from edge_sdk import verify_signature

# In your webhook handler (e.g., FastAPI)
@app.post("/webhook")
async def handle_webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-Edge-Signature", "")

    if not verify_signature(body, signature, WEBHOOK_SECRET):
        raise HTTPException(401, "Invalid signature")

    event = json.loads(body)
    print(f"Received event: {event['event_type']}")
    # Process event...

The X-Edge-Signature header format is sha256=<hex_digest>.

Webhook event types (37 registered — see docs/API.md for the full list). Common ones:

  • trade.executed — A trade was placed
  • market.created — A new market was created
  • market.settled — A market was settled with an outcome
  • market.suspended — A market was suspended
  • surge.activated — Reserved, not yet emitted. Surge pricing was triggered
  • liquidity.adjusted — Reserved, not yet emitted. Dynamic liquidity parameter changed
  • parlay.created / parlay.leg_resolved — Parlay lifecycle
  • rebate.participant_credited / rebate.period_closed — Balance Bonus

Advanced Usage

Custom HTTP Client

You can provide your own httpx.AsyncClient for custom timeouts, proxies, or connection pooling:

import httpx

custom_client = httpx.AsyncClient(
    base_url="https://edge-production-7b77.up.railway.app",
    timeout=60.0,
    headers={"X-API-Key": "your-api-key", "Content-Type": "application/json"},
    limits=httpx.Limits(max_connections=20),
)

client = EdgeClient(
    base_url="https://edge-production-7b77.up.railway.app",
    api_key="your-api-key",
    http_client=custom_client,
)

Type Safety

The SDK is fully typed with Pydantic models. All responses are validated and provide IDE autocompletion. The py.typed marker (PEP 561) enables type checking in tools like mypy and pyright.


Links


License

Proprietary — see LICENSE. Copyright (c) 2026 Predigy Inc. All rights reserved. Use is permitted only under a separate written license agreement with Predigy Inc.

Release files for predigy-edge-sdk 4.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for predigy-edge-sdk 4.3.0
File Size Uploaded
predigy_edge_sdk-4.3.0.tar.gz 53.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for predigy-edge-sdk 4.3.0
File Interpreter ABI Platform
predigy_edge_sdk-4.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 87.2 kB

Release files / predigy_edge_sdk-4.3.0.tar.gz

Download URL predigy_edge_sdk-4.3.0.tar.gz
Size 53.8 kB
Tags Source
SHA-256 checksum
How to use checksums
3b82e980df9962ecfacf4e597e14015f507fb993050e830cbd34307d53c8d420
BLAKE2b-256 checksum
How to use checksums
d16d06149ad3845e6226ae72bdbfe42ab41578eb97de66003d4b59d91b8ad30e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.16

Release files / predigy_edge_sdk-4.3.0-py3-none-any.whl

Download URL predigy_edge_sdk-4.3.0-py3-none-any.whl
Size 33.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
833c8751ce95625383dd754608ec6b51caf516f68a1c4baad896f1040b15fe38
BLAKE2b-256 checksum
How to use checksums
c0708a791ae590156f223d0b92b91013723230023f3bfcc1fc54c7303befa606
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.16

Release history Release notifications | RSS feed

8.2.0

2 release files

8.1.0

2 release files

8.0.0

2 release files

7.0.0

2 release files

6.0.0

2 release files

5.5.0

2 release files

5.4.0

2 release files

5.3.0

2 release files

5.2.0

2 release files

5.1.0

2 release files

5.0.0

2 release files

This release

4.3.0 This release

2 release files

4.2.0

2 release files

4.1.0

2 release files

4.0.0

2 release files

3.0.0

2 release files

2.3.0

2 release files

2.1.0

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page