Metadata-Version: 2.4
Name: solvex-sdk
Version: 0.1.0
Summary: Python client for the Solvex FunCaptcha solving API.
Project-URL: Homepage, https://solvex.run
Project-URL: Documentation, https://solvex.run/docs
Project-URL: Repository, https://github.com/sexfrance/ARK-FC-REVERSE
Project-URL: Issues, https://github.com/sexfrance/ARK-FC-REVERSE/issues
Author: Solvex
License-Expression: MIT
License-File: LICENSE
Keywords: api,arkose,captcha,funcaptcha,roblox,solver
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Topic :: Internet
Requires-Python: >=3.10
Requires-Dist: httpx<1.0,>=0.26
Description-Content-Type: text/markdown

# solvex-sdk

Python client for the [Solvex](https://solvex.run) FunCaptcha solving API.

## Install

```bash
pip install solvex-sdk
# or, from this repo:
pip install .
```

The distribution is published as `solvex-sdk`; the import name is `solvex`.

Requires Python 3.10+. Depends on `httpx`.

## Quickstart

```python
from solvex import SolvexClient, FunCaptchaTask, Proxy

with SolvexClient("sk_live_...") as sx:
    result = sx.solve(FunCaptchaTask(
        website_url="https://roblox.com",
        website_public_key="476068BF-9607-4799-B53D-966BE98E2B81",
        proxy=Proxy.from_url("http://user:pass@184.82.39.210:8080"),
    ))
    print(result.token)
    print(f"cost=${result.cost_usd}  solved in {result.solve_seconds}s")
```

A proxy is **required** — Solvex does not run proxyless solves.

## Supported site keys

Solvex rejects `websitePublicKey` values it doesn't know, rather than silently
forwarding bad context (wrong `surl`, wrong `location.href`, wrong `referrer`)
that Arkose would quietly suppress a token for. The currently-registered keys:

| Label          | `websitePublicKey`                     |
|----------------|----------------------------------------|
| Roblox Login   | `476068BF-9607-4799-B53D-966BE98E2B81` |
| Roblox Signup  | `A2A14B1D-1AF3-C791-9BBC-EE33CC7A0A6F` |

Passing anything else raises `UnsupportedSiteKeyError` (`errorId: 23`) — contact
support to add the site you need.

## Logged-in sessions (blob + cookies)

When the challenge is tied to a logged-in Roblox session (e.g. authenticated
actions that hand you a `dataExchangeBlob`), pass the blob in `data` **and** the
matching session cookie string in `cookies`. Without the cookie Arkose will
treat the session as mismatched and suppress the token:

```python
result = sx.solve(FunCaptchaTask(
    website_url="https://www.roblox.com",
    website_public_key="476068BF-9607-4799-B53D-966BE98E2B81",
    proxy=Proxy.from_url("http://user:pass@1.2.3.4:8080"),
    data=roblox_data_exchange_blob,
    cookies=".ROBLOSECURITY=...; RBXEventTrackerV2=...;",
))
```

## PoW-flagged sessions

When Arkose returns `pow=true`, the server can solve the PoW challenge and
still hand you a token instead of failing. Opt in with `solve_pow=True` —
it trades 1–3s (C++ fast path) or 15–30s (Node fallback) of latency for a
higher success rate on flagged sessions. Billed as a normal solve.

```python
task = FunCaptchaTask(..., solve_pow=True)
```

## Async

```python
import asyncio
from solvex import AsyncSolvexClient, FunCaptchaTask, Proxy

async def main():
    async with AsyncSolvexClient("sk_live_...") as sx:
        result = await sx.solve(FunCaptchaTask(
            website_url="https://roblox.com",
            website_public_key="476068BF-9607-4799-B53D-966BE98E2B81",
            proxy=Proxy.from_url("socks5://184.82.39.210:1080"),
            use_http3=True,   # None = server default (enabled)
        ))
        print(result.token)

asyncio.run(main())
```

## Low-level

If you want to manage polling yourself:

```python
task_id = sx.create_task(task)
while True:
    r = sx.get_task_result(task_id)
    if r["status"] == "ready":
        print(r["solution"]["token"])
        break
```

## Balance

```python
print(f"${sx.get_balance():.4f} remaining")
```

## Errors

Every non-zero `errorId` raises a typed exception:

| Exception                   | errorId | Meaning                             |
|-----------------------------|---------|-------------------------------------|
| `InvalidKeyError`           | 1       | clientKey missing / bad / revoked   |
| `UnsupportedTaskError`      | 2, 22   | task type not enabled               |
| `UnsupportedSiteKeyError`   | 23      | `websitePublicKey` not registered — contact support |
| `InsufficientCreditsError`  | 10      | account balance too low             |
| `TaskFailedError`           | 12      | solver couldn't produce a token (credits refunded) |
| `TaskNotFoundError`         | 16      | unknown taskId                      |
| `RateLimitedError`          | 31      | per-key or per-user rate limit      |
| `TaskTimeoutError`          | 40      | task exceeded server timeout / client wait |

Catch `SolvexError` to catch everything.

```python
from solvex import SolvexClient, TaskFailedError, RateLimitedError

try:
    result = sx.solve(task, timeout=90)
except TaskFailedError:
    # server already refunded — retry with fresh proxy
    ...
except RateLimitedError:
    time.sleep(5)
```

## Idempotency

Pass `idempotency_key=` to guard against double-billing on retries. Replays
within 24h return the same `taskId` without a second charge.

```python
sx.solve(task, idempotency_key=f"user-42-{run_id}")
```

## HTTP/3

Set `use_http3=False` on the task to force HTTP/2 (useful when debugging
proxies that mangle h3). Default is server-side (h3 enabled).

## License

MIT.
