Metadata-Version: 2.5
Name: predictefy
Version: 1.0.0b2
Summary: Official Python client + CLI for the Predictefy unified prediction-market API.
Project-URL: Homepage, https://predictefy.com
Author: Predictefy Inc.
License: MIT
License-File: LICENSE
Keywords: api,kalshi,polymarket,predictefy,prediction-markets,sdk
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# predictefy — Python SDK + CLI

The official Python client for **Predictefy's unified prediction-market intelligence
and execution infrastructure**. Integrate once against one normalized contract, then
change the venue client or `venue` parameter to access a different prediction-market
venue.

The Python client covers the normalized data and intelligence surface across all 16
current product venues (Polymarket, Kalshi, Smarkets, Opinion, Myriad, Gemini,
Hyperliquid, Limitless, Polymarket US, Rain, PredictFun, SX Bet, Pascal, XO Market,
PRED, and PredictStreet), plus the cross-venue router, history, market relationships, and Trader
Intelligence. Current execution and client-side signing coverage is documented separately
per SDK and venue.

This Python client exposes Pascal, XO Market, and PRED as read integrations: catalog reads and
order books on all three, plus a public trades tape on Pascal only. None has a hosted account or
venue-history lane. Their execution lanes — like every other venue's — are reachable only through
the separate execution service (`client.exec`, see below), and only where the deployment has
armed them; `client.exec.list_venues()` is the honest answer for any given deployment.

Thin by design: **one runtime dependency (`httpx`)**, Python 3.10+, synchronous. All venue
logic lives server-side; this SDK is a typed wrapper over the hosted REST API.

## Install

> **Release status:** the package is built, but public PyPI publication is held for
> the launch sign-off. The install command below becomes available when that
> owner-controlled release completes; repository users can install the local package.

```bash
pip install predictefy
```

## Quickstart

```python
from predictefy import Predictefy

client = Predictefy(api_key="pk_...")  # or set PREDICTEFY_API_KEY
markets = client.polymarket.fetch_markets({"limit": 5, "query": "fed"})
for m in markets:
    print(m["marketId"], m["title"])
print(f"{len(markets)} of {markets.page['total']} total")
```

The same verbs exist on every venue and are drop-in compatible with familiar exchange-style SDK verbs:

```python
book_response = client.kalshi.fetch_order_book("KXFED-26MAR-T4.00")
book = book_response["data"]
client.exchange("hyperliquid").fetch_trades("BTC-100K")
client.router.fetch_markets({"query": "election", "status": "active"})  # all venues
client.polymarket.fetch_categories()
client.router.fetch_tags({"category": "sports"})
candle_response = client.polymarket.fetch_ohlcv({
    "outcomeId": "123",
    "resolution": "1h",
    "limit": 500,
})
candles = candle_response["data"]
history_provenance = candle_response.get("meta")
client.kalshi.has()                                   # the venue's per-verb capability map
client.kalshi.fetch_event_metadata("kalshi:KXFED-26MAR")  # venue-native metadata (Kalshi only)
```

`fetch_markets_paginated` and `fetch_events_paginated` are the documented aliases of
`fetch_markets` / `fetch_events` (same handler, same page envelope). `has()` returns `True`,
`False`, or `"emulated"` per verb — an emulated book is reconstructed, never a native feed.
`fetch_event_metadata` passes Kalshi's body through under `raw`; every other venue honestly
raises `NotSupportedError`.

**Beta return-shape change:** `fetch_ohlcv` and `fetch_order_book` return the full
`{"success", "data", "meta"?}` response envelope. Before this fix they returned only
`data`, which discarded history provenance and archive coverage/truncation evidence.
For `fetch_order_book`, `data` is one order-book dictionary except for a `since` +
`until` archive range, where it is an ascending list of order-book dictionaries.

List verbs return a `PageList` — an ordinary `list` of dicts with `.page`, `.meta`,
`.next_cursor`, and `.total_count` attached when the endpoint supplies those hints. To
walk an entire catalog, `iterate_markets` follows the cursor for you:

```python
for market in client.polymarket.iterate_markets({"status": "active"}):
    ...  # transparently pages via nextCursor until exhausted
```

## Auth

Pass `api_key`, or set the `PREDICTEFY_API_KEY` environment variable. The key is sent as
`Authorization: Bearer <key>`, is never logged, and is redacted from every error message.

```python
client = Predictefy(
    api_key="pk_...",
    base_url="https://data.predictefy.com",  # the default
    exec_base_url=None,  # opt in to client.exec; execution is a separate service
    retry_on_429=True,  # auto-retry a GET once on 429, honoring Retry-After
)
```

Webhook endpoint management and delivery polling use `client.webhooks`:

```python
endpoint = client.webhooks.create({
    "url": "https://hooks.example.com/predictefy",
    "events": ["execution.status.changed"],
})
endpoints = client.webhooks.list()
page = client.webhooks.deliveries(endpoint["id"], {"after": None, "limit": 50})
client.webhooks.delete(endpoint["id"])
```

The create response is the only response containing the signing secret. Delivery pages are
oldest-to-newest; pass `nextCursor` back as `after`.

## Hosted Account Intelligence

The singular `client.account` namespace reads only the hosted public account lanes. It
never accepts venue credentials and never performs owner-authenticated venue calls:

```python
capabilities = client.account.fetch_capabilities("polymarket")
snapshot = client.account.fetch_snapshot("polymarket", "0xabc")
balances = client.account.fetch_balances("polymarket", "0xabc", limit=20)
positions = client.account.fetch_positions("polymarket", "0xabc", limit=20, cursor=None)
orders = client.account.fetch_open_orders("polymarket", "0xabc", limit=20, cursor=None)
fills = client.account.fetch_fills("polymarket", "0xabc", limit=20, cursor=None)
```

Account lists preserve `.meta` provenance/cache fields and `.next_cursor`. Balances are
not cursor-paginated and expose the exact wire `totalCount` as `.total_count`. Snapshot
resource dictionaries preserve their `totalCount` fields verbatim. Local venue-credential
and owner-authenticated account lanes are intentionally deferred.

On `429 Too Many Requests`, a GET is retried **once**, honoring the `Retry-After` header
(bounded to 30s); if it is still rate-limited, a `RateLimitError` is raised. POSTs (batch
books, execution price, checkout) are never auto-retried.

## Execution — pre-signed artifacts only

Execution lives on a **separate service**, and the reads API deliberately does not proxy it.
Opt in with `exec_base_url`; without it, `client.exec` raises `EXEC_BASE_URL_REQUIRED`. Every
execution route needs an API key with the `trade` scope.

> **This SDK never signs anything.** It never accepts, transports, or derives a private key,
> mnemonic, or seed. `build_*` returns the venue-shaped **unsigned** artifact; you sign it with
> your own wallet or tooling, and `submit_order` carries the resulting signature back. Where a
> lane instead authenticates with your own venue API credentials, those are passed through for
> that single request exactly as the route contract defines, and are never persisted.

```python
from predictefy import Predictefy, PREDICTEFY_EXEC_BASE_URL

client = Predictefy(api_key="pk_...", exec_base_url=PREDICTEFY_EXEC_BASE_URL)

client.exec.list_venues()                      # lanes armed on this deployment

built = client.exec.build_order("polymarket", {
    "outcome": "42", "outcomeSide": "YES",
    "isBuy": True, "price": 0.52, "size": 10,
    "owner": "0x1111111111111111111111111111111111111111",
})
for warning in built.get("warnings", []):      # e.g. a region-blocked hosted relay
    print(warning)

signature = sign_however_you_like(built["unsigned"])   # OUTSIDE this SDK
client.exec.submit_order("polymarket", {
    "executionId": built["executionId"],
    "signature": signature,
    "owner": "0x1111111111111111111111111111111111111111",
})
```

| Method                                                     | Route                                        |
| ---------------------------------------------------------- | -------------------------------------------- |
| `exec.list_venues()`                                        | `GET /v1/exec/venues`                        |
| `exec.build_order(venue, params)`                           | `POST /v1/exec/{venue}/orders/build`         |
| `exec.precheck_order(venue, params)`                        | same route with `dryRun` (persists nothing)  |
| `exec.submit_order(venue, params)`                          | `POST /v1/exec/{venue}/orders/submit`        |
| `exec.fetch_order(venue, execution_id)`                     | `GET /v1/exec/{venue}/orders/{executionId}`  |
| `exec.build_cancel(venue, execution_id, params=None)`       | `POST .../orders/{executionId}/cancel`       |
| `exec.build_modify(venue, execution_id, params)`            | `POST .../orders/{executionId}/modify`       |
| `exec.refresh_order_status(venue, execution_id, params)`    | `POST .../orders/{executionId}/refresh`      |
| `exec.fetch_open_orders / fetch_closed_orders / fetch_all_orders(venue)` | `GET /v1/exec/{venue}/orders`   |
| `exec.fetch_my_trades(venue)`                               | `GET /v1/exec/{venue}/trades`                |
| `exec.fetch_positions(venue)`                               | `GET /v1/exec/{venue}/positions`             |
| `exec.fetch_balance(venue)`                                 | `GET /v1/exec/{venue}/balance`               |

Build, cancel, modify, and submit each require a unique `Idempotency-Key`; the SDK mints one
per call unless you pass `idempotency_key=`. Refresh takes none — it only reads venue state.

`precheck_order` returns `{"ok": True, "result": preview}` when the builder accepts the market,
or `{"ok": False, "refusal": error}` for a typed server refusal; a transport failure still
raises, because no decision arrived. A cancel or modify build returns **its own** new
`executionId` — sign and submit that one, not the target order's. `acked` means the venue
accepted and relayed the artifact, not that it is resting or filled: confirm with `fetch_order`
or `refresh_order_status`. `fetch_positions` discloses a fills-derived fallback as
`result.meta["derivation"] == "fills"`, and `fetch_balance` raises `NotSupportedError` while no
lane exposes a native balance reader.

## Funding & bridges

`client.funding` reads the non-custodial funding registry and returns **unsigned** material for
your own wallet to review, sign, and broadcast. Predictefy never holds funds or keys.

```python
client.funding.get_requirements("polymarket")

steps = client.funding.build_funding_steps("polymarket", {
    "amount": "25", "sourceAsset": "usdce", "exchange": "ctf",
    "recipient": "0x1111111111111111111111111111111111111111",
})
for step in steps["steps"]:                    # sign and broadcast in index order
    print(step["index"], step["description"], step["unsignedTransaction"])

quote = client.funding.get_bridge_quote({
    "fromChain": "42161", "fromToken": "0x...", "fromAmount": "25000000",
    "fromAddress": "0x1111111111111111111111111111111111111111", "toVenue": "hyperliquid",
})
print(quote["meta"]["provenance"])             # which provider answered: glide or lifi
```

| Method                                                     | Route                                          |
| ---------------------------------------------------------- | ---------------------------------------------- |
| `funding.get_requirements(venue)`                           | `GET /v1/funding/{venue}/requirements`         |
| `funding.build_funding_steps(venue, params)`                | `POST /v1/funding/{venue}/steps`               |
| `funding.get_bridge_quote(params)`                          | `GET /v1/bridge/quote`                         |
| `funding.create_bridge_session(params)`                     | `POST /v1/bridge/session`                      |
| `funding.get_bridge_session(session_id)`                    | `GET /v1/bridge/session/{sessionId}`           |
| `funding.update_bridge_session_payment(session_id, params)` | `POST /v1/bridge/session/{sessionId}/payment`  |
| `funding.get_bridge_status(params)`                         | `GET /v1/bridge/status`                        |

The four bridge methods return the **full response envelope**, because `meta.provenance` is the
only record of which provider answered. Quote first, sign and broadcast the session's
`unsignedTransaction` yourself, report the hash with `update_bridge_session_payment`, then poll
`get_bridge_session` with backoff. Bridge status is richer than PENDING/DONE/FAILED — `REFUNDED`
and `PARTIAL` are real outcomes — and there are no completion webhooks, so never assume a
completion time.

`get_bridge_quote` takes **either** `toVenue` **or** an explicit `toChain` + `toToken` pair, never
both. The explicit form is not restricted to Predictefy venues — it forwards to LI.FI unrestricted,
so any supported chain and token is a valid destination, including a chain's native gas token
through the zero-address sentinel:

```python
# Arbitrum USDC -> native POL on Polygon. Verified against production on 2026-08-18.
gas = client.funding.get_bridge_quote({
    "fromChain": "42161", "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
    "fromAmount": "10000000", "fromAddress": "0x1111111111111111111111111111111111111111",
    "toChain": "137", "toToken": "0x0000000000000000000000000000000000000000",
})
```

That matters because bridging leaves you holding a token on a chain where you may have no gas, and
the approval and order that follow need native gas on **both** chains. Quote gas to the destination
first, then bridge the collateral. Cross-VM destinations such as Solana additionally require
`toAddress`, the recipient on the destination chain; without it LI.FI defaults the recipient to the
EVM `fromAddress` and rejects the quote. It is forwarded untouched and not address-validated,
because valid formats differ per VM.

**There is no withdrawal or bridge-out route, in this SDK or in the API.** Each venue is a custody
island: funds leave only by that venue's own rails, and no balance moves between venues through
Predictefy. That is the deliberate no-escrow posture — Predictefy never holds the funds — and
unified funding remains roadmap. Hyperliquid in particular withdraws to **Arbitrum only**, because
the venue's `withdraw3` action is its Arbitrum bridge; reaching another chain means withdrawing to
Arbitrum first and then bridging onward with `get_bridge_quote`.

## Clusters & indicative price discrepancies

```python
clusters = client.fetch_clusters({"limit": 20})
cluster = client.fetch_cluster("cl_abc123")           # one cluster + its per-venue members
gaps = client.fetch_discrepancies(live=True)          # indicative price discrepancies
```

`fetch_discrepancies` returns **indicative price discrepancies** — the cross-venue price
gap between matched markets. These are _indicative_, not executable trades: they do not
account for live executable depth, fees, or resolution edge cases. Use them for signal, not
as a guaranteed profit.

## Errors

Every failure raises a subclass of `PredictefyError`; the server's `code` and `message` are
preserved on the exception.

| Class                      | HTTP    | Meaning                                              |
| -------------------------- | ------- | ---------------------------------------------------- |
| `ValidationError`          | 400     | Bad input / missing required param                   |
| `AuthenticationError`      | 401     | Missing / unknown / revoked key                      |
| `InsufficientCreditsError` | 402     | Balance can't cover the request (`.top_up_hint`)     |
| `ScopeError`               | 403     | Key lacks scope for this route                       |
| `NotFoundError`            | 404     | Unknown exchange/market/event/outcome/cluster        |
| `NotSupportedError`        | 400/501 | Venue/param honestly can't do that (`NOT_SUPPORTED`) |
| `RateLimitError`           | 429     | Rate-limited (raised after the retry is exhausted)   |
| `FeatureDisabledError`     | 503     | A documented route is dark on this deployment        |
| `ServerError`              | 5xx     | Transient platform failure                           |
| `NetworkError`             | —       | Transport failure / unparseable body                 |

```python
from predictefy import Predictefy, InsufficientCreditsError

try:
    client.polymarket.fetch_markets()
except InsufficientCreditsError as err:
    print(err.code, err.top_up_hint)
```

## CLI

The package installs a `predictefy` console command:

```bash
export PREDICTEFY_API_KEY=pk_...

predictefy markets polymarket --limit 10 --q fed
predictefy market kalshi KXFED-26MAR-T4.00
predictefy discrepancies --limit 20 --live
predictefy clusters --limit 20
predictefy account capabilities polymarket
predictefy account snapshot polymarket 0xabc
predictefy account balances polymarket 0xabc --limit 20
predictefy account positions polymarket 0xabc --limit 20 --cursor next-page
```

Add `--json` to any command for raw JSON. `--api-key` and `--base-url` override the
`PREDICTEFY_API_KEY` / `PREDICTEFY_BASE_URL` environment variables.

## License

MIT — see [LICENSE](./LICENSE).
