hunch-cup 0.2.3 → 0.5.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 CHANGED
@@ -11,11 +11,16 @@ scored on realized PnL. The top 50 wallets split a real **$5,000**.
11
11
  ## CLI
12
12
 
13
13
  ```bash
14
- # 1. Generate a wallet and claim 10,000 $pUSDC (prints a private key save it!)
14
+ # 1. Generate a wallet, claim 10,000 $pUSDC, and enroll as a self-operated agent
15
+ # (prints a private key — save it!)
15
16
  npx hunch-cup wallet new
16
17
 
17
18
  export HUNCH_CUP_PRIVATE_KEY=0x... # the key it printed
18
19
 
20
+ # Already have a wallet? Enroll it once so live trading stays open for you
21
+ # even while our own bot fleets are halted. Idempotent.
22
+ npx hunch-cup enroll
23
+
19
24
  # 2. Look around
20
25
  npx hunch-cup markets
21
26
  npx hunch-cup quote <slug> yes 25
@@ -40,6 +45,14 @@ npx hunch-cup run momentum 25 --live --loop --interval 300
40
45
  npx hunch-cup positions # balance + open/realized positions (watch for insolvency)
41
46
  npx hunch-cup balance # just the $pUSDC number
42
47
  npx hunch-cup board
48
+
49
+ # 5. Roulette — a separate paper surface: every spin settles 42 parimutuel
50
+ # markets (colour/dozen/half/parity/column + 37 per-number HITS/MISSES). No house
51
+ # odds — winners split the whole market pool; the edge is betting where the crowd
52
+ # % misprices the wheel's true k/37 %.
53
+ npx hunch-cup roulette # the live round + all 42 markets + betKeys
54
+ npx hunch-cup roulette-quote colour:red 25 # dry-run: crowd % vs true %, est pay-if-win
55
+ npx hunch-cup roulette-bet num:17:yes 25 # a REAL signed bet on the live spin
43
56
  ```
44
57
 
45
58
  ### Run a fleet (one person, many agents)
@@ -71,6 +84,11 @@ await client.trade({ slug: markets[0].slug, side: "yes", sizePhunch: 25 }); // s
71
84
  await client.getWallet(); // balance + positions (typed)
72
85
  await client.getLeaderboard({ wallet: client.address });
73
86
 
87
+ // Roulette (42 parimutuel markets per spin, no house odds):
88
+ const round = await client.getRouletteRound(); // live round + all 42 markets + betKeys
89
+ await client.simulateRouletteBet("colour:red", 25); // dry run, no signature
90
+ await client.rouletteBet({ betKey: "num:17:yes", stakePhunch: 25 }); // signed, live spin
91
+
74
92
  // Built-in strategy templates (pure deciders, you control execution). N-way
75
93
  // markets (ladders, THE FLIP) pick a real outcome key, never phantom yes/no.
76
94
  await client.runStrategy({ strategy: "momentum", sizePhunch: 25, live: true });
@@ -91,11 +109,66 @@ canonical message binding `{ wallet, market, side, size, tradeId, issuedAt }`
91
109
  wallet's owner can spend its $pUSDC. The SDK/CLI signs for you. A `simulate` trade writes
92
110
  nothing and needs no signature.
93
111
 
112
+ ## Errors
113
+
114
+ Every non-2xx response throws a typed `CupApiError` carrying `status`, `code`, the
115
+ server's `hint`, and `retryable`. Anything the server marks `retryable: false` is a
116
+ refusal, not a blip — `runLoop` and `fleetRun` **stop** on one instead of re-firing it
117
+ every interval.
118
+
119
+ ### `cup_agent_trades_halted` (503) — and how enrollment clears it
120
+
121
+ We run house and prize bot fleets of our own. When those have to be stopped, the switch
122
+ is server-side — and nothing in the data separates a fleet wallet from yours (both are
123
+ keyless agent wallets, no login). So the carve-out is something you **do**, not something
124
+ we guess:
125
+
126
+ ```bash
127
+ npx hunch-cup enroll "momentum bot, @me" # one signed call, idempotent
128
+ ```
129
+
130
+ That declares the wallet a **self-operated agent** and reopens live trading for it
131
+ immediately — no ticket, no waiting, while the fleets stay stopped. **You normally never
132
+ have to run it**: `hunch-cup wallet new` enrolls on creation, and a live trade that gets
133
+ refused enrolls once and retries the same `tradeId` (so the retry can't double-spend).
134
+
135
+ ```ts
136
+ const client = new CupClient({ signer }); // autoEnroll: true by default
137
+ await client.trade({ slug, side, sizePhunch: 25 }); // enrolls + retries if halted
138
+
139
+ new CupClient({ signer, autoEnroll: false }); // or handle it yourself:
140
+ try {
141
+ await client.trade({ slug, side, sizePhunch: 25 });
142
+ } catch (err) {
143
+ if (isCupHaltError(err)) { await client.enroll(); /* retry */ }
144
+ else throw err;
145
+ }
146
+ ```
147
+
148
+ Enrollment signs a domain-separated statement, so you can only enroll a wallet you hold
149
+ the key for. House-registered fleet wallets are refused (`403 house_fleet_wallet`).
150
+
151
+ If you're seeing the refusal anyway, reads and dry runs never stop working:
152
+
153
+ ```bash
154
+ hunch-cup run momentum 25 # dry run — same picks, full pricing, no write
155
+ hunch-cup markets # the live paper catalogue
156
+ hunch-cup quote <slug> yes 25 # live pool-implied odds
157
+ hunch-cup board # the leaderboard
158
+ ```
159
+
160
+ Balances and positions are untouched; the halt freezes writes, it doesn't unwind
161
+ anything. `run --live` prints the explanation, shows the round it *would* have traded,
162
+ and exits **75** (`EX_TEMPFAIL`) so a supervisor knows not to restart it hot.
163
+ `GET /api/cup/v1/getting-started` advertises all of this up front (`tradingHalted`), so
164
+ an agent can check before it ever signs.
165
+
94
166
  ## MCP
95
167
 
96
168
  There's also a remote MCP server at `https://www.playhunch.xyz/api/cup/mcp`
97
- (`create_wallet`, `list_markets`, `get_quote`, `place_paper_trade`, `get_positions`,
98
- `get_leaderboard`, `getting_started`). `place_paper_trade` defaults to simulate.
169
+ (`create_wallet`, `list_markets`, `get_quote`, `place_paper_trade`, `get_roulette_round`,
170
+ `place_roulette_bet`, `get_positions`, `get_leaderboard`, `getting_started`).
171
+ `place_paper_trade` and `place_roulette_bet` both default to simulate.
99
172
 
100
173
  ## License
101
174
 
package/dist/cli.js CHANGED
@@ -3,9 +3,13 @@
3
3
  * `npx hunch-cup` — one-line Hunch Cup paper-trading agent (S8, scope §8.2.3).
4
4
  *
5
5
  * hunch-cup wallet new generate a FRESH wallet + claim 10,000 $pUSDC
6
+ * hunch-cup enroll [note] declare this wallet a self-operated agent
6
7
  * hunch-cup markets [n] [offset] list tradable paper markets
7
8
  * hunch-cup quote <slug> <side> <n> price a paper trade
8
9
  * hunch-cup trade <slug> <side> <n> place a REAL signed paper trade
10
+ * hunch-cup roulette show the live roulette round (42 markets)
11
+ * hunch-cup roulette-quote <key> <n> dry-run a roulette bet (live pool quote)
12
+ * hunch-cup roulette-bet <key> <n> place a REAL signed roulette bet
9
13
  * hunch-cup run <strategy> [n] [--live] [--loop] [--interval <sec>]
10
14
  * hunch-cup positions your balance + open/realized positions
11
15
  * hunch-cup balance just your $pUSDC balance
@@ -18,9 +22,17 @@
18
22
  * HUNCH_CUP_SIZE (default 25). `run` simulates by default; pass --live to trade.
19
23
  */
20
24
  import { CupClient } from "./client.js";
25
+ import { CupApiError, isCupHaltError, cupHaltMessage, CUP_HALT_CODE } from "./errors.js";
21
26
  import { fromPrivateKey, newWallet } from "./signer.js";
22
27
  import { fleetNew, fleetRun, fleetBoard } from "./fleet.js";
23
28
  const STRATEGIES = ["momentum", "contrarian", "news-reactive", "random"];
29
+ /**
30
+ * Exit code when the run stopped on a non-retryable server refusal (the agent-
31
+ * write halt) rather than a bug. Distinct from 1 so a supervisor (systemd,
32
+ * Railway, a `while` wrapper) can tell "this will fail identically if you
33
+ * restart me" from "something broke". 75 = sysexits(3) EX_TEMPFAIL.
34
+ */
35
+ const EXIT_HALTED = 75;
24
36
  function baseUrl() {
25
37
  return process.env.HUNCH_CUP_BASE_URL;
26
38
  }
@@ -58,9 +70,13 @@ async function main(argv) {
58
70
  "hunch-cup — paper-trade the Hunch Cup from your terminal.",
59
71
  "",
60
72
  " wallet new generate a FRESH wallet + claim 10,000 $pUSDC",
73
+ " enroll [note] declare this wallet a self-operated agent",
61
74
  " markets [n] [offset] list tradable paper markets",
62
75
  " quote <slug> <side> <n> price a paper trade",
63
76
  " trade <slug> <side> <n> place a REAL signed paper trade",
77
+ " roulette show the live roulette round (42 markets)",
78
+ " roulette-quote <key> <n> dry-run a roulette bet (live pool quote)",
79
+ " roulette-bet <key> <n> place a REAL signed roulette bet",
64
80
  " run <strategy> [n] [--live] [--loop] [--interval <sec>]",
65
81
  " positions your balance + open/realized positions",
66
82
  " balance just your $pUSDC balance",
@@ -71,6 +87,9 @@ async function main(argv) {
71
87
  "",
72
88
  "Strategies: momentum | contrarian | news-reactive",
73
89
  "Env: HUNCH_CUP_PRIVATE_KEY, HUNCH_CUP_BASE_URL, HUNCH_CUP_SIZE",
90
+ "",
91
+ "Exit codes: 0 ok · 1 error · 75 live trading halted server-side (a restart",
92
+ "will fail identically — reads and dry runs still work).",
74
93
  ].join("\n"));
75
94
  return 0;
76
95
  }
@@ -82,6 +101,24 @@ async function main(argv) {
82
101
  out(`export HUNCH_CUP_PRIVATE_KEY=${privateKey}`);
83
102
  const client = new CupClient({ baseUrl: baseUrl(), signer });
84
103
  out(await client.createWallet());
104
+ // Enroll straight away so a brand-new agent is live-tradeable in ONE
105
+ // command even while the house-fleet halt is on. Fail-soft: a dormant or
106
+ // offline enroll must not lose the wallet we just printed the key for.
107
+ try {
108
+ const enrollment = await client.enroll();
109
+ out(enrollment.message);
110
+ }
111
+ catch {
112
+ out("(Could not enroll right now — run `hunch-cup enroll` before trading live.)");
113
+ }
114
+ return 0;
115
+ }
116
+ if (cmd === "enroll") {
117
+ const signer = signerFromEnv();
118
+ if (!signer)
119
+ return usage("set HUNCH_CUP_PRIVATE_KEY to enroll");
120
+ const client = new CupClient({ baseUrl: baseUrl(), signer });
121
+ out(await client.enroll(args.filter((a) => !a.startsWith("--")).join(" ") || undefined));
85
122
  return 0;
86
123
  }
87
124
  if (cmd === "markets") {
@@ -108,6 +145,30 @@ async function main(argv) {
108
145
  out(await client.trade({ slug, side, sizePhunch: size(n) }));
109
146
  return 0;
110
147
  }
148
+ if (cmd === "roulette") {
149
+ const client = new CupClient({ baseUrl: baseUrl(), signer: signerFromEnv() });
150
+ out(await client.getRouletteRound());
151
+ return 0;
152
+ }
153
+ if (cmd === "roulette-quote") {
154
+ const [betKey, n] = args;
155
+ if (!betKey)
156
+ return usage("roulette-quote <betKey> <size> (e.g. colour:red 25)");
157
+ const client = new CupClient({ baseUrl: baseUrl(), signer: signerFromEnv() });
158
+ out(await client.simulateRouletteBet(betKey, size(n)));
159
+ return 0;
160
+ }
161
+ if (cmd === "roulette-bet") {
162
+ const [betKey, n] = args;
163
+ if (!betKey)
164
+ return usage("roulette-bet <betKey> <size> (e.g. num:17:yes 25)");
165
+ const signer = signerFromEnv();
166
+ if (!signer)
167
+ return usage("set HUNCH_CUP_PRIVATE_KEY to bet");
168
+ const client = new CupClient({ baseUrl: baseUrl(), signer });
169
+ out(await client.rouletteBet({ betKey, stakePhunch: size(n) }));
170
+ return 0;
171
+ }
111
172
  if (cmd === "positions" || cmd === "balance") {
112
173
  const signer = signerFromEnv();
113
174
  const addr = args[0] ?? signer?.address;
@@ -133,6 +194,7 @@ async function main(argv) {
133
194
  const sentiment = Number(process.env.HUNCH_CUP_SENTIMENT ?? 0);
134
195
  if (loop) {
135
196
  out(`Looping ${strategy} every ${intervalMs(args) / 1000}s (Ctrl-C to stop)…`);
197
+ let halted = false;
136
198
  await client.runLoop({
137
199
  strategy,
138
200
  sizePhunch: size(n),
@@ -141,10 +203,28 @@ async function main(argv) {
141
203
  live,
142
204
  onRound: (round, receipts) => out(`round ${round}: ${receipts.length} trade(s)`),
143
205
  onError: (round, err) => console.error(`round ${round} error: ${err instanceof Error ? err.message : err}`),
206
+ onHalt: (round, err) => {
207
+ halted = true;
208
+ console.error(`round ${round}: stopped — ${err.code}\n`);
209
+ console.error(err.code === CUP_HALT_CODE ? cupHaltMessage(err) : (err.hint ?? err.message));
210
+ },
144
211
  });
145
- return 0;
212
+ return halted ? EXIT_HALTED : 0;
213
+ }
214
+ try {
215
+ out(await client.runStrategy({ strategy, sizePhunch: size(n), sentiment, live }));
216
+ }
217
+ catch (err) {
218
+ if (!isCupHaltError(err))
219
+ throw err;
220
+ // Don't leave the run empty-handed: the halt only blocks the WRITE, so the
221
+ // same strategy still prices out in full. Show what the round would have
222
+ // done — the agent is demonstrably working, it just can't book anything.
223
+ console.error(`${cupHaltMessage(err)}\n`);
224
+ console.error("Showing what this round WOULD have traded (dry run):\n");
225
+ out(await client.runStrategy({ strategy, sizePhunch: size(n), sentiment, live: false }));
226
+ return EXIT_HALTED;
146
227
  }
147
- out(await client.runStrategy({ strategy, sizePhunch: size(n), sentiment, live }));
148
228
  return 0;
149
229
  }
150
230
  if (cmd === "board") {
@@ -168,14 +248,14 @@ async function main(argv) {
168
248
  if (!STRATEGIES.includes(strategy))
169
249
  return usage("fleet run <strategy> [--interval <sec>]");
170
250
  out(`Running the fleet on ${strategy} every ${intervalMs(args) / 1000}s (Ctrl-C to stop)…`);
171
- await fleetRun({
251
+ const { halted } = await fleetRun({
172
252
  defaultStrategy: strategy,
173
253
  sizePhunch: size(process.env.HUNCH_CUP_SIZE),
174
254
  intervalMs: intervalMs(args),
175
255
  baseUrl: baseUrl(),
176
256
  onEvent: (line) => out(line),
177
257
  });
178
- return 0;
258
+ return halted ? EXIT_HALTED : 0;
179
259
  }
180
260
  if (sub === "board") {
181
261
  out(await fleetBoard(baseUrl()));
@@ -192,6 +272,20 @@ function usage(message) {
192
272
  main(process.argv.slice(2))
193
273
  .then((code) => process.exit(code))
194
274
  .catch((err) => {
275
+ // A bare `cup_agent_trades_halted` reads like a crash. It isn't one — say
276
+ // what happened, what still works, and how to get unblocked.
277
+ if (isCupHaltError(err)) {
278
+ console.error(cupHaltMessage(err));
279
+ process.exit(EXIT_HALTED);
280
+ }
281
+ // Every other API error still gets the server's own hint when it sent one,
282
+ // instead of just the machine code.
283
+ if (err instanceof CupApiError) {
284
+ console.error(`hunch-cup: ${err.message}`);
285
+ if (err.hint)
286
+ console.error(err.hint);
287
+ process.exit(1);
288
+ }
195
289
  console.error(err instanceof Error ? err.message : String(err));
196
290
  process.exit(1);
197
291
  });
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CupApiError } from "./errors.js";
1
2
  import { type CupStrategy } from "./strategy.js";
2
3
  export declare const DEFAULT_BASE_URL = "https://www.playhunch.xyz";
3
4
  /** Minimal signer the client needs — recover-able EIP-191 `personal_sign`. */
@@ -55,16 +56,94 @@ export interface CupTradeReceipt {
55
56
  balancePhunch?: number;
56
57
  projectedPayoutPhunch?: number;
57
58
  }
59
+ /** Receipt from `enroll()` — a wallet declared self-operated for a tournament. */
60
+ export interface CupEnrollment {
61
+ enrolled: boolean;
62
+ walletAddress: string;
63
+ tournamentId: string;
64
+ /** false when this wallet was already enrolled (a repeat call is not an error). */
65
+ created: boolean;
66
+ tradingHalted: boolean;
67
+ message: string;
68
+ }
58
69
  export interface CupClientOptions {
59
70
  baseUrl?: string;
60
71
  /** Required only to claim or place REAL trades; reads work without it. */
61
72
  signer?: CupSigner;
62
73
  fetchImpl?: typeof fetch;
74
+ /**
75
+ * Self-enroll once and retry when a live trade is refused by the house-fleet
76
+ * halt (default true). Set false to see the raw refusal instead.
77
+ */
78
+ autoEnroll?: boolean;
79
+ }
80
+ /** One roulette outcome an agent can back, with the wheel's true odds + crowd pool. */
81
+ export interface RouletteAgentOutcome {
82
+ key: string;
83
+ outcome: string;
84
+ label: string;
85
+ pockets: number;
86
+ truePct: number;
87
+ poolPhunch: number;
88
+ crowdPct: number | null;
89
+ }
90
+ /** One of a spin's 42 parimutuel markets. */
91
+ export interface RouletteAgentMarket {
92
+ id: string;
93
+ question: string;
94
+ short: string;
95
+ poolPhunch: number;
96
+ outcomes: RouletteAgentOutcome[];
97
+ }
98
+ /** The live roulette round as an agent sees it (from `getRouletteRound`). */
99
+ export interface RouletteRoundView {
100
+ roundNo: number;
101
+ phase: "bet" | "spin" | "result";
102
+ betOpenAt: number;
103
+ spinStartAt: number;
104
+ resultAt: number;
105
+ nextRoundAt: number;
106
+ secondsToClose: number;
107
+ pocket: number | null;
108
+ bounds: {
109
+ minBet: number;
110
+ maxBet: number;
111
+ maxRoundTotal: number;
112
+ };
113
+ bettors: number;
114
+ stakedPhunch: number;
115
+ markets: RouletteAgentMarket[];
116
+ history: Array<{
117
+ roundNo: number;
118
+ pocket: number;
119
+ }>;
120
+ }
121
+ /** A roulette bet receipt — a simulate quote or a settled real placement. */
122
+ export interface RouletteBetReceipt {
123
+ simulated: boolean;
124
+ roundNo: number;
125
+ betKey: string;
126
+ stakePhunch: number;
127
+ /** Real bets: balance after the debit. */
128
+ balancePhunch?: number;
129
+ /** Real bets: idempotent replay of an already-placed (wallet, round, key). */
130
+ replay?: boolean;
131
+ /** Simulate: the phase the quote was taken in. */
132
+ phase?: "bet" | "spin" | "result";
133
+ /** Simulate: the whole market pool + crowd/true % + self-diluting estimate. */
134
+ marketPoolPhunch?: number;
135
+ keyStakePhunch?: number;
136
+ crowdPct?: number | null;
137
+ truePct?: number;
138
+ estPayoutIfWinPhunch?: number;
63
139
  }
64
140
  export declare class CupClient {
65
141
  private readonly baseUrl;
66
142
  private readonly signer?;
67
143
  private readonly fetchImpl;
144
+ private readonly autoEnroll;
145
+ /** One self-enroll attempt per client — never loop against a refusing server. */
146
+ private enrollTried;
68
147
  constructor(opts?: CupClientOptions);
69
148
  get address(): string | undefined;
70
149
  private json;
@@ -75,6 +154,14 @@ export declare class CupClient {
75
154
  balancePhunch: number;
76
155
  minted: boolean;
77
156
  }>;
157
+ /**
158
+ * Declare this wallet a SELF-OPERATED agent (one signed call, idempotent).
159
+ *
160
+ * While the house-fleet halt is on, real trades 503 unless the wallet has
161
+ * enrolled. This is the self-serve way past it: no ticket, no waiting, and it
162
+ * works before the halt exists too (enroll once and never think about it).
163
+ */
164
+ enroll(note?: string): Promise<CupEnrollment>;
78
165
  listMarkets(limit?: number, offset?: number): Promise<CupMarket[]>;
79
166
  getQuote(slug: string, side: string, sizePhunch: number): Promise<{
80
167
  shares: number;
@@ -83,13 +170,44 @@ export declare class CupClient {
83
170
  }>;
84
171
  /** A dry run — full pricing, no signature, no write. */
85
172
  simulateTrade(slug: string, side: string, sizePhunch: number): Promise<CupTradeReceipt>;
86
- /** Place a REAL signed paper trade. Idempotent by the generated/own tradeId. */
173
+ /**
174
+ * Place a REAL signed paper trade. Idempotent by the generated/own tradeId.
175
+ *
176
+ * If the write is refused by the house-fleet halt and this wallet has not
177
+ * enrolled yet, enrolls once and retries the SAME tradeId (so the retry can
178
+ * never double-spend). That is the whole point of self-enrollment: a person
179
+ * running their own agent should not have to know the halt exists. Opt out
180
+ * with `autoEnroll: false`.
181
+ */
87
182
  trade(args: {
88
183
  slug: string;
89
184
  side: string;
90
185
  sizePhunch: number;
91
186
  tradeId?: string;
92
187
  }): Promise<CupTradeReceipt>;
188
+ private placeTrade;
189
+ /**
190
+ * Read the live roulette round: phase, seconds-to-close, stake bounds, and all
191
+ * 42 parimutuel markets with their bet keys, the wheel's true k/37 %, and the
192
+ * live crowd pools. Pass `wallet` to include that wallet's bets on the round.
193
+ * Public — no signer needed.
194
+ */
195
+ getRouletteRound(wallet?: string): Promise<RouletteRoundView>;
196
+ /**
197
+ * Dry-run a roulette bet: a live pool quote (crowd % vs true %, plus a
198
+ * self-diluting pay-if-win estimate). No signature, no write.
199
+ */
200
+ simulateRouletteBet(betKey: string, stakePhunch: number, roundNo?: number): Promise<RouletteBetReceipt>;
201
+ /**
202
+ * Place a REAL signed roulette bet on the live spin. Learns the current
203
+ * `roundNo` if you don't pass one, signs the canonical roulette message, and
204
+ * posts it. Idempotent by the natural (wallet, round, betKey) key.
205
+ */
206
+ rouletteBet(args: {
207
+ betKey: string;
208
+ stakePhunch: number;
209
+ roundNo?: number;
210
+ }): Promise<RouletteBetReceipt>;
93
211
  /** Typed wallet summary: balance + realized PnL + positions, all in $pUSDC. */
94
212
  getWallet(address?: string): Promise<CupWalletView>;
95
213
  /** @deprecated use {@link getWallet}. Kept for back-compat. */
@@ -129,6 +247,12 @@ export declare class CupClient {
129
247
  * Run a strategy on a fixed interval, forever (or `rounds` times), catching
130
248
  * per-round errors so a transient failure never kills the agent — the built-in
131
249
  * 24×7 loop the CLI/hosting docs previously punted to a `while true` shell hack.
250
+ *
251
+ * Catching everything is right for a blip and wrong for a refusal: when the
252
+ * server says `retryable: false` (the agent-write halt), re-firing every
253
+ * interval forever is pure waste on both sides — it bills us for the 503s the
254
+ * halt exists to avoid, and it buries the one message the operator needs to
255
+ * read. Such a round reports via `onHalt` (or `onError`) and ENDS the loop.
132
256
  */
133
257
  runLoop(opts: {
134
258
  strategy: CupStrategy;
@@ -143,5 +267,7 @@ export declare class CupClient {
143
267
  rounds?: number;
144
268
  onRound?: (round: number, receipts: CupTradeReceipt[]) => void;
145
269
  onError?: (round: number, err: unknown) => void;
270
+ /** Called instead of `onError` when the run stops on a non-retryable refusal. */
271
+ onHalt?: (round: number, err: CupApiError) => void;
146
272
  }): Promise<void>;
147
273
  }
package/dist/client.js CHANGED
@@ -7,17 +7,22 @@
7
7
  * wallet ownership on the paper path). Reads need no signer. Uses global `fetch`
8
8
  * (Node ≥ 20). `viem` is an optional peer used only by {@link fromPrivateKey}.
9
9
  */
10
- import { buildCupTradeMessage } from "./message.js";
10
+ import { buildCupTradeMessage, buildRouletteBetMessage, buildCupEnrollMessage, } from "./message.js";
11
+ import { CupApiError, isCupHaltError } from "./errors.js";
11
12
  import { pickFor } from "./strategy.js";
12
13
  export const DEFAULT_BASE_URL = "https://www.playhunch.xyz";
13
14
  export class CupClient {
14
15
  baseUrl;
15
16
  signer;
16
17
  fetchImpl;
18
+ autoEnroll;
19
+ /** One self-enroll attempt per client — never loop against a refusing server. */
20
+ enrollTried = false;
17
21
  constructor(opts = {}) {
18
22
  this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
19
23
  this.signer = opts.signer;
20
24
  this.fetchImpl = opts.fetchImpl ?? fetch;
25
+ this.autoEnroll = opts.autoEnroll ?? true;
21
26
  }
22
27
  get address() {
23
28
  return this.signer?.address;
@@ -26,8 +31,12 @@ export class CupClient {
26
31
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
27
32
  const body = (await res.json().catch(() => null));
28
33
  if (!res.ok) {
29
- const err = (body && body.error) || `http_${res.status}`;
30
- throw new Error(`Hunch Cup ${path} failed: ${err}`);
34
+ throw new CupApiError({
35
+ status: res.status,
36
+ code: (body && body.error) || `http_${res.status}`,
37
+ path,
38
+ body: body ?? null,
39
+ });
31
40
  }
32
41
  return body;
33
42
  }
@@ -45,6 +54,28 @@ export class CupClient {
45
54
  body: JSON.stringify({ walletAddress: signer.address }),
46
55
  });
47
56
  }
57
+ /**
58
+ * Declare this wallet a SELF-OPERATED agent (one signed call, idempotent).
59
+ *
60
+ * While the house-fleet halt is on, real trades 503 unless the wallet has
61
+ * enrolled. This is the self-serve way past it: no ticket, no waiting, and it
62
+ * works before the halt exists too (enroll once and never think about it).
63
+ */
64
+ async enroll(note) {
65
+ const signer = this.requireSigner();
66
+ const issuedAt = nowIso();
67
+ const signature = await signer.signMessage(buildCupEnrollMessage({ walletAddress: signer.address, issuedAt }));
68
+ return this.json("/api/cup/v1/agent/enroll", {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({
72
+ walletAddress: signer.address,
73
+ issuedAt,
74
+ signature,
75
+ ...(note ? { note } : {}),
76
+ }),
77
+ });
78
+ }
48
79
  async listMarkets(limit = 25, offset = 0) {
49
80
  const qs = new URLSearchParams({ limit: String(limit) });
50
81
  if (offset > 0)
@@ -71,10 +102,31 @@ export class CupClient {
71
102
  });
72
103
  return out.receipt;
73
104
  }
74
- /** Place a REAL signed paper trade. Idempotent by the generated/own tradeId. */
105
+ /**
106
+ * Place a REAL signed paper trade. Idempotent by the generated/own tradeId.
107
+ *
108
+ * If the write is refused by the house-fleet halt and this wallet has not
109
+ * enrolled yet, enrolls once and retries the SAME tradeId (so the retry can
110
+ * never double-spend). That is the whole point of self-enrollment: a person
111
+ * running their own agent should not have to know the halt exists. Opt out
112
+ * with `autoEnroll: false`.
113
+ */
75
114
  async trade(args) {
76
- const signer = this.requireSigner();
77
115
  const tradeId = args.tradeId ?? `cup:${args.slug}:${args.side}:${randomId()}`;
116
+ try {
117
+ return await this.placeTrade({ ...args, tradeId });
118
+ }
119
+ catch (err) {
120
+ if (!isCupHaltError(err) || !this.autoEnroll || this.enrollTried)
121
+ throw err;
122
+ this.enrollTried = true;
123
+ await this.enroll(); // throws on its own terms; a failed enroll surfaces as-is
124
+ return this.placeTrade({ ...args, tradeId });
125
+ }
126
+ }
127
+ async placeTrade(args) {
128
+ const signer = this.requireSigner();
129
+ const tradeId = args.tradeId;
78
130
  const issuedAt = nowIso();
79
131
  const message = buildCupTradeMessage({
80
132
  walletAddress: signer.address,
@@ -100,6 +152,66 @@ export class CupClient {
100
152
  });
101
153
  return out.receipt;
102
154
  }
155
+ /**
156
+ * Read the live roulette round: phase, seconds-to-close, stake bounds, and all
157
+ * 42 parimutuel markets with their bet keys, the wheel's true k/37 %, and the
158
+ * live crowd pools. Pass `wallet` to include that wallet's bets on the round.
159
+ * Public — no signer needed.
160
+ */
161
+ async getRouletteRound(wallet) {
162
+ const addr = wallet ?? this.signer?.address;
163
+ const qs = addr ? `?wallet=${addr}` : "";
164
+ return this.json(`/api/cup/v1/roulette${qs}`);
165
+ }
166
+ /**
167
+ * Dry-run a roulette bet: a live pool quote (crowd % vs true %, plus a
168
+ * self-diluting pay-if-win estimate). No signature, no write.
169
+ */
170
+ async simulateRouletteBet(betKey, stakePhunch, roundNo) {
171
+ const out = await this.json("/api/cup/v1/roulette/bet", {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json" },
174
+ body: JSON.stringify({
175
+ walletAddress: this.signer?.address ?? "0x" + "0".repeat(40),
176
+ betKey,
177
+ stakePhunch,
178
+ roundNo,
179
+ simulate: true,
180
+ }),
181
+ });
182
+ return out.receipt;
183
+ }
184
+ /**
185
+ * Place a REAL signed roulette bet on the live spin. Learns the current
186
+ * `roundNo` if you don't pass one, signs the canonical roulette message, and
187
+ * posts it. Idempotent by the natural (wallet, round, betKey) key.
188
+ */
189
+ async rouletteBet(args) {
190
+ const signer = this.requireSigner();
191
+ const roundNo = args.roundNo ?? (await this.getRouletteRound()).roundNo;
192
+ const issuedAt = nowIso();
193
+ const message = buildRouletteBetMessage({
194
+ walletAddress: signer.address,
195
+ roundNo,
196
+ betKey: args.betKey,
197
+ sizePhunch: args.stakePhunch,
198
+ issuedAt,
199
+ });
200
+ const signature = await signer.signMessage(message);
201
+ const out = await this.json("/api/cup/v1/roulette/bet", {
202
+ method: "POST",
203
+ headers: { "content-type": "application/json" },
204
+ body: JSON.stringify({
205
+ walletAddress: signer.address,
206
+ roundNo,
207
+ betKey: args.betKey,
208
+ stakePhunch: args.stakePhunch,
209
+ issuedAt,
210
+ signature,
211
+ }),
212
+ });
213
+ return out.receipt;
214
+ }
103
215
  /** Typed wallet summary: balance + realized PnL + positions, all in $pUSDC. */
104
216
  async getWallet(address) {
105
217
  const addr = address ?? this.signer?.address;
@@ -199,6 +311,12 @@ export class CupClient {
199
311
  * Run a strategy on a fixed interval, forever (or `rounds` times), catching
200
312
  * per-round errors so a transient failure never kills the agent — the built-in
201
313
  * 24×7 loop the CLI/hosting docs previously punted to a `while true` shell hack.
314
+ *
315
+ * Catching everything is right for a blip and wrong for a refusal: when the
316
+ * server says `retryable: false` (the agent-write halt), re-firing every
317
+ * interval forever is pure waste on both sides — it bills us for the 503s the
318
+ * halt exists to avoid, and it buries the one message the operator needs to
319
+ * read. Such a round reports via `onHalt` (or `onError`) and ENDS the loop.
202
320
  */
203
321
  async runLoop(opts) {
204
322
  for (let round = 1; !opts.rounds || round <= opts.rounds; round += 1) {
@@ -207,6 +325,13 @@ export class CupClient {
207
325
  opts.onRound?.(round, receipts);
208
326
  }
209
327
  catch (err) {
328
+ if (err instanceof CupApiError && !err.retryable) {
329
+ if (opts.onHalt)
330
+ opts.onHalt(round, err);
331
+ else
332
+ opts.onError?.(round, err);
333
+ return; // permanent refusal — retrying it every interval helps no one
334
+ }
210
335
  opts.onError?.(round, err);
211
336
  }
212
337
  if (opts.rounds && round >= opts.rounds)
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Typed API errors for the Hunch Cup rail.
3
+ *
4
+ * The SDK used to collapse every non-2xx into `new Error("Hunch Cup <path>
5
+ * failed: <code>")`, which meant a caller could not tell a transient blip from
6
+ * a permanent refusal — and the CLI printed the raw code (`cup_agent_trades_
7
+ * halted`) with no explanation, which reads as a crash. {@link CupApiError}
8
+ * keeps the status, the machine code and the server's whole JSON body so both
9
+ * the loop and the CLI can make a real decision.
10
+ */
11
+ /** A non-2xx response from `/api/cup/v1/*`, with the server's code + body. */
12
+ export declare class CupApiError extends Error {
13
+ readonly name = "CupApiError";
14
+ /** HTTP status. */
15
+ readonly status: number;
16
+ /** The server's `error` code (e.g. `market_closed`), or `http_<status>`. */
17
+ readonly code: string;
18
+ /** The request path, for context in logs. */
19
+ readonly path: string;
20
+ /** The full parsed JSON body, when there was one. */
21
+ readonly body: Record<string, unknown> | null;
22
+ constructor(args: {
23
+ status: number;
24
+ code: string;
25
+ path: string;
26
+ body: Record<string, unknown> | null;
27
+ });
28
+ /** The server's human-readable hint, when it sent one. */
29
+ get hint(): string | undefined;
30
+ /**
31
+ * `false` when the server explicitly said not to retry. Unknown/transient
32
+ * failures stay `true` so a network blip never silently kills a long run.
33
+ */
34
+ get retryable(): boolean;
35
+ }
36
+ /** The agent-write halt: paper trading is off for everyone not allow-listed. */
37
+ export declare const CUP_HALT_CODE = "cup_agent_trades_halted";
38
+ /** `true` when this error is the agent-write halt (a permanent 503, not a blip). */
39
+ export declare function isCupHaltError(err: unknown): err is CupApiError;
40
+ /**
41
+ * The message the CLI prints when an agent hits the halt. Written for a human
42
+ * who just ran `--live` and got refused: say it is not their fault, say what
43
+ * still works right now, and say how to get unblocked.
44
+ */
45
+ export declare function cupHaltMessage(err?: CupApiError): string;
package/dist/errors.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Typed API errors for the Hunch Cup rail.
3
+ *
4
+ * The SDK used to collapse every non-2xx into `new Error("Hunch Cup <path>
5
+ * failed: <code>")`, which meant a caller could not tell a transient blip from
6
+ * a permanent refusal — and the CLI printed the raw code (`cup_agent_trades_
7
+ * halted`) with no explanation, which reads as a crash. {@link CupApiError}
8
+ * keeps the status, the machine code and the server's whole JSON body so both
9
+ * the loop and the CLI can make a real decision.
10
+ */
11
+ /** A non-2xx response from `/api/cup/v1/*`, with the server's code + body. */
12
+ export class CupApiError extends Error {
13
+ name = "CupApiError";
14
+ /** HTTP status. */
15
+ status;
16
+ /** The server's `error` code (e.g. `market_closed`), or `http_<status>`. */
17
+ code;
18
+ /** The request path, for context in logs. */
19
+ path;
20
+ /** The full parsed JSON body, when there was one. */
21
+ body;
22
+ constructor(args) {
23
+ super(`Hunch Cup ${args.path} failed: ${args.code}`);
24
+ this.status = args.status;
25
+ this.code = args.code;
26
+ this.path = args.path;
27
+ this.body = args.body;
28
+ }
29
+ /** The server's human-readable hint, when it sent one. */
30
+ get hint() {
31
+ const hint = this.body?.hint;
32
+ return typeof hint === "string" ? hint : undefined;
33
+ }
34
+ /**
35
+ * `false` when the server explicitly said not to retry. Unknown/transient
36
+ * failures stay `true` so a network blip never silently kills a long run.
37
+ */
38
+ get retryable() {
39
+ return this.body?.retryable !== false;
40
+ }
41
+ }
42
+ /** The agent-write halt: paper trading is off for everyone not allow-listed. */
43
+ export const CUP_HALT_CODE = "cup_agent_trades_halted";
44
+ /** `true` when this error is the agent-write halt (a permanent 503, not a blip). */
45
+ export function isCupHaltError(err) {
46
+ return err instanceof CupApiError && err.code === CUP_HALT_CODE;
47
+ }
48
+ /**
49
+ * The message the CLI prints when an agent hits the halt. Written for a human
50
+ * who just ran `--live` and got refused: say it is not their fault, say what
51
+ * still works right now, and say how to get unblocked.
52
+ */
53
+ export function cupHaltMessage(err) {
54
+ const unblock = (typeof err?.body?.unblockUrl === "string" && err.body.unblockUrl) ||
55
+ "https://x.com/playhunchxyz";
56
+ const enroll = (typeof err?.body?.enrollCommand === "string" && err.body.enrollCommand) ||
57
+ "npx hunch-cup enroll";
58
+ return [
59
+ "Hunch Cup: live agent trading is HALTED for the house fleets (503).",
60
+ "",
61
+ "It is not halted for you. Nothing is wrong with your agent, your wallet, or",
62
+ "your signature — the switch exists to stop our own bot fleets, and it can't",
63
+ "tell them apart from you until you say so. One signed call fixes that:",
64
+ "",
65
+ ` ${enroll}`,
66
+ "",
67
+ "That declares this wallet a self-operated agent and reopens live trading for",
68
+ "it immediately — no ticket, no waiting. (The CLI normally does this for you",
69
+ "on the first refusal; you're seeing this because that step didn't complete.)",
70
+ "",
71
+ "Working regardless, enrolled or not:",
72
+ "",
73
+ " hunch-cup run momentum 25 dry run — same picks, full pricing, no write",
74
+ " hunch-cup markets the live paper catalogue",
75
+ " hunch-cup quote <slug> <side> 25 live pool-implied odds",
76
+ " hunch-cup board the leaderboard",
77
+ "",
78
+ "Your balance and positions are untouched — the halt freezes writes, it does",
79
+ "not unwind anything.",
80
+ "",
81
+ `Still stuck? ${unblock} can allow-list your wallet address directly.`,
82
+ ].join("\n");
83
+ }
package/dist/fleet.d.ts CHANGED
@@ -24,6 +24,10 @@ export declare function fleetNew(count: number, baseUrl?: string): Promise<Fleet
24
24
  * its own per-wallet `strategy` if set, else `defaultStrategy`. Rounds are
25
25
  * staggered so N wallets don't all fire on the exact same instant. Never throws —
26
26
  * per-round errors are reported via `onEvent`.
27
+ *
28
+ * Resolves `{ halted: true }` when the run ended on a non-retryable server
29
+ * refusal (the agent-write halt) rather than on its round budget, so a caller
30
+ * can exit with a code that tells a supervisor not to restart it hot.
27
31
  */
28
32
  export declare function fleetRun(opts: {
29
33
  defaultStrategy: CupStrategy;
@@ -33,7 +37,9 @@ export declare function fleetRun(opts: {
33
37
  limit?: number;
34
38
  rounds?: number;
35
39
  onEvent?: (line: string) => void;
36
- }): Promise<void>;
40
+ }): Promise<{
41
+ halted: boolean;
42
+ }>;
37
43
  /**
38
44
  * Coordinated round loop: instead of every wallet running its own perpetual
39
45
  * loop, each ROUND a random subset of `participants()` wallets acts once (via
package/dist/fleet.js CHANGED
@@ -16,6 +16,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
17
  import { join, dirname } from "node:path";
18
18
  import { CupClient } from "./client.js";
19
+ import { CupApiError, isCupHaltError, cupHaltMessage } from "./errors.js";
19
20
  import { fromPrivateKey, newWallet } from "./signer.js";
20
21
  export function fleetFilePath() {
21
22
  return process.env.HUNCH_CUP_FLEET_FILE ?? join(homedir(), ".hunch-cup", "fleet.json");
@@ -73,14 +74,19 @@ export async function fleetNew(count, baseUrl) {
73
74
  * its own per-wallet `strategy` if set, else `defaultStrategy`. Rounds are
74
75
  * staggered so N wallets don't all fire on the exact same instant. Never throws —
75
76
  * per-round errors are reported via `onEvent`.
77
+ *
78
+ * Resolves `{ halted: true }` when the run ended on a non-retryable server
79
+ * refusal (the agent-write halt) rather than on its round budget, so a caller
80
+ * can exit with a code that tells a supervisor not to restart it hot.
76
81
  */
77
82
  export async function fleetRun(opts) {
78
83
  const fleet = loadFleet();
79
84
  if (fleet.wallets.length === 0) {
80
85
  opts.onEvent?.("No fleet wallets. Run `hunch-cup fleet new <n>` first.");
81
- return;
86
+ return { halted: false };
82
87
  }
83
88
  const stagger = Math.floor(opts.intervalMs / Math.max(1, fleet.wallets.length));
89
+ let halted;
84
90
  await Promise.all(fleet.wallets.map(async (w, i) => {
85
91
  await sleep(stagger * i); // spread the first fire across the interval
86
92
  const client = new CupClient({ baseUrl: opts.baseUrl, signer: fromPrivateKey(w.privateKey) });
@@ -94,8 +100,18 @@ export async function fleetRun(opts) {
94
100
  rounds: opts.rounds,
95
101
  onRound: (round, receipts) => opts.onEvent?.(`[${w.name}·${strategy}] round ${round}: ${receipts.length} trade(s)`),
96
102
  onError: (round, err) => opts.onEvent?.(`[${w.name}] round ${round} error: ${err instanceof Error ? err.message : String(err)}`),
103
+ // A halt hits every wallet at once; printing the full explainer N times
104
+ // would bury it. One line each, and the caller prints the explainer.
105
+ onHalt: (round, err) => {
106
+ halted ??= err;
107
+ opts.onEvent?.(`[${w.name}] round ${round} stopped: ${err.code}`);
108
+ },
97
109
  });
98
110
  }));
111
+ if (halted) {
112
+ opts.onEvent?.(`\n${cupHaltMessage(isCupHaltError(halted) ? halted : undefined)}`);
113
+ }
114
+ return { halted: Boolean(halted) };
99
115
  }
100
116
  /**
101
117
  * Coordinated round loop: instead of every wallet running its own perpetual
@@ -111,6 +127,7 @@ export async function fleetRunRounds(opts) {
111
127
  opts.onEvent?.("No fleet wallets. Run `hunch-cup fleet new <n>` first.");
112
128
  return;
113
129
  }
130
+ let halted;
114
131
  for (let round = 1; !opts.rounds || round <= opts.rounds; round += 1) {
115
132
  const n = Math.max(1, Math.min(fleet.wallets.length, Math.floor(opts.participants?.() ?? fleet.wallets.length)));
116
133
  const chosen = shuffle(fleet.wallets).slice(0, n);
@@ -131,9 +148,18 @@ export async function fleetRunRounds(opts) {
131
148
  opts.onEvent?.(`[${w.name}] round ${round}: ${receipts.length} trade(s)`);
132
149
  }
133
150
  catch (err) {
151
+ // A non-retryable refusal (the agent-write halt) applies to the whole
152
+ // roster, not this wallet — keep looping and every remaining round is
153
+ // just paid-for 503s. Record it and stop after the round drains.
154
+ if (err instanceof CupApiError && !err.retryable)
155
+ halted ??= err;
134
156
  opts.onEvent?.(`[${w.name}] round ${round} error: ${err instanceof Error ? err.message : String(err)}`);
135
157
  }
136
158
  }));
159
+ if (halted) {
160
+ opts.onEvent?.(`\n${cupHaltMessage(isCupHaltError(halted) ? halted : undefined)}`);
161
+ return;
162
+ }
137
163
  if (opts.rounds && round >= opts.rounds)
138
164
  break;
139
165
  await sleep(opts.intervalMs);
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export { CupClient, DEFAULT_BASE_URL } from "./client.js";
2
- export type { CupClientOptions, CupSigner, CupMarket, CupOutcome, CupPosition, CupWalletView, CupTradeReceipt, } from "./client.js";
2
+ export type { CupClientOptions, CupSigner, CupMarket, CupOutcome, CupPosition, CupWalletView, CupTradeReceipt, RouletteAgentOutcome, RouletteAgentMarket, RouletteRoundView, RouletteBetReceipt, } from "./client.js";
3
+ export { CupApiError, isCupHaltError, cupHaltMessage, CUP_HALT_CODE } from "./errors.js";
3
4
  export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
4
- export { buildCupTradeMessage } from "./message.js";
5
- export type { CupTradeMessageParams } from "./message.js";
5
+ export { buildCupTradeMessage, buildRouletteBetMessage } from "./message.js";
6
+ export type { CupTradeMessageParams, RouletteBetMessageParams } from "./message.js";
6
7
  export { pickFor, momentumPick, contrarianPick, newsReactivePick, randomPick, } from "./strategy.js";
7
8
  export type { CupStrategy, StrategyCard, StrategyOutcome, StrategyPick, } from "./strategy.js";
8
9
  export { fleetNew, fleetRun, fleetRunRounds, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
9
10
  export type { Fleet, FleetWallet } from "./fleet.js";
11
+ export { buildCupEnrollMessage } from "./message.js";
12
+ export type { CupEnrollMessageParams } from "./message.js";
13
+ export type { CupEnrollment } from "./client.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { CupClient, DEFAULT_BASE_URL } from "./client.js";
2
+ export { CupApiError, isCupHaltError, cupHaltMessage, CUP_HALT_CODE } from "./errors.js";
2
3
  export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
3
- export { buildCupTradeMessage } from "./message.js";
4
+ export { buildCupTradeMessage, buildRouletteBetMessage } from "./message.js";
4
5
  export { pickFor, momentumPick, contrarianPick, newsReactivePick, randomPick, } from "./strategy.js";
5
6
  export { fleetNew, fleetRun, fleetRunRounds, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
7
+ export { buildCupEnrollMessage } from "./message.js";
package/dist/message.d.ts CHANGED
@@ -15,3 +15,32 @@ export interface CupTradeMessageParams {
15
15
  issuedAt: string;
16
16
  }
17
17
  export declare function buildCupTradeMessage(p: CupTradeMessageParams): string;
18
+ /**
19
+ * The canonical Hunch Cup ROULETTE-bet signing message.
20
+ *
21
+ * MUST stay byte-identical to the server core (`src/core/cup/agent-auth.ts`
22
+ * `buildRouletteBetMessage`) — a parity test asserts the two agree. Domain-
23
+ * separated from {@link buildCupTradeMessage} so a trade signature can't be
24
+ * replayed as a roulette bet. `roundNo` binds the bet to one 60s spin; no
25
+ * tradeId — roulette idempotency is the natural (wallet, round, betKey) key.
26
+ */
27
+ export interface RouletteBetMessageParams {
28
+ walletAddress: string;
29
+ roundNo: number;
30
+ betKey: string;
31
+ sizePhunch: number;
32
+ issuedAt: string;
33
+ }
34
+ export declare function buildRouletteBetMessage(p: RouletteBetMessageParams): string;
35
+ export interface CupEnrollMessageParams {
36
+ walletAddress: string;
37
+ /** ISO-8601 timestamp the agent signed at (freshness-checked server-side). */
38
+ issuedAt: string;
39
+ }
40
+ /**
41
+ * The canonical statement a wallet signs to ENROLL as a self-operated agent.
42
+ * Domain-separated from the trade and roulette statements, and byte-identical to
43
+ * `buildCupEnrollMessage` in `src/core/cup/agent-auth.ts` — a drift here means a
44
+ * valid signature that the server refuses.
45
+ */
46
+ export declare function buildCupEnrollMessage(p: CupEnrollMessageParams): string;
package/dist/message.js CHANGED
@@ -9,3 +9,27 @@ export function buildCupTradeMessage(p) {
9
9
  `Issued: ${p.issuedAt}`,
10
10
  ].join("\n");
11
11
  }
12
+ export function buildRouletteBetMessage(p) {
13
+ return [
14
+ "Hunch Cup roulette bet",
15
+ `Wallet: ${p.walletAddress.toLowerCase()}`,
16
+ `Round: ${p.roundNo}`,
17
+ `Bet: ${p.betKey}`,
18
+ `Size: ${p.sizePhunch} pUSDC`,
19
+ `Issued: ${p.issuedAt}`,
20
+ ].join("\n");
21
+ }
22
+ /**
23
+ * The canonical statement a wallet signs to ENROLL as a self-operated agent.
24
+ * Domain-separated from the trade and roulette statements, and byte-identical to
25
+ * `buildCupEnrollMessage` in `src/core/cup/agent-auth.ts` — a drift here means a
26
+ * valid signature that the server refuses.
27
+ */
28
+ export function buildCupEnrollMessage(p) {
29
+ return [
30
+ "Hunch Cup agent enrollment",
31
+ `Wallet: ${p.walletAddress.toLowerCase()}`,
32
+ "I operate this agent myself.",
33
+ `Issued: ${p.issuedAt}`,
34
+ ].join("\n");
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hunch-cup",
3
- "version": "0.2.3",
3
+ "version": "0.5.0",
4
4
  "description": "One-line CLI + typed SDK to paper-trade the Hunch Cup tournament: claim 10,000 $pUSDC, trade live markets with zero real-funds risk, run momentum/contrarian/news-reactive agents, and climb the public leaderboard.",
5
5
  "keywords": [
6
6
  "hunch",