hunch-cup 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # hunch-cup
2
+
3
+ One-line CLI + typed SDK to play the **Hunch Cup** — a risk-free, **paper-trading**
4
+ prediction-market tournament. Claim **1,000 $pHUNCH** (paper money, zero real-funds
5
+ risk), trade the live Hunch catalog, run a 24/7 agent, and climb a public leaderboard
6
+ scored on realized PnL. The top 50 wallets split a real **$5,000**.
7
+
8
+ > Everything here is **paper**. $pHUNCH never touches a chain and is never redeemable.
9
+ > The only real money is the prize, paid to the top-50 wallet addresses at the end.
10
+
11
+ ## CLI
12
+
13
+ ```bash
14
+ # 1. Generate a wallet and claim 1,000 $pHUNCH (prints a private key — save it!)
15
+ npx hunch-cup wallet new
16
+
17
+ export HUNCH_CUP_PRIVATE_KEY=0x... # the key it printed
18
+
19
+ # 2. Look around
20
+ npx hunch-cup markets
21
+ npx hunch-cup quote <slug> yes 25
22
+
23
+ # 3. Trade (a REAL paper trade is signed automatically by your key)
24
+ npx hunch-cup trade <slug> yes 25
25
+
26
+ # Binary markets take `yes`/`no` (up/down markets accept `yes`/`no` too).
27
+ # Multi-outcome markets (mcap/price ladders, token races, head-to-heads) take an
28
+ # OUTCOME KEY as the side — e.g. a ladder bucket key like `63m-67m`, or a token
29
+ # candidate key like `zec`. Recurring markets trade on their current round.
30
+
31
+ # 4. Or let an agent do it — momentum | contrarian | news-reactive
32
+ npx hunch-cup run momentum 25 # dry-run (simulated)
33
+ npx hunch-cup run contrarian 25 --live # actually place trades
34
+
35
+ npx hunch-cup board
36
+ ```
37
+
38
+ Env: `HUNCH_CUP_PRIVATE_KEY`, `HUNCH_CUP_BASE_URL` (default `https://www.playhunch.xyz`),
39
+ `HUNCH_CUP_SIZE` (default 25), `HUNCH_CUP_SENTIMENT` (for `news-reactive`).
40
+
41
+ ## SDK
42
+
43
+ ```ts
44
+ import { CupClient, fromPrivateKey } from "hunch-cup";
45
+
46
+ const client = new CupClient({ signer: fromPrivateKey(process.env.HUNCH_CUP_PRIVATE_KEY!) });
47
+
48
+ await client.createWallet(); // claim 1,000 $pHUNCH (once)
49
+ const markets = await client.listMarkets();
50
+ await client.simulateTrade(markets[0].slug, "yes", 25); // dry run, no signature
51
+ await client.trade({ slug: markets[0].slug, side: "yes", sizePhunch: 25 }); // signed
52
+ await client.getPositions();
53
+ await client.getLeaderboard({ wallet: client.address });
54
+
55
+ // Built-in strategy templates (pure deciders, you control execution):
56
+ await client.runStrategy({ strategy: "momentum", sizePhunch: 25, live: true });
57
+ ```
58
+
59
+ ## How auth works
60
+
61
+ Reads are public. A **real** paper trade is proven with an **EIP-191 signature** over a
62
+ canonical message binding `{ wallet, market, side, size, tradeId, issuedAt }` — so only a
63
+ wallet's owner can spend its $pHUNCH. The SDK/CLI signs for you. A `simulate` trade writes
64
+ nothing and needs no signature.
65
+
66
+ ## MCP
67
+
68
+ There's also a remote MCP server at `https://www.playhunch.xyz/api/cup/mcp`
69
+ (`create_wallet`, `list_markets`, `get_quote`, `place_paper_trade`, `get_positions`,
70
+ `get_leaderboard`, `getting_started`). `place_paper_trade` defaults to simulate.
71
+
72
+ ## License
73
+
74
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx hunch-cup` — one-line Hunch Cup paper-trading agent (S8, scope §8.2.3).
4
+ *
5
+ * hunch-cup wallet new generate a wallet + claim 1,000 $pHUNCH
6
+ * hunch-cup markets list tradable paper markets
7
+ * hunch-cup quote <slug> <side> <n> price a paper trade
8
+ * hunch-cup trade <slug> <side> <n> place a REAL signed paper trade
9
+ * hunch-cup run <strategy> [n] run momentum|contrarian|news-reactive
10
+ * hunch-cup board show the leaderboard
11
+ *
12
+ * Config via env: HUNCH_CUP_PRIVATE_KEY (0x…), HUNCH_CUP_BASE_URL,
13
+ * HUNCH_CUP_SIZE (default 25). `run` simulates by default; pass --live to trade.
14
+ */
15
+ import { CupClient } from "./client.js";
16
+ import { fromPrivateKey, newWallet } from "./signer.js";
17
+ function baseUrl() {
18
+ return process.env.HUNCH_CUP_BASE_URL;
19
+ }
20
+ function signerFromEnv() {
21
+ const pk = process.env.HUNCH_CUP_PRIVATE_KEY;
22
+ return pk ? fromPrivateKey(pk) : undefined;
23
+ }
24
+ function size(arg) {
25
+ const n = Number(arg ?? process.env.HUNCH_CUP_SIZE ?? 25);
26
+ return Number.isFinite(n) && n > 0 ? n : 25;
27
+ }
28
+ function out(value) {
29
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
30
+ }
31
+ async function main(argv) {
32
+ const [cmd, ...args] = argv;
33
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
34
+ out([
35
+ "hunch-cup — paper-trade the Hunch Cup from your terminal.",
36
+ "",
37
+ " wallet new generate a wallet + claim 1,000 $pHUNCH",
38
+ " markets list tradable paper markets",
39
+ " quote <slug> <side> <n> price a paper trade",
40
+ " trade <slug> <side> <n> place a REAL signed paper trade",
41
+ " run <strategy> [n] [--live] momentum | contrarian | news-reactive",
42
+ " board show the leaderboard",
43
+ "",
44
+ "Env: HUNCH_CUP_PRIVATE_KEY, HUNCH_CUP_BASE_URL, HUNCH_CUP_SIZE",
45
+ ].join("\n"));
46
+ return 0;
47
+ }
48
+ if (cmd === "wallet" && args[0] === "new") {
49
+ let signer = signerFromEnv();
50
+ let privateKey = process.env.HUNCH_CUP_PRIVATE_KEY;
51
+ if (!signer) {
52
+ const w = newWallet();
53
+ signer = w.signer;
54
+ privateKey = w.privateKey;
55
+ out(`Generated a new wallet. SAVE THIS PRIVATE KEY:\n ${privateKey}`);
56
+ out(`export HUNCH_CUP_PRIVATE_KEY=${privateKey}`);
57
+ }
58
+ const client = new CupClient({ baseUrl: baseUrl(), signer });
59
+ out(await client.createWallet());
60
+ return 0;
61
+ }
62
+ if (cmd === "markets") {
63
+ const client = new CupClient({ baseUrl: baseUrl() });
64
+ out(await client.listMarkets(Number(args[0] ?? 25)));
65
+ return 0;
66
+ }
67
+ if (cmd === "quote") {
68
+ const [slug, side, n] = args;
69
+ if (!slug || !side)
70
+ return usage("quote <slug> <side> <size>");
71
+ const client = new CupClient({ baseUrl: baseUrl() });
72
+ out(await client.getQuote(slug, side, size(n)));
73
+ return 0;
74
+ }
75
+ if (cmd === "trade") {
76
+ const [slug, side, n] = args;
77
+ if (!slug || !side)
78
+ return usage("trade <slug> <side> <size>");
79
+ const signer = signerFromEnv();
80
+ if (!signer)
81
+ return usage("set HUNCH_CUP_PRIVATE_KEY to trade");
82
+ const client = new CupClient({ baseUrl: baseUrl(), signer });
83
+ out(await client.trade({ slug, side, sizePhunch: size(n) }));
84
+ return 0;
85
+ }
86
+ if (cmd === "run") {
87
+ const strategy = args[0];
88
+ if (!strategy || !["momentum", "contrarian", "news-reactive"].includes(strategy)) {
89
+ return usage("run <momentum|contrarian|news-reactive> [size] [--live]");
90
+ }
91
+ const live = args.includes("--live");
92
+ const n = args.find((a, i) => i > 0 && !a.startsWith("--"));
93
+ const signer = signerFromEnv();
94
+ if (live && !signer)
95
+ return usage("set HUNCH_CUP_PRIVATE_KEY to run --live");
96
+ const client = new CupClient({ baseUrl: baseUrl(), signer });
97
+ out(await client.runStrategy({
98
+ strategy,
99
+ sizePhunch: size(n),
100
+ sentiment: Number(process.env.HUNCH_CUP_SENTIMENT ?? 0),
101
+ live,
102
+ }));
103
+ return 0;
104
+ }
105
+ if (cmd === "board") {
106
+ const client = new CupClient({ baseUrl: baseUrl() });
107
+ out(await client.getLeaderboard({ limit: Number(args[0] ?? 50) }));
108
+ return 0;
109
+ }
110
+ return usage(`unknown command: ${cmd}`);
111
+ }
112
+ function usage(message) {
113
+ console.error(`hunch-cup: ${message}`);
114
+ return 1;
115
+ }
116
+ main(process.argv.slice(2))
117
+ .then((code) => process.exit(code))
118
+ .catch((err) => {
119
+ console.error(err instanceof Error ? err.message : String(err));
120
+ process.exit(1);
121
+ });
@@ -0,0 +1,84 @@
1
+ import { type CupStrategy } from "./strategy.js";
2
+ export declare const DEFAULT_BASE_URL = "https://www.playhunch.xyz";
3
+ /** Minimal signer the client needs — recover-able EIP-191 `personal_sign`. */
4
+ export interface CupSigner {
5
+ address: string;
6
+ signMessage: (message: string) => Promise<string>;
7
+ }
8
+ export interface CupMarket {
9
+ slug: string;
10
+ marketId: string;
11
+ question: string;
12
+ shortTitle: string;
13
+ tokenSymbol: string;
14
+ direction: boolean;
15
+ deadlineAt: string;
16
+ yesCents: number;
17
+ noCents: number;
18
+ poolPhunch: number;
19
+ tradeCount: number;
20
+ }
21
+ export interface CupTradeReceipt {
22
+ simulated: boolean;
23
+ tradeId?: string;
24
+ slug: string;
25
+ side: string;
26
+ sizePhunch: number;
27
+ shares: number;
28
+ balancePhunch?: number;
29
+ projectedPayoutPhunch?: number;
30
+ }
31
+ export interface CupClientOptions {
32
+ baseUrl?: string;
33
+ /** Required only to claim or place REAL trades; reads work without it. */
34
+ signer?: CupSigner;
35
+ fetchImpl?: typeof fetch;
36
+ }
37
+ export declare class CupClient {
38
+ private readonly baseUrl;
39
+ private readonly signer?;
40
+ private readonly fetchImpl;
41
+ constructor(opts?: CupClientOptions);
42
+ get address(): string | undefined;
43
+ private json;
44
+ private requireSigner;
45
+ /** Claim 1,000 $pHUNCH once for the signer's wallet. */
46
+ createWallet(): Promise<{
47
+ walletAddress: string;
48
+ balancePhunch: number;
49
+ minted: boolean;
50
+ }>;
51
+ listMarkets(limit?: number): Promise<CupMarket[]>;
52
+ getQuote(slug: string, side: string, sizePhunch: number): Promise<{
53
+ shares: number;
54
+ projectedPayoutPhunch: number;
55
+ poolPhunch: number;
56
+ }>;
57
+ /** A dry run — full pricing, no signature, no write. */
58
+ simulateTrade(slug: string, side: string, sizePhunch: number): Promise<CupTradeReceipt>;
59
+ /** Place a REAL signed paper trade. Idempotent by the generated/own tradeId. */
60
+ trade(args: {
61
+ slug: string;
62
+ side: string;
63
+ sizePhunch: number;
64
+ tradeId?: string;
65
+ }): Promise<CupTradeReceipt>;
66
+ getPositions(address?: string): Promise<unknown>;
67
+ getLeaderboard(opts?: {
68
+ limit?: number;
69
+ week?: number;
70
+ wallet?: string;
71
+ }): Promise<unknown>;
72
+ /**
73
+ * One round of a built-in strategy: pick a side per market and place a trade
74
+ * (simulated unless `live`). Returns the receipts. The decision is pure
75
+ * ({@link pickFor}); placement is deterministic code.
76
+ */
77
+ runStrategy(opts: {
78
+ strategy: CupStrategy;
79
+ sizePhunch: number;
80
+ limit?: number;
81
+ sentiment?: number;
82
+ live?: boolean;
83
+ }): Promise<CupTradeReceipt[]>;
84
+ }
package/dist/client.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Hunch Cup — thin typed SDK client over `/api/cup/v1/*` (S8).
3
+ *
4
+ * Wallet-native + keyless: pass a viem `LocalAccount` (or any `CupSigner`) and
5
+ * the client claims your 1,000 $pHUNCH, lists/quotes paper markets, and — for a
6
+ * REAL trade — signs the canonical message for you (the only thing that proves
7
+ * wallet ownership on the paper path). Reads need no signer. Uses global `fetch`
8
+ * (Node ≥ 20). `viem` is an optional peer used only by {@link fromPrivateKey}.
9
+ */
10
+ import { buildCupTradeMessage } from "./message.js";
11
+ import { pickFor } from "./strategy.js";
12
+ export const DEFAULT_BASE_URL = "https://www.playhunch.xyz";
13
+ export class CupClient {
14
+ baseUrl;
15
+ signer;
16
+ fetchImpl;
17
+ constructor(opts = {}) {
18
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
19
+ this.signer = opts.signer;
20
+ this.fetchImpl = opts.fetchImpl ?? fetch;
21
+ }
22
+ get address() {
23
+ return this.signer?.address;
24
+ }
25
+ async json(path, init) {
26
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
27
+ const body = (await res.json().catch(() => null));
28
+ if (!res.ok) {
29
+ const err = (body && body.error) || `http_${res.status}`;
30
+ throw new Error(`Hunch Cup ${path} failed: ${err}`);
31
+ }
32
+ return body;
33
+ }
34
+ requireSigner() {
35
+ if (!this.signer)
36
+ throw new Error("A signer is required for this action (claim/trade).");
37
+ return this.signer;
38
+ }
39
+ /** Claim 1,000 $pHUNCH once for the signer's wallet. */
40
+ async createWallet() {
41
+ const signer = this.requireSigner();
42
+ return this.json("/api/cup/v1/wallet", {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify({ walletAddress: signer.address }),
46
+ });
47
+ }
48
+ async listMarkets(limit = 25) {
49
+ const out = await this.json(`/api/cup/v1/markets?limit=${limit}`);
50
+ return out.markets;
51
+ }
52
+ async getQuote(slug, side, sizePhunch) {
53
+ const qs = new URLSearchParams({ side, sizePhunch: String(sizePhunch) });
54
+ return this.json(`/api/cup/v1/markets/${slug}/quote?${qs}`);
55
+ }
56
+ /** A dry run — full pricing, no signature, no write. */
57
+ async simulateTrade(slug, side, sizePhunch) {
58
+ const out = await this.json("/api/cup/v1/trade", {
59
+ method: "POST",
60
+ headers: { "content-type": "application/json" },
61
+ body: JSON.stringify({
62
+ walletAddress: this.signer?.address ?? "0x" + "0".repeat(40),
63
+ slug,
64
+ side,
65
+ sizePhunch,
66
+ simulate: true,
67
+ }),
68
+ });
69
+ return out.receipt;
70
+ }
71
+ /** Place a REAL signed paper trade. Idempotent by the generated/own tradeId. */
72
+ async trade(args) {
73
+ const signer = this.requireSigner();
74
+ const tradeId = args.tradeId ?? `cup:${args.slug}:${args.side}:${randomId()}`;
75
+ const issuedAt = nowIso();
76
+ const message = buildCupTradeMessage({
77
+ walletAddress: signer.address,
78
+ slug: args.slug,
79
+ side: args.side,
80
+ sizePhunch: args.sizePhunch,
81
+ tradeId,
82
+ issuedAt,
83
+ });
84
+ const signature = await signer.signMessage(message);
85
+ const out = await this.json("/api/cup/v1/trade", {
86
+ method: "POST",
87
+ headers: { "content-type": "application/json" },
88
+ body: JSON.stringify({
89
+ walletAddress: signer.address,
90
+ slug: args.slug,
91
+ side: args.side,
92
+ sizePhunch: args.sizePhunch,
93
+ tradeId,
94
+ issuedAt,
95
+ signature,
96
+ }),
97
+ });
98
+ return out.receipt;
99
+ }
100
+ async getPositions(address) {
101
+ const addr = address ?? this.signer?.address;
102
+ if (!addr)
103
+ throw new Error("No wallet address (pass one or construct with a signer).");
104
+ return this.json(`/api/cup/v1/wallet/${addr}`);
105
+ }
106
+ async getLeaderboard(opts = {}) {
107
+ const qs = new URLSearchParams();
108
+ if (opts.limit)
109
+ qs.set("limit", String(opts.limit));
110
+ if (opts.week)
111
+ qs.set("week", String(opts.week));
112
+ if (opts.wallet)
113
+ qs.set("wallet", opts.wallet);
114
+ const suffix = qs.toString() ? `?${qs}` : "";
115
+ return this.json(`/api/cup/v1/leaderboard${suffix}`);
116
+ }
117
+ /**
118
+ * One round of a built-in strategy: pick a side per market and place a trade
119
+ * (simulated unless `live`). Returns the receipts. The decision is pure
120
+ * ({@link pickFor}); placement is deterministic code.
121
+ */
122
+ async runStrategy(opts) {
123
+ const markets = await this.listMarkets(opts.limit ?? 10);
124
+ const receipts = [];
125
+ for (const m of markets) {
126
+ const card = {
127
+ slug: m.slug,
128
+ yesCents: m.yesCents,
129
+ noCents: m.noCents,
130
+ direction: m.direction,
131
+ };
132
+ const pick = pickFor(opts.strategy, card, opts.sentiment ?? 0);
133
+ receipts.push(opts.live
134
+ ? await this.trade({ slug: pick.slug, side: pick.side, sizePhunch: opts.sizePhunch })
135
+ : await this.simulateTrade(pick.slug, pick.side, opts.sizePhunch));
136
+ }
137
+ return receipts;
138
+ }
139
+ }
140
+ function randomId() {
141
+ // Best-effort unique id; the server idempotency key is the full tradeId.
142
+ return Math.abs(Math.floor(Math.random() * 1e15)).toString(36) + Date.now().toString(36);
143
+ }
144
+ function nowIso() {
145
+ return new Date().toISOString();
146
+ }
@@ -0,0 +1,7 @@
1
+ export { CupClient, DEFAULT_BASE_URL } from "./client.js";
2
+ export type { CupClientOptions, CupSigner, CupMarket, CupTradeReceipt, } from "./client.js";
3
+ export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
4
+ export { buildCupTradeMessage } from "./message.js";
5
+ export type { CupTradeMessageParams } from "./message.js";
6
+ export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
7
+ export type { CupStrategy, StrategyCard, StrategyPick } from "./strategy.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { CupClient, DEFAULT_BASE_URL } from "./client.js";
2
+ export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
3
+ export { buildCupTradeMessage } from "./message.js";
4
+ export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The canonical Hunch Cup paper-trade signing message.
3
+ *
4
+ * MUST stay byte-identical to the server core `src/core/cup/agent-auth.ts`
5
+ * (`buildCupTradeMessage`) — a parity test asserts the two implementations agree.
6
+ * The wallet is lowercased so a checksummed vs. lowercase address signs the same
7
+ * bytes. Pure, dependency-free.
8
+ */
9
+ export interface CupTradeMessageParams {
10
+ walletAddress: string;
11
+ slug: string;
12
+ side: string;
13
+ sizePhunch: number;
14
+ tradeId: string;
15
+ issuedAt: string;
16
+ }
17
+ export declare function buildCupTradeMessage(p: CupTradeMessageParams): string;
@@ -0,0 +1,11 @@
1
+ export function buildCupTradeMessage(p) {
2
+ return [
3
+ "Hunch Cup paper trade",
4
+ `Wallet: ${p.walletAddress.toLowerCase()}`,
5
+ `Market: ${p.slug}`,
6
+ `Side: ${p.side}`,
7
+ `Size: ${p.sizePhunch} pHUNCH`,
8
+ `Trade: ${p.tradeId}`,
9
+ `Issued: ${p.issuedAt}`,
10
+ ].join("\n");
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { LocalAccount } from "viem";
2
+ import type { CupSigner } from "./client.js";
3
+ /** Adapt a viem local account to a {@link CupSigner}. */
4
+ export declare function fromViemAccount(account: LocalAccount): CupSigner;
5
+ /** Build a {@link CupSigner} from a 0x-prefixed private key. */
6
+ export declare function fromPrivateKey(privateKey: string): CupSigner;
7
+ /** Generate a fresh throwaway keypair → `{ privateKey, signer }`. */
8
+ export declare function newWallet(): {
9
+ privateKey: string;
10
+ signer: CupSigner;
11
+ };
package/dist/signer.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * viem signer bridges for the Hunch Cup client. `viem` is a runtime peer used
3
+ * only here, so importing the SDK without trading pulls in nothing heavy.
4
+ */
5
+ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
6
+ /** Adapt a viem local account to a {@link CupSigner}. */
7
+ export function fromViemAccount(account) {
8
+ return {
9
+ address: account.address,
10
+ signMessage: (message) => account.signMessage({ message }),
11
+ };
12
+ }
13
+ /** Build a {@link CupSigner} from a 0x-prefixed private key. */
14
+ export function fromPrivateKey(privateKey) {
15
+ return fromViemAccount(privateKeyToAccount(privateKey));
16
+ }
17
+ /** Generate a fresh throwaway keypair → `{ privateKey, signer }`. */
18
+ export function newWallet() {
19
+ const privateKey = generatePrivateKey();
20
+ return { privateKey, signer: fromPrivateKey(privateKey) };
21
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Hunch Cup — built-in trading strategy templates (S8, scope §8.2.4).
3
+ *
4
+ * Pure, deterministic deciders. The LLM (if any) NEVER executes — a strategy
5
+ * maps a market card to a concrete `{ side }` and order placement is plain code.
6
+ * Direction markets resolve to UP/DOWN; binary markets to YES/NO.
7
+ */
8
+ export interface StrategyCard {
9
+ slug: string;
10
+ /** Implied paper-pool probability of the positive outcome, in cents (0–100). */
11
+ yesCents: number;
12
+ /** Implied paper-pool probability of the negative outcome, in cents (0–100). */
13
+ noCents: number;
14
+ /** True → the positive/negative outcomes are UP/DOWN, not YES/NO. */
15
+ direction: boolean;
16
+ }
17
+ export interface StrategyPick {
18
+ slug: string;
19
+ side: string;
20
+ }
21
+ export type CupStrategy = "momentum" | "contrarian" | "news-reactive";
22
+ /** Momentum: trade WITH the crowd — back the pool's favorite. */
23
+ export declare function momentumPick(card: StrategyCard): StrategyPick;
24
+ /** Contrarian: fade the crowd — back the underdog. */
25
+ export declare function contrarianPick(card: StrategyCard): StrategyPick;
26
+ /**
27
+ * News-reactive: trade off an external sentiment signal in [-1, 1] (e.g. a
28
+ * headline classifier). Positive → back the positive outcome; negative → the
29
+ * negative; a neutral 0 falls back to the pool favorite.
30
+ */
31
+ export declare function newsReactivePick(card: StrategyCard, sentiment: number): StrategyPick;
32
+ /** Dispatch a named strategy. `news-reactive` uses `sentiment` (default 0). */
33
+ export declare function pickFor(strategy: CupStrategy, card: StrategyCard, sentiment?: number): StrategyPick;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Hunch Cup — built-in trading strategy templates (S8, scope §8.2.4).
3
+ *
4
+ * Pure, deterministic deciders. The LLM (if any) NEVER executes — a strategy
5
+ * maps a market card to a concrete `{ side }` and order placement is plain code.
6
+ * Direction markets resolve to UP/DOWN; binary markets to YES/NO.
7
+ */
8
+ /** The outcome label for a pole on this card (UP/DOWN vs YES/NO). */
9
+ function sideFor(card, pole) {
10
+ if (card.direction)
11
+ return pole === "pos" ? "up" : "down";
12
+ return pole === "pos" ? "yes" : "no";
13
+ }
14
+ /** The pool's favorite pole (ties → positive). */
15
+ function favorite(card) {
16
+ return card.yesCents >= card.noCents ? "pos" : "neg";
17
+ }
18
+ /** Momentum: trade WITH the crowd — back the pool's favorite. */
19
+ export function momentumPick(card) {
20
+ return { slug: card.slug, side: sideFor(card, favorite(card)) };
21
+ }
22
+ /** Contrarian: fade the crowd — back the underdog. */
23
+ export function contrarianPick(card) {
24
+ const pole = favorite(card) === "pos" ? "neg" : "pos";
25
+ return { slug: card.slug, side: sideFor(card, pole) };
26
+ }
27
+ /**
28
+ * News-reactive: trade off an external sentiment signal in [-1, 1] (e.g. a
29
+ * headline classifier). Positive → back the positive outcome; negative → the
30
+ * negative; a neutral 0 falls back to the pool favorite.
31
+ */
32
+ export function newsReactivePick(card, sentiment) {
33
+ if (sentiment > 0)
34
+ return { slug: card.slug, side: sideFor(card, "pos") };
35
+ if (sentiment < 0)
36
+ return { slug: card.slug, side: sideFor(card, "neg") };
37
+ return momentumPick(card);
38
+ }
39
+ /** Dispatch a named strategy. `news-reactive` uses `sentiment` (default 0). */
40
+ export function pickFor(strategy, card, sentiment = 0) {
41
+ switch (strategy) {
42
+ case "momentum":
43
+ return momentumPick(card);
44
+ case "contrarian":
45
+ return contrarianPick(card);
46
+ case "news-reactive":
47
+ return newsReactivePick(card, sentiment);
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "hunch-cup",
3
+ "version": "0.1.0",
4
+ "description": "One-line CLI + typed SDK to paper-trade the Hunch Cup tournament: claim 1,000 $pHUNCH, trade live markets with zero real-funds risk, run momentum/contrarian/news-reactive agents, and climb the public leaderboard.",
5
+ "keywords": [
6
+ "hunch",
7
+ "hunch-cup",
8
+ "prediction-markets",
9
+ "paper-trading",
10
+ "agents",
11
+ "mcp",
12
+ "phunch"
13
+ ],
14
+ "homepage": "https://www.playhunch.xyz/cup",
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "bin": {
21
+ "hunch-cup": "./dist/cli.js"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "viem": ">=2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": ">=20"
43
+ },
44
+ "engines": {
45
+ "node": ">=20"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }