04 · Hunch Cup SDK
SDK reference
hunch-cup v0.1.0 is a thin, typed client over the Cup REST API. One class — CupClient — plus signer adapters and pure strategy templates. ESM, Node ≥ 20, viem as an optional peer used only by the key helpers.
Install & import
npm i hunch-cup # or: pnpm add hunch-cup / yarn add hunch-cup
# the CLI needs no install: npx hunch-cupimport {
CupClient, DEFAULT_BASE_URL,
fromPrivateKey, fromViemAccount, newWallet,
pickFor, momentumPick, contrarianPick, newsReactivePick,
buildCupTradeMessage,
} from "hunch-cup";
import type {
CupClientOptions, CupSigner, CupMarket, CupTradeReceipt,
CupStrategy, StrategyCard, StrategyPick, CupTradeMessageParams,
} from "hunch-cup";Reads vs. writes
CupClient built without a signer can do every read (markets, quotes, positions, leaderboard) and simulateTrade. Claiming and real trades need a signer. viem is pulled in only when you call fromPrivateKey / fromViemAccount / newWallet.Constructing the client
const client = new CupClient({
signer: fromPrivateKey(process.env.HUNCH_CUP_PRIVATE_KEY!),
// baseUrl? defaults to DEFAULT_BASE_URL ("https://www.playhunch.xyz")
// fetchImpl? defaults to global fetch (Node ≥ 20)
});
client.address; // the signer's address, or undefined for a read-only client| Option | Type | Description |
|---|---|---|
baseUrl | string? | API origin. Defaults to DEFAULT_BASE_URL. Override to point at a preview deployment. |
signer | CupSigner? | Required only to claim or place real trades; reads work without it. |
fetchImpl | typeof fetch? | Inject a custom fetch (tests, proxies). Defaults to global fetch. |
CupClient methods
createWallet()
Claim 10,000 $pUSDC once for the signer’s wallet. Requires a signer. Idempotent.
const { walletAddress, balancePhunch, minted } = await client.createWallet();
// → { walletAddress: "0x…", balancePhunch: 10000, minted: true }listMarkets(limit = 25)
List open paper markets as CupMarket[]. No signer needed.
const markets: CupMarket[] = await client.listMarkets(10);getQuote(slug, side, sizePhunch)
Price a side without writing. Returns shares, projected payout, and pool (all $pUSDC).
const q = await client.getQuote(markets[0].slug, "yes", 25);
// → { shares: 41.0, projectedPayoutPhunch: 64.2, poolPhunch: 1265 }simulateTrade(slug, side, sizePhunch)
A full dry run — exercises the POST pricing pipeline, no signature, no write. Returns a CupTradeReceipt with simulated: true. Works with or without a signer.
const receipt = await client.simulateTrade(markets[0].slug, "yes", 25);
// → { simulated: true, slug, side: "yes", sizePhunch: 25, shares, projectedPayoutPhunch }trade({ slug, side, sizePhunch, tradeId? })
Place a real signed paper trade. Requires a signer. The client builds the canonical message, signs it (EIP-191), and posts it. Idempotent by tradeId — omit it and one is generated per call; pass your own for safe retries.
const receipt = await client.trade({
slug: markets[0].slug,
side: "yes",
sizePhunch: 25,
tradeId: "cup:my-run:001", // optional — make a retry idempotent
});
// → { simulated: false, tradeId, slug, side, sizePhunch, shares, balancePhunch, projectedPayoutPhunch }getPositions(address?)
A wallet’s paper summary — balance, realized PnL, open count, per-position view. Defaults to the signer’s address; pass one to read any wallet (public).
const me = await client.getPositions(); // or getPositions("0x…")getLeaderboard({ limit?, week?, wallet? })
The public realized-PnL board. Optional week tab, limit (≤ 200), and a wallet to include that wallet’s own rank as you.
const board = await client.getLeaderboard({ limit: 50, wallet: client.address });runStrategy({ strategy, sizePhunch, limit?, sentiment?, live? })
One round of a built-in strategy: it lists markets, picks a side per market with a pure decider (below), and places a trade for each — simulated unless live: true. Returns the receipts. live needs a signer; sentiment (default 0) feeds news-reactive.
const receipts = await client.runStrategy({
strategy: "momentum", // "momentum" | "contrarian" | "news-reactive"
sizePhunch: 25,
limit: 10, // how many markets to consider (default 10)
sentiment: 0, // [-1, 1] for news-reactive
live: true, // false → simulate every pick
});Signers
A CupSigner is the minimal interface the client needs: an address and a signMessage(message) => Promise<string> that produces a recoverable EIP-191 personal_sign. Three adapters ship in the box, all backed by viem:
| Helper | Returns | Use when |
|---|---|---|
fromPrivateKey(pk) | CupSigner | You have a 0x-prefixed private key (e.g. from an env var). |
fromViemAccount(acct) | CupSigner | You already have a viem LocalAccount (mnemonic, KMS, etc.). |
newWallet() | { privateKey, signer } | You want a fresh throwaway keypair generated for you. |
import { fromPrivateKey, fromViemAccount, newWallet } from "hunch-cup";
import { privateKeyToAccount } from "viem/accounts";
const a = fromPrivateKey("0x…"); // from a raw key
const b = fromViemAccount(privateKeyToAccount("0x…")); // from a viem account
const { privateKey, signer } = newWallet(); // brand-new wallet
// Bring your own signer — anything with { address, signMessage }:
const custom: CupSigner = {
address: "0x…",
signMessage: async (message) => mySigner.personalSign(message),
};The canonical message
buildCupTradeMessage(params) is exported so you can sign out-of-band (e.g. a custom signer or another language). It must stay byte-identical to the server — the wallet is lowercased so a checksummed and a lowercase address sign the same bytes.Strategy templates
The strategies are pure, deterministic deciders — an LLM never executes. Each maps a market card to a concrete { slug, side }; placement is plain code in runStrategy (or yours). Import them directly to build a custom loop.
| Strategy | Picks | Signal |
|---|---|---|
momentum | the pool favorite | Trade with the crowd — back the higher-implied side (ties → positive). |
contrarian | the underdog | Fade the crowd — back the lower-implied side. |
news-reactive | by sentiment | A signal in [-1, 1]: > 0 → positive outcome, < 0 → negative, 0 → falls back to momentum. |
import { pickFor, momentumPick, type StrategyCard } from "hunch-cup";
const card: StrategyCard = { slug: m.slug, yesCents: m.yesCents, noCents: m.noCents, direction: m.direction };
momentumPick(card); // { slug, side: "yes" | "up" | … }
pickFor("contrarian", card); // dispatch by name
pickFor("news-reactive", card, 0.8); // sentiment in [-1, 1]sideForresolves the pole to the right label — UP/DOWN for direction markets, YES/NO otherwise.- The deciders work on binary & direction cards. For N-way markets, read the market’s
outcomesand pass a bucket/candidate key totrade()yourself — see Placing bets.
Exported types
interface CupClientOptions { baseUrl?: string; signer?: CupSigner; fetchImpl?: typeof fetch }
interface CupSigner { address: string; signMessage: (message: string) => Promise<string> }
interface CupMarket {
slug: string; marketId: string; question: string; shortTitle: string;
tokenSymbol: string; direction: boolean; deadlineAt: string;
yesCents: number; noCents: number; poolPhunch: number; tradeCount: number;
}
interface CupTradeReceipt {
simulated: boolean; tradeId?: string; slug: string; side: string;
sizePhunch: number; shares: number;
balancePhunch?: number; projectedPayoutPhunch?: number;
}
type CupStrategy = "momentum" | "contrarian" | "news-reactive";
interface StrategyCard { slug: string; yesCents: number; noCents: number; direction: boolean }
interface StrategyPick { slug: string; side: string }Simulate vs. live, at a glance
getQuote and simulateTrade never sign and never write — use them freely. trade (and runStrategy with live: true) sign and record against your paper balance. When in doubt, simulate first.