HunchCup
Browse the docs

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

bash
npm i hunch-cup      # or: pnpm add hunch-cup / yarn add hunch-cup
# the CLI needs no install: npx hunch-cup
typescript
import {
  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

A 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

typescript
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
OptionTypeDescription
baseUrlstring?API origin. Defaults to DEFAULT_BASE_URL. Override to point at a preview deployment.
signerCupSigner?Required only to claim or place real trades; reads work without it.
fetchImpltypeof 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.

typescript
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.

typescript
const markets: CupMarket[] = await client.listMarkets(10);

getQuote(slug, side, sizePhunch)

Price a side without writing. Returns shares, projected payout, and pool (all $pUSDC).

typescript
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.

typescript
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.

typescript
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).

typescript
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.

typescript
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.

typescript
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:

HelperReturnsUse when
fromPrivateKey(pk)CupSignerYou have a 0x-prefixed private key (e.g. from an env var).
fromViemAccount(acct)CupSignerYou already have a viem LocalAccount (mnemonic, KMS, etc.).
newWallet(){ privateKey, signer }You want a fresh throwaway keypair generated for you.
typescript
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.

StrategyPicksSignal
momentumthe pool favoriteTrade with the crowd — back the higher-implied side (ties → positive).
contrarianthe underdogFade the crowd — back the lower-implied side.
news-reactiveby sentimentA signal in [-1, 1]: > 0 → positive outcome, < 0 → negative, 0 → falls back to momentum.
typescript
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]
  • sideFor resolves 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 outcomes and pass a bucket/candidate key to trade() yourself — see Placing bets.

Exported types

typescript
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.