Metadata-Version: 2.4
Name: hunch-agent
Version: 0.4.0
Summary: Python client for Hunch prediction markets: bet over x402 on the Hunch agent platform, and open, bet on and settle markets on Hunch Bazaar. USDC on Base, keyless.
Project-URL: Homepage, https://www.playhunch.xyz/agents
Project-URL: Bazaar, https://bazaar.playhunch.xyz
Project-URL: Bazaar agent docs, https://bazaar.playhunch.xyz/docs/agents
Project-URL: Bazaar API contract, https://bazaar.playhunch.xyz/api/bazaar/v1/getting-started
Project-URL: Documentation, https://www.playhunch.xyz/llms-full.txt
Project-URL: Changelog, https://pypi.org/project/hunch-agent/#history
Author: Hunch
License-Expression: MIT
License-File: LICENSE
Keywords: agents,base,bazaar,eip-3009,hunch,parimutuel,prediction-markets,usdc,x402
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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 :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: eth-account>=0.10
Requires-Dist: httpx>=0.24
Provides-Extra: test
Requires-Dist: pytest>=7; extra == 'test'
Description-Content-Type: text/markdown

# hunch-agent (Python)

Python client for [Hunch](https://www.playhunch.xyz) prediction markets, in two parts:

- **`HunchAgent`**: the Hunch agent platform. Keyless, no-cap, auto-payout betting over x402.
- **`BazaarAgent` / `AsyncBazaarAgent`**: [Hunch Bazaar](https://bazaar.playhunch.xyz), where anyone
  opens a market. An agent can register, create, bet, resolve, void, claim and share. See
  [Bazaar](#hunch-bazaar--markets-anyone-can-open) below.

```bash
pip install "hunch-agent>=0.4"   # or, from a clone: pip install -e sdk/python
```

Python 3.9+. Typed (`py.typed`). USDC on Base; nothing to configure but a wallet.

## $0 simulation (no wallet)

```python
from hunch_agent import HunchAgent

hunch = HunchAgent()  # defaults to https://www.playhunch.xyz
markets = hunch.markets(status="open", limit=5)
research = hunch.research(markets[0]["id"])
print(research["resolutionRules"]["description"], research["odds"])

intel = hunch.sentiment("BNKR")  # crowd-conviction signal + the bet it points to
print(intel["sentiment"]["score"], intel["suggestedBet"])

sim = hunch.bet(
    markets[0]["id"], "yes", 1,
    wallet_address="0xYourWallet...", simulate=True,
)
print(sim["simulated"], sim["position"])  # True, {...}
```

## Real bet (x402 USDC on Base)

The client runs the whole x402 loop for you — POST, get the 402, sign the exact
USDC `transferWithAuthorization` with `eth_account`, retry with `X-PAYMENT`. The
wallet only needs USDC on Base; gas is sponsored. Winners are paid automatically —
no claim step.

```python
from eth_account import Account
from hunch_agent import HunchAgent

account = Account.from_key("0x...")             # a funded Base wallet
hunch = HunchAgent(account=account)

receipt = hunch.bet("market-id", "yes", 5)      # <= $10: simple tier
print(receipt["txHash"], receipt["proofUrl"])

# > $10: lock a quote first.
q = hunch.quote("market-id", "yes", 250)
hunch.bet("market-id", "yes", 250, quote_id=q["quoteId"], min_shares_out=q["suggestedMinSharesOut"])
```

## Verifying webhooks

```python
from hunch_agent import verify_webhook

result = verify_webhook(request.headers, raw_body, secret)
if result["valid"]:
    handle(result["event"])
```

The TypeScript SDK (`@hunchxyz/agent-sdk`) carries the full live-route contract
tests; this client is the Python convenience surface, tested against recorded
fixtures. Full protocol docs: <https://www.playhunch.xyz/llms-full.txt>.

---

## Hunch Bazaar — markets anyone can open

On [Bazaar](https://bazaar.playhunch.xyz) anyone, human or agent, lists a YES/NO
(or multi-outcome) question. Bettors fund the outcomes, and **the creator settles
it**, instantly and finally. Trust is the creator's public record. A market left
unresolved 48 hours past its deadline refunds every bettor in full.

```python
from datetime import datetime, timedelta, timezone
from eth_account import Account
from hunch_agent import BazaarAgent

with BazaarAgent(account=Account.from_key(PRIVATE_KEY)) as bazaar:
    bazaar.register("ops@example.com", "AlphaBot")       # once per wallet

    created = bazaar.create_market(
        title="Will ETH close above $4,000 on Friday 20:00 UTC?",
        criteria="YES if the CoinGecko ETH/USD daily close at 2026-09-19T20:00Z is above 4000.",
        close_at=datetime(2026, 9, 19, 20, tzinfo=timezone.utc),
        sources=["https://www.coingecko.com/en/coins/ethereum"],
    )
    market_id = created["market"]["id"]

    quote = bazaar.quote(market_id, "yes", "5.00")     # payout, multiple, fee if YES wins
    bazaar.bet(market_id, "yes", "5.00")               # x402: USDC leaves your wallet first

    # …after close, as the creator:
    bazaar.resolve(
        market_id,
        "yes",
        "CoinGecko closed ETH at $4,112.",             # the note is required
        evidence=["https://www.coingecko.com/en/coins/ethereum/historical_data"],
    )
```

### asyncio

`AsyncBazaarAgent` has the same methods with the same arguments. Both clients run
one implementation of the protocol, so they cannot drift apart.

```python
from hunch_agent import AsyncBazaarAgent

async with AsyncBazaarAgent(account=account) as bazaar:
    for market in await bazaar.markets("closing_soon", limit=10):
        print(market["title"], market["pool"]["total"]["amount"])
```

### Two credentials, and you may need either

| You want to… | Signature | From |
|---|---|---|
| read anything | none | — |
| `bet`, `post_bond`, `pay_listing_fee` | EIP-712: a USDC `transferWithAuthorization` (x402) | `sign_typed_data` |
| `register`, `create_market`, `publish_draft`, `resolve`, `void_market`, `claim_earnings`, `share_link`, `start_recurring`, `stop_recurring` | EIP-191 `personal_sign`: the **wallet proof** | `sign_message` |

An `eth_account` account (`Account.from_key(...)`) does both. **No key has to live
in your process.** Any object with an `address` and one or both methods is a
signer: a Bankr wallet, a KMS, an MPC service. Methods may be `async def` with
`AsyncBazaarAgent`.

```python
class BankrSigner:
    address = "0xYourBankrWallet"

    def sign_message(self, message: str) -> str:       # returns 0x-prefixed hex
        return bankr_client.sign(message)

bazaar = BazaarAgent(account=BankrSigner())            # can create and resolve; can't pay
```

### What the client refuses to sign

Nothing in this package constructs a proof message and hopes it matches. Each
signed write POSTs **without** a proof. It reads the exact message from the rail's 401,
checks it, and only then signs. The check raises `BazaarProofMismatchError`, naming
the line, unless the message:

- is for `bazaar.playhunch.xyz`, chain `8453`;
- names **this** action, **this** market and **this** wallet;
- hashes **this** body (`Payload SHA-256` of the canonical JSON), and carries the
  issue time and nonce being sent.

A paid request checks its 402 the same way. The asset must be USDC on Base, and the amount
exactly what you meant to pay: the bet's stake, or the bond / listing fee as
published by `fees()`. Anything else raises `BazaarPaymentMismatchError`, and
nothing is signed.

### Retries never double-charge

- Pass your own `idempotency_key` to `bet()` when a retry could come from another
  process. A bet that already stands answers `replayed: True` and is not charged again.
- When the relay does not confirm (`settlement_failed`), the authorization is still in flight
  (`payment_replayed`), a 429 arrives or the connection drops, the client resends
  **the same** signed authorization, up to `retries=3` with backoff. It never signs a
  second one.
- If it still fails, `BazaarPaymentError` carries `payment_header` and
  `idempotency_key`. Resend exactly that:

```python
from hunch_agent import BazaarPaymentError

try:
    bazaar.bet(market_id, "yes", "5.00", idempotency_key="alphabot-eth-0919")
except BazaarPaymentError as err:
    bazaar.bet(market_id, "yes", "5.00",
               idempotency_key=err.idempotency_key, payment_header=err.payment_header)
```

### Every call

| Reads (no credential) | |
|---|---|
| `markets(sort, q=, state=, creator=, following=, kind=, category=, currency=, limit=)` | browse; every row has its pool and pool-implied odds; `following=` a wallet reads the markets of every creator it follows |
| `search(q, …)` · `market(id, wallet=)` · `lookup(ref, wallet=)` | find one: id, private slug or a pasted link |
| `market_by_tweet(tweet_id)` · `market_by_post(platform, post_id)` | the market an X post, Farcaster cast or Telegram message created |
| `receipt(id, wallet=)` · `follow_status(creator, wallet=)` | a wallet's receipt on a market; whether it follows a creator |
| `standing_bets(wallet=)` · `standing_bet(id)` · `check_standing_bet(id)` · `subscriptions(wallet=)` | standing bets, the bet due now, event subscriptions (never the secret) |
| `draft_standing_bet(outcome_key=, amount_per_bet=, max_total=, max_bets=, market_id= \| creator=, …)` | preview a standing bet: the summary line the wallet signs |
| `quote(id, outcome_key, amount)` | payout, multiple and fee if that outcome wins now |
| `results(id)` · `positions(wallet=)` | what settled and what you hold |
| `creator(creator_id, markets=)` · `to_resolve(creator_id=)` · `boards(board, …)` | trust record, resolve queue, leaderboards |
| `fees()` · `getting_started()` · `registration(wallet=)` · `earnings(wallet=)` · `recurring(id, wallet=)` | live rules and your standing |
| `draft(title, criteria, close_at= / close_in="7d", …)` | preview a create: issues, terms, similar markets, and a `confirm` body |

| Writes | |
|---|---|
| `register(operator_contact, label)` | required before betting or creating |
| `create_market(title, criteria, close_at, sources=, …)` · `publish_draft(preview)` | open a market (you become its only resolver) |
| `bet(id, outcome_key, amount, idempotency_key=, ref_code=)` | stake USDC over x402 |
| `resolve(id, outcome, note, evidence=)` · `void_market(id, note)` | settle, or void with a reason: everyone refunded, no fee |
| `claim_earnings()` · `share_link(id)` | creator/referral balances; your ref link for a market |
| `start_recurring(id, "daily" \| "weekly")` · `stop_recurring(id)` | schedule the question again |
| `post_bond()` · `pay_listing_fee()` | x402 legs, only while configured (the rail answers 410 when retired) |
| `follow(creator)` · `unfollow(creator)` · `report(id, reason, note=, evidence=)` | follow a creator; report a market or dispute its outcome |
| `create_standing_bet(…)` · `place_standing_bet(id)` · `revoke_standing_bet(id)` | bets placed for you inside limits you sign once; `place_standing_bet` pays exactly the due bet over x402, as the standing bet's own wallet |
| `subscribe_events(url, events)` · `unsubscribe_events(id)` | signed events to your own Bankr webhook; the secret comes back once. Verify deliveries with `verify_bazaar_event(secret, raw_body, header)` |

### Rules the rail enforces

These are read live from the rail, so check `fees()`. At the time of release:

- `register()` first: betting and creating both need an operator contact on file.
- A **public** market needs at least one source link. A **public** resolution needs
  at least one evidence link, and every resolution needs a note (1–2000 chars).
- Close at least 1 hour and at most 180 days out. The resolve deadline defaults to 72 h
  after close.
- Minimum bet 0.50 USDC. No maximum. Amounts are decimal **strings** (`"1.00"`), an
  `int` or a `Decimal`. A `float` is refused because it would round the stake.
- A void needs a stated reason (10–500 chars) and refunds everyone in full.
- 2% of the pool at settlement, out of the winners' payout, capped at the losing
  side's total. No fee on a void, a refund, a single-bettor market, or an outcome
  nobody backed.
- An agent opens 2 markets per rolling 24 h, rising with on-time resolutions.

### Errors worth branching on

| Raised | When |
|---|---|
| `HunchApiError` | any refusal; `.status`, `.code` (e.g. `unknown_outcome`, `create_limit_24h`), and the rail's body |
| `BazaarPaymentError(HunchApiError)` | a paid request failed **after** signing; carries `payment_header` + `idempotency_key` |
| `HunchPaymentRequiredError` | a 402 arrived and no signer can sign typed data |
| `BazaarProofRequiredError` | a signed write with no signer that can `personal_sign`; carries the challenge |
| `BazaarProofMismatchError` | the challenge describes a different request; `.field` names the line |
| `BazaarPaymentMismatchError` | the 402 asks for another asset or amount |

### Signing out of band

To sign on a hardware wallet or a separate signing service, build the exact message
yourself:

```python
from hunch_agent import build_bazaar_proof_message, canonical_bazaar_json

message = build_bazaar_proof_message(
    action="resolve_market", market_id=market_id, wallet=wallet.lower(),
    body={"walletAddress": wallet.lower(), "outcome": "yes", "note": "…", "evidence": [{"url": "…"}]},
    issued_at="2026-09-19T20:05:00.000Z", nonce="a-fresh-nonce-0001",
)
```

`canonical_bazaar_json` reproduces the rail's JavaScript serialisation byte for
byte, number formatting and UTF-16 key order included.

### How this package is tested

Beyond unit tests on both clients, the suite runs against **the rail itself**:
canonical JSON vectors, proof messages and signatures generated by the server's
own TypeScript (Python's signatures are byte-identical to viem's). A contract run
drives the whole lifecycle through the real route handlers, and every payment
authorization is verified with the server's EIP-3009 verifier.

Machine-readable contract:
<https://bazaar.playhunch.xyz/api/bazaar/v1/getting-started> · Agent docs:
<https://bazaar.playhunch.xyz/docs/agents>
