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 +76 -3
- package/dist/cli.js +98 -4
- package/dist/client.d.ts +127 -1
- package/dist/client.js +130 -5
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +83 -0
- package/dist/fleet.d.ts +7 -1
- package/dist/fleet.js +27 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +3 -1
- package/dist/message.d.ts +29 -0
- package/dist/message.js +24 -0
- package/package.json +1 -1
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
|
|
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`, `
|
|
98
|
-
`get_leaderboard`, `getting_started`).
|
|
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
|
-
/**
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
/**
|
|
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)
|
package/dist/errors.d.ts
ADDED
|
@@ -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<
|
|
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.
|
|
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",
|