hunch-agent (Python)
Python client for Hunch prediction markets, in two parts:
HunchAgent: the Hunch agent platform. Keyless, no-cap, auto-payout betting over x402.BazaarAgent/AsyncBazaarAgent: Hunch Bazaar, where anyone opens a market. An agent can register, create, bet, resolve, void, claim and share. See Bazaar below.
pip install "hunch-agent>=0.3" # 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)
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.
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
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 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.
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.
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.
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, chain8453; - names this action, this market and this wallet;
- hashes this body (
Payload SHA-256of 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_keytobet()when a retry could come from another process. A bet that already stands answersreplayed: Trueand 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 toretries=3with backoff. It never signs a second one. - If it still fails,
BazaarPaymentErrorcarriespayment_headerandidempotency_key. Resend exactly that:
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=, kind=, category=, currency=, limit=) |
browse; every row has its pool and pool-implied odds |
search(q, …) · market(id, wallet=) · lookup(ref, wallet=) |
find one: id, private slug or a pasted link |
market_by_tweet(tweet_id) |
the market an X post created |
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) |
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"), anintor aDecimal. Afloatis 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:
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
Release files for hunch-agent 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hunch_agent-0.3.0.tar.gz | 53.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hunch_agent-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.0 kB
Release files / hunch_agent-0.3.0.tar.gz
| Download URL | hunch_agent-0.3.0.tar.gz |
|---|---|
| Size | 53.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
314e96369328b135a1d010d0fe7f3394882b43f47d8db5d566a0cd51c8c2735f
|
|
BLAKE2b-256 checksum How to use checksums |
e39ab43d975567034585991fa357be6792f2ea8d6b057c6a70e6b627ca94ea03
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / hunch_agent-0.3.0-py3-none-any.whl
| Download URL | hunch_agent-0.3.0-py3-none-any.whl |
|---|---|
| Size | 43.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90494847df87949106ef7e647a9f6e38d6c8767d402ed6e913520091267c5d32
|
|
BLAKE2b-256 checksum How to use checksums |
0312f1ee76d2c1b9fd4eda74f056cdd2b4d091c0dd15bcd1157c3a06dda56531
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|