Metadata-Version: 2.4
Name: predigy-edge-sdk
Version: 5.4.0
Summary: Official Python SDK for the EDGE by Predigy prediction market API (retail, parlay, compliance/MICS, LP)
Author: Predigy Inc.
License: Proprietary
Project-URL: Homepage, https://edge-by-predigy.netlify.app
Project-URL: Repository, https://github.com/Predigy-LLC/EDGE
Project-URL: Documentation, https://edge-production-7b77.up.railway.app/docs
Project-URL: Bug Tracker, https://github.com/Predigy-LLC/EDGE/issues
Keywords: edge,predigy,prediction-market,lmsr,sdk,retail,parlay,compliance,mics,liquidity-provider
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: mypy==2.3.0; extra == "dev"
Dynamic: license-file

# EDGE Python SDK

Official Python SDK for the [EDGE by Predigy](https://edge-by-predigy.netlify.app) 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

```bash
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:** 5.4.0
**Requirements:** Python 3.10+ (dependencies: `httpx>=0.25.0`, `pydantic>=2.0.0`)

---

## Quick Start

```python
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:

```python
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](#error-handling)).

### Player identity (`client.for_user`)

**Without this, every quote, trade, sell and portfolio read on your API key acts as ONE shared account** — one wallet, one set of cooldowns, one holding for the sell-quote "exceeds the player's holding" check, and no restricted-player match. The EDGE API tells players apart by the `X-Edge-User-Id` header; `client.for_user(user_id)` returns a client that sends it on every request (flat methods and every sub-client alike), with your own reference for the player as the value. The client you built is unchanged and keeps sending no header. A scoped client is a shallow copy over the **same** `httpx` client (one connection pool, however many players) — make one per request, call `for_user` again to switch players (it replaces, never stacks), and close the client you built, not the scoped one (closing a scoped client is a no-op).

```python
player = client.for_user("their-player-id")   # 1-64 chars of A-Z a-z 0-9 @ . _ : + -
quote = await player.get_quote("mkt_abc123", side="YES", amount=100.0)
trade = await player.execute_trade("mkt_abc123", side="YES", amount=100.0)
portfolio = await player.get_portfolio()      # THIS player's wallet and positions
```

It is a client-level method rather than a kwarg on each call because *who the caller is acting as* is a property of a session, not of one request — and one seam covers every player-scoped route, today's seven and any added later. (`idempotency_key` stays per call because idempotency genuinely is per call.) The server's rule for the value — 1–64 characters of `A-Z a-z 0-9 @ . _ : + -` — is enforced by the SDK before any request, with `EdgeValidationError`: the cheap failures are silent otherwise (`""` is treated by the server as no header — the shared account again). EDGE hashes the value into its own `user_external_id` and stores only the hash, so responses and webhooks carry `user_external_id`, never the value you sent.

---

## API Reference

### Markets

```python
# 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")

# Simulated order-book depth (num_levels 0-50, step_cents 1-10) and trade history, newest first (limit 1-200)
depth = await client.get_depth("mkt_abc123", num_levels=5, step_cents=2)
history = await client.get_history("mkt_abc123", limit=20)
```

```python
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 and
> the time to the event ($69.32–$6.93M at 50¢ and $391.21–$39.1M at 2¢ for a
> market created a day or more out, `k = 1.0`; 1.5x those inside 15 minutes); an
> out-of-range amount is
> refused with a `422` naming the nearest achievable figure, never silently
> adjusted.

### Quotes and Trades

```python
# 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
)

# Preview a sell's payout without executing (quote.contracts is what would actually close)
quote = await client.get_sell_quote("mkt_abc123", side="YES", contracts=50.0)
print(f"Would net: ${quote.net_payout:.2f}")

# 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}")

# Sell with a floor: the WHOLE sell is refused (EdgeAPIError, 400) if the average fill would be below it
sell = await client.sell_position("mkt_abc123", side="YES", contracts=50.0, min_avg_price=0.45)

# Replay-safe retries: generate a key ONCE per attempt, persist it with the order, and
# retry a timed-out call with the SAME key — the replay carries idempotent_replay=True
import uuid

key = str(uuid.uuid4())
trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0, idempotency_key=key)
sell = await client.sell_position("mkt_abc123", side="YES", contracts=50.0, idempotency_key=str(uuid.uuid4()))
```

> **Idempotency:** `POST .../trade` and `POST .../sell` accept an optional `Idempotency-Key` header — retrying with the same key and the same request replays the original response (`idempotent_replay=True`) instead of executing twice; the same key with a different request is refused with 409. Pass it per call as `idempotency_key`: the SDK forwards it exactly as given and never generates a key or retries on its own — you own the retry loop. Omit the kwarg and no header is sent; the request is the one the SDK always made. The server bounds the key to 1–255 characters and refuses anything else with 422 (`EdgeValidationError`) — `""` included; the SDK does not check it.

### Data feeds (`client.feeds`)

EDGE exposes seven feed endpoints under `/admin/feeds` — `status`, `sync`, `events`,
`configure`, `manual-event`, `pending` and `import` — and `client.feeds` wraps all seven
(from 5.2.0; on 5.1.0 call them over raw HTTP):

```python
# ADMIN key. `configure` is a partial update: send only what you are changing.
await client.feeds.configure({"feed_adapter": "sportsdataio", "feed_sports": ["NBA"], "sync_mode": "approval"})
sync = await client.feeds.trigger_sync()   # raises EdgeAPIError (400) if no adapter is configured
if sync.stopped:
    print("sync ended early:", sync.stopped)   # a 200 is not "complete"

# Approval mode: staged events wait in `list_pending` until imported (or their event_time passes).
pending = await client.feeds.list_pending(limit=50)
refs = [e["event_ref"] for e in pending["events"]]
if refs:
    await client.feeds.import_pending(refs)
# Also: get_status() (READ_ONLY+), list_events(status=, sport=, limit=, offset=) (any scope),
# create_manual_event(sport=, home_team=, away_team=, event_time=, initial_probability_home=) — NOT idempotent.
```

`get_status`, `trigger_sync` and `import_pending` return models; the other four return the JSON
object as a `dict`. Nothing is validated in the SDK — a bad value is the server's 400/422. **The
feed reference is `docs/API.md`, section *Data feeds — `/admin/feeds/*`*** — parameters, response
shapes, scopes and error codes for all seven; `docs/DATA_FEED_ADAPTER_GUIDE.md` describes the
adapter model behind them.

### Portfolio

```python
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}")

# The 5 headline figures without the positions list
summary = await client.get_portfolio_summary()

# The player's locked Balance Bonus credits (limit 1-500) and whether each is still "pending"
bonus = await client.get_balance_bonus_history(limit=50)
print(f"Locked: ${bonus.total_locked:.2f}, earned: ${bonus.total_earned:.2f}, rows: {bonus.count}")
```

### Admin Operations

```python
# 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")

# Halt, reopen, reschedule (omitted kwarg = unchanged) or void a market
await client.suspend_market("mkt_abc123")
await client.unsuspend_market("mkt_abc123")
await client.reschedule_market("mkt_abc123", event_start_time="2026-09-06T19:00:00Z")
voided = await client.void_market("mkt_abc123")  # voided["total_refunded"] is a decimal STRING

# Revenue breakdown (floats, rounded to cents) and per-player positions on a market
revenue = await client.get_revenue()
page = await client.get_market_positions("mkt_abc123", limit=100)
for pos in page.positions:
    if not pos.is_retail:  # retail players are paid at the cage — never credit their wallet
        ...

# 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 |
| `client.feeds` | Data feeds — `get_status`, `trigger_sync`, `list_events`, `configure`, `create_manual_event`, `list_pending`, `import_pending` (from 5.2.0) |

#### Retail (`client.retail`)

```python
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`)

```python
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`:

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

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

```python
# 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`.

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

```python
# 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!

webhook_id = webhook.webhook.external_id

# List / read back (neither returns the secret)
webhooks = await client.list_webhooks()
one = await client.get_webhook(webhook_id)

# Change only the fields you pass; an omitted kwarg is left unchanged
await client.update_webhook(webhook_id, events=["trade.executed"], description="Trades only")

# Prove the endpoint is reachable: a signed test.ping, and the log it lands in
ping = await client.test_webhook(webhook_id)
print(ping.status, ping.response_status)        # "delivered" 200  /  "failed" 503
for attempt in await client.get_webhook_deliveries(webhook_id, limit=20):  # newest first
    print(attempt.event_type, attempt.attempt_number, attempt.delivered_at, attempt.error_message)

# Rotate the signing secret — the new one is shown ONCE, and the old one stops
# verifying immediately. (403 for the demo operator in production.)
rotated = await client.rotate_webhook_secret(webhook_id)
print(f"New secret: {rotated.secret}")

# Deactivate (soft delete) — the row stays listed with is_active=False;
# update_webhook(webhook_id, is_active=True) reverses it
await client.delete_webhook(webhook_id)
```

### Health Check

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

---

## Error Handling

The SDK raises typed exceptions for different error scenarios:

```python
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:

```python
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** (35 registered — see [docs/API.md](https://github.com/Predigy-LLC/EDGE/blob/main/docs/API.md) for the full list). Every one has a real dispatch site; six names that never did were withdrawn in 5.0.0. 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.sell_only` — The cutoff worker reached `event_start_time` on a market with `accept_in_play_trades=false`: buys refused, existing positions still sellable until `Market.sell_closes_at`. Before 2026-09-04 this transition fired `market.suspended` — if you keyed on that for "buys closed at kickoff", move to this event
- `market.suspended` — An operator halted a market (`/suspend`, `/auto_pause` or the sportsbook mirror); the cutoff worker no longer fires it
- `parlay.created` / `parlay.leg_resolved` — Parlay lifecycle
- `rebate.participant_credited` / `rebate.period_closed` — Balance Bonus
- `market.voided` / `position.refund_applied` — A voided market and its per-position refunds (5.1.0)
- `market.rescheduled` — The operator moved a market's schedule after creation (5.2.0)

---

## Advanced Usage

### Custom HTTP Client

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

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

- **API Documentation:** [docs/API.md](https://github.com/Predigy-LLC/EDGE/blob/main/docs/API.md)
- **Integration Guide:** [docs/EDGE_INTEGRATION_GUIDE.md](https://github.com/Predigy-LLC/EDGE/blob/main/docs/EDGE_INTEGRATION_GUIDE.md)
- **Live API (Swagger):** [edge-production-7b77.up.railway.app/docs](https://edge-production-7b77.up.railway.app/docs)
- **Frontend Demo:** [edge-by-predigy.netlify.app](https://edge-by-predigy.netlify.app)

---

## License

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