nico commited on
Commit
7c735db
Β·
1 Parent(s): 47c2ae6

Initial commit: casino-cli

Browse files
Files changed (6) hide show
  1. .env-example +19 -0
  2. .gitignore +5 -0
  3. README.md +56 -0
  4. package-lock.json +280 -0
  5. package.json +32 -0
  6. src/index.ts +198 -0
.env-example ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Required: your Base-mainnet wallet private key (held by you, used to pay USDC into the casino)
2
+ PRIVATE_KEY=0xYourPrivateKeyHere
3
+
4
+ # Required: your shared-secret bearer token for this casino identity.
5
+ # The server hashes it (sha256) and keys your balance by that hash.
6
+ # Treat it like a password β€” anyone with it can cash out your balance.
7
+ CASINO_TOKEN=my-agent-secret
8
+
9
+ # Required: where the casino is running.
10
+ CASINO_URL=http://localhost:8000
11
+
12
+ # Optional: the OpenRouter (or other OpenAI-compatible) model your agent will play as.
13
+ MODEL=openai/gpt-5-nano
14
+
15
+ # Optional: OpenRouter API key passed to the server so our side can call your model during play.
16
+ OPENROUTER_API_KEY=
17
+
18
+ # Optional: on-chain wallet you want the cashout paid to (mocked in dev, real in prod).
19
+ CASHOUT_WALLET=
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .env
2
+ node_modules/
3
+ bun.lock
4
+ dist/
5
+ *.log
README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # casino-cli
2
+
3
+ Agent client for the AI Poker/Blackjack Casino. Pays USDC on Base mainnet, joins tables, watches hands β€” no Coinbase, no facilitator, just viem + a free public Base RPC.
4
+
5
+ Pattern inspired by [BankrBot's x402-cli-example](https://github.com/BankrBot/x402-cli-example).
6
+
7
+ ## Install
8
+
9
+ **Recommended (bun, ~10Γ— faster startup):**
10
+ ```bash
11
+ curl -fsSL https://bun.sh/install | bash # one-time, if you don't have bun
12
+ bun install
13
+ ```
14
+
15
+ **Fallback (npm + tsx, no install needed if you have node 18+):**
16
+ ```bash
17
+ npm install
18
+ # every command becomes: npx tsx src/index.ts <cmd>
19
+ ```
20
+
21
+ Then:
22
+ ```bash
23
+ cp .env-example .env # or just: bun src/index.ts wallet new (auto-creates .env)
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ bun src/index.ts wallet new # generate fresh burner β†’ auto-saves to .env
30
+ bun src/index.ts balance # check your balance
31
+ bun src/index.ts topup 0.01 # send 1Β’ USDC β†’ credited to your balance
32
+ bun src/index.ts join MyAgent openai/gpt-5-nano
33
+ bun src/index.ts state
34
+ bun src/index.ts cashout 0xYourWallet
35
+ bun src/index.ts watch # tail the table every 3s
36
+ ```
37
+
38
+ (Substitute `npx tsx` for `bun` if you went the fallback route. ~1s slower per command.)
39
+
40
+ ## How it works
41
+
42
+ 1. `topup` asks the casino for payment instructions (`pay_to` address + USDC contract)
43
+ 2. Uses `viem` to sign a regular ERC20 `transfer()` with your `PRIVATE_KEY`
44
+ 3. Waits for 1 confirmation on Base
45
+ 4. POSTs the `tx_hash` back β€” server self-verifies via Base RPC, credits your balance
46
+
47
+ No third-party escrow. No API key to Coinbase. Your private key never leaves this machine. The casino only ever sees the public `tx_hash`.
48
+
49
+ ## Costs
50
+
51
+ - **Gas**: ~$0.0005 per USDC transfer on Base
52
+ - **USDC**: whatever you topup (default $0.01)
53
+
54
+ ## License
55
+
56
+ MIT
package-lock.json ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "casino-cli",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "casino-cli",
9
+ "version": "0.1.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "chalk": "^5.3.0",
13
+ "dotenv": "^16.4.5",
14
+ "viem": "^2.21.25"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.7.4",
18
+ "bun-types": "^1.1.7"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ }
23
+ },
24
+ "node_modules/@adraffy/ens-normalize": {
25
+ "version": "1.11.1",
26
+ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
27
+ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
28
+ "license": "MIT"
29
+ },
30
+ "node_modules/@noble/ciphers": {
31
+ "version": "1.3.0",
32
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
33
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": "^14.21.3 || >=16"
37
+ },
38
+ "funding": {
39
+ "url": "https://paulmillr.com/funding/"
40
+ }
41
+ },
42
+ "node_modules/@noble/curves": {
43
+ "version": "1.9.1",
44
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
45
+ "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@noble/hashes": "1.8.0"
49
+ },
50
+ "engines": {
51
+ "node": "^14.21.3 || >=16"
52
+ },
53
+ "funding": {
54
+ "url": "https://paulmillr.com/funding/"
55
+ }
56
+ },
57
+ "node_modules/@noble/hashes": {
58
+ "version": "1.8.0",
59
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
60
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
61
+ "license": "MIT",
62
+ "engines": {
63
+ "node": "^14.21.3 || >=16"
64
+ },
65
+ "funding": {
66
+ "url": "https://paulmillr.com/funding/"
67
+ }
68
+ },
69
+ "node_modules/@scure/base": {
70
+ "version": "1.2.6",
71
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
72
+ "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
73
+ "license": "MIT",
74
+ "funding": {
75
+ "url": "https://paulmillr.com/funding/"
76
+ }
77
+ },
78
+ "node_modules/@scure/bip32": {
79
+ "version": "1.7.0",
80
+ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
81
+ "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
82
+ "license": "MIT",
83
+ "dependencies": {
84
+ "@noble/curves": "~1.9.0",
85
+ "@noble/hashes": "~1.8.0",
86
+ "@scure/base": "~1.2.5"
87
+ },
88
+ "funding": {
89
+ "url": "https://paulmillr.com/funding/"
90
+ }
91
+ },
92
+ "node_modules/@scure/bip39": {
93
+ "version": "1.6.0",
94
+ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
95
+ "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
96
+ "license": "MIT",
97
+ "dependencies": {
98
+ "@noble/hashes": "~1.8.0",
99
+ "@scure/base": "~1.2.5"
100
+ },
101
+ "funding": {
102
+ "url": "https://paulmillr.com/funding/"
103
+ }
104
+ },
105
+ "node_modules/@types/node": {
106
+ "version": "22.19.17",
107
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
108
+ "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
109
+ "dev": true,
110
+ "license": "MIT",
111
+ "dependencies": {
112
+ "undici-types": "~6.21.0"
113
+ }
114
+ },
115
+ "node_modules/abitype": {
116
+ "version": "1.2.3",
117
+ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
118
+ "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
119
+ "license": "MIT",
120
+ "funding": {
121
+ "url": "https://github.com/sponsors/wevm"
122
+ },
123
+ "peerDependencies": {
124
+ "typescript": ">=5.0.4",
125
+ "zod": "^3.22.0 || ^4.0.0"
126
+ },
127
+ "peerDependenciesMeta": {
128
+ "typescript": {
129
+ "optional": true
130
+ },
131
+ "zod": {
132
+ "optional": true
133
+ }
134
+ }
135
+ },
136
+ "node_modules/bun-types": {
137
+ "version": "1.3.13",
138
+ "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.13.tgz",
139
+ "integrity": "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==",
140
+ "dev": true,
141
+ "license": "MIT",
142
+ "dependencies": {
143
+ "@types/node": "*"
144
+ }
145
+ },
146
+ "node_modules/chalk": {
147
+ "version": "5.6.2",
148
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
149
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
150
+ "license": "MIT",
151
+ "engines": {
152
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
153
+ },
154
+ "funding": {
155
+ "url": "https://github.com/chalk/chalk?sponsor=1"
156
+ }
157
+ },
158
+ "node_modules/dotenv": {
159
+ "version": "16.6.1",
160
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
161
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
162
+ "license": "BSD-2-Clause",
163
+ "engines": {
164
+ "node": ">=12"
165
+ },
166
+ "funding": {
167
+ "url": "https://dotenvx.com"
168
+ }
169
+ },
170
+ "node_modules/eventemitter3": {
171
+ "version": "5.0.1",
172
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
173
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
174
+ "license": "MIT"
175
+ },
176
+ "node_modules/isows": {
177
+ "version": "1.0.7",
178
+ "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
179
+ "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
180
+ "funding": [
181
+ {
182
+ "type": "github",
183
+ "url": "https://github.com/sponsors/wevm"
184
+ }
185
+ ],
186
+ "license": "MIT",
187
+ "peerDependencies": {
188
+ "ws": "*"
189
+ }
190
+ },
191
+ "node_modules/ox": {
192
+ "version": "0.14.17",
193
+ "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.17.tgz",
194
+ "integrity": "sha512-jOzNb2Wlfzsr8z/GoCtd1bf6OSRuWuysvbhnHGD+7fV1WRbcBR6B0RYoe3xWnUedF7zp4l5APmS7CzAhUok/lA==",
195
+ "funding": [
196
+ {
197
+ "type": "github",
198
+ "url": "https://github.com/sponsors/wevm"
199
+ }
200
+ ],
201
+ "license": "MIT",
202
+ "dependencies": {
203
+ "@adraffy/ens-normalize": "^1.11.0",
204
+ "@noble/ciphers": "^1.3.0",
205
+ "@noble/curves": "1.9.1",
206
+ "@noble/hashes": "^1.8.0",
207
+ "@scure/bip32": "^1.7.0",
208
+ "@scure/bip39": "^1.6.0",
209
+ "abitype": "^1.2.3",
210
+ "eventemitter3": "5.0.1"
211
+ },
212
+ "peerDependencies": {
213
+ "typescript": ">=5.4.0"
214
+ },
215
+ "peerDependenciesMeta": {
216
+ "typescript": {
217
+ "optional": true
218
+ }
219
+ }
220
+ },
221
+ "node_modules/undici-types": {
222
+ "version": "6.21.0",
223
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
224
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
225
+ "dev": true,
226
+ "license": "MIT"
227
+ },
228
+ "node_modules/viem": {
229
+ "version": "2.48.1",
230
+ "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.1.tgz",
231
+ "integrity": "sha512-GJC3gKV1Hngeo1IB9YanJKHH2pcmoqDymyPxddmzDtG8boXA7eFw8qdnn1PSaToJ93f3LpOZPlLLJ9beAF/Lzg==",
232
+ "funding": [
233
+ {
234
+ "type": "github",
235
+ "url": "https://github.com/sponsors/wevm"
236
+ }
237
+ ],
238
+ "license": "MIT",
239
+ "dependencies": {
240
+ "@noble/curves": "1.9.1",
241
+ "@noble/hashes": "1.8.0",
242
+ "@scure/bip32": "1.7.0",
243
+ "@scure/bip39": "1.6.0",
244
+ "abitype": "1.2.3",
245
+ "isows": "1.0.7",
246
+ "ox": "0.14.17",
247
+ "ws": "8.18.3"
248
+ },
249
+ "peerDependencies": {
250
+ "typescript": ">=5.0.4"
251
+ },
252
+ "peerDependenciesMeta": {
253
+ "typescript": {
254
+ "optional": true
255
+ }
256
+ }
257
+ },
258
+ "node_modules/ws": {
259
+ "version": "8.18.3",
260
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
261
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
262
+ "license": "MIT",
263
+ "engines": {
264
+ "node": ">=10.0.0"
265
+ },
266
+ "peerDependencies": {
267
+ "bufferutil": "^4.0.1",
268
+ "utf-8-validate": ">=5.0.2"
269
+ },
270
+ "peerDependenciesMeta": {
271
+ "bufferutil": {
272
+ "optional": true
273
+ },
274
+ "utf-8-validate": {
275
+ "optional": true
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "casino-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI agent client for the AI Poker/Blackjack Casino. Pays USDC on Base mainnet, joins tables, watches hands.",
5
+ "main": "src/index.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "bun src/index.ts",
9
+ "balance": "bun src/index.ts balance",
10
+ "topup": "bun src/index.ts topup",
11
+ "join": "bun src/index.ts join",
12
+ "state": "bun src/index.ts state",
13
+ "cashout": "bun src/index.ts cashout",
14
+ "watch": "bun src/index.ts watch",
15
+ "wallet:new": "bun src/index.ts wallet new",
16
+ "tsx": "npx tsx src/index.ts"
17
+ },
18
+ "dependencies": {
19
+ "viem": "^2.21.25",
20
+ "chalk": "^5.3.0",
21
+ "dotenv": "^16.4.5"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.7.4",
25
+ "bun-types": "^1.1.7"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "keywords": ["casino", "poker", "blackjack", "x402", "usdc", "base", "ai-agents"],
31
+ "license": "MIT"
32
+ }
src/index.ts ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * casino-cli β€” agent client for the AI Poker/Blackjack Casino.
4
+ *
5
+ * Uses viem to send USDC on Base mainnet (no Coinbase, no facilitator),
6
+ * then posts the tx_hash to the casino's /api/twitch/topup for credit.
7
+ *
8
+ * Commands:
9
+ * bun start balance
10
+ * bun start topup [usdc] (default 0.01)
11
+ * bun start join <name> <model>
12
+ * bun start state
13
+ * bun start cashout [wallet]
14
+ * bun start watch (poll state every 3s)
15
+ */
16
+ import { config } from "dotenv";
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import * as crypto from "crypto";
20
+ import chalk from "chalk";
21
+ import { createWalletClient, createPublicClient, http, erc20Abi, parseUnits, type Hex } from "viem";
22
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
23
+ import { base } from "viem/chains";
24
+
25
+ config();
26
+
27
+ const TOKEN = process.env.CASINO_TOKEN || ""; // required for server calls; validated lazily
28
+ const CASINO = (process.env.CASINO_URL || "http://localhost:8000").replace(/\/$/, "");
29
+ const MODEL = process.env.MODEL || "openai/gpt-5-nano";
30
+ const APIKEY = process.env.OPENROUTER_API_KEY || "";
31
+ const CASHOUT_WALLET = process.env.CASHOUT_WALLET || "";
32
+
33
+ function mustEnv(name: string): string {
34
+ const v = process.env[name];
35
+ if (!v) { console.error(chalk.red(`Missing ${name} in .env`)); process.exit(1); }
36
+ return v;
37
+ }
38
+
39
+ function requireToken() { if (!TOKEN) { console.error(chalk.red("Missing CASINO_TOKEN in .env")); process.exit(1); } }
40
+
41
+ async function api<T = any>(path: string, body?: any, method: "GET" | "POST" = body ? "POST" : "GET"): Promise<T> {
42
+ const url = CASINO + path + (method === "GET" && body ? "?" + new URLSearchParams(body).toString() : "");
43
+ const init: RequestInit = { method, headers: { "Content-Type": "application/json" } };
44
+ if (method !== "GET" && body) init.body = JSON.stringify(body);
45
+ const r = await fetch(url, init);
46
+ const text = await r.text();
47
+ try { return JSON.parse(text) as T; } catch { throw new Error(`Non-JSON ${r.status}: ${text.slice(0, 200)}`); }
48
+ }
49
+
50
+ async function cmdBalance() {
51
+ requireToken();
52
+ const r: any = await api("/api/twitch/balance", { token: TOKEN }, "GET");
53
+ console.log(chalk.cyan(JSON.stringify(r, null, 2)));
54
+ }
55
+
56
+ async function cmdTopup(usdc = 0.01) {
57
+ requireToken();
58
+ // Step 1 β€” get payment instructions
59
+ const quote: any = await api("/api/twitch/topup", { token: TOKEN, usdc });
60
+ const inst = quote.instructions;
61
+ if (!inst) { console.error(chalk.red("No instructions in 402:"), quote); return; }
62
+ console.log(chalk.yellow(`β†’ Pay ${usdc} USDC to ${inst.pay_to} on ${inst.network}`));
63
+
64
+ // Step 2 β€” send USDC ERC20 transfer
65
+ const pk = mustEnv("PRIVATE_KEY") as Hex;
66
+ const account = privateKeyToAccount(pk);
67
+ const walletClient = createWalletClient({ account, chain: base, transport: http() });
68
+ const publicClient = createPublicClient({ chain: base, transport: http() });
69
+
70
+ console.log(chalk.gray(` from ${account.address} β†’ sending transfer...`));
71
+ const txHash = await walletClient.writeContract({
72
+ address: inst.usdc_contract as Hex,
73
+ abi: erc20Abi,
74
+ functionName: "transfer",
75
+ args: [inst.pay_to as Hex, parseUnits(String(usdc), 6)], // USDC has 6 decimals
76
+ });
77
+ console.log(chalk.green(` sent: ${txHash}`));
78
+ console.log(chalk.gray(" waiting for 1 confirmation..."));
79
+
80
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
81
+ if (receipt.status !== "success") {
82
+ console.error(chalk.red(`tx reverted: ${txHash}`));
83
+ return;
84
+ }
85
+ console.log(chalk.green(` confirmed in block ${receipt.blockNumber}`));
86
+
87
+ // Step 3 β€” post tx_hash to casino. RPC indexers lag a few seconds behind the block;
88
+ // retry with linear backoff before giving up.
89
+ let credit: any;
90
+ for (const delay of [0, 3000, 5000, 8000, 12000]) {
91
+ if (delay) await new Promise(r => setTimeout(r, delay));
92
+ credit = await api("/api/twitch/topup", { token: TOKEN, usdc, tx_hash: txHash });
93
+ if (credit.ok) break;
94
+ const reason = String(credit.reason || "");
95
+ if (!reason.includes("no result")) break; // only retry RPC-propagation errors
96
+ console.log(chalk.gray(` indexer lag, retrying in ${delay || 3000}ms...`));
97
+ }
98
+ if (credit?.ok) {
99
+ console.log(chalk.green.bold(`βœ“ credited ${credit.credited_mcents} mcents; balance ${credit.balance_mcents}`));
100
+ } else {
101
+ console.error(chalk.red("credit failed:"), credit);
102
+ console.error(chalk.gray(` retry manually: curl -X POST ${CASINO}/api/twitch/topup -H 'Content-Type: application/json' -d '{"token":"${TOKEN.slice(0,8)}...","usdc":${usdc},"tx_hash":"${txHash}"}'`));
103
+ }
104
+ }
105
+
106
+ async function cmdJoin(name: string, model = MODEL, buyIn = 10.0) {
107
+ requireToken();
108
+ if (!name) { console.error(chalk.red("usage: join <name> [model]")); process.exit(1); }
109
+ const r: any = await api("/api/twitch/join", {
110
+ name, model, api_key: APIKEY, buy_in_usdc: buyIn, token: TOKEN,
111
+ });
112
+ console.log(chalk.cyan(JSON.stringify(r, null, 2)));
113
+ }
114
+
115
+ async function cmdState() {
116
+ const s: any = await api("/api/twitch/state", undefined, "GET");
117
+ const me = (s.players || []).find((p: any) => p.name && p.name !== "β€”");
118
+ console.log(chalk.white(`hand #${s.hand_num} street=${s.street} pot=$${s.pot}`));
119
+ for (const p of (s.players || [])) {
120
+ console.log(` seat ${p.seat}: ${p.name.padEnd(18)} stack=$${p.stack} ${p.last_action || ""}`);
121
+ }
122
+ if (s.queue_names?.length) console.log(chalk.gray(`queue: ${s.queue_names.join(", ")}`));
123
+ }
124
+
125
+ async function cmdCashout(wallet = CASHOUT_WALLET) {
126
+ requireToken();
127
+ const r: any = await api("/api/twitch/cashout", { token: TOKEN, wallet });
128
+ console.log(chalk.cyan(JSON.stringify(r, null, 2)));
129
+ }
130
+
131
+ async function cmdWalletNew(force = false) {
132
+ // Offline β€” generates a fresh Base/EVM-compatible keypair + auto-saves to .env.
133
+ const envPath = path.resolve(process.cwd(), ".env");
134
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
135
+ const hasPK = /^PRIVATE_KEY=0x[0-9a-fA-F]{64}/m.test(existing);
136
+ if (hasPK && !force) {
137
+ console.error(chalk.red("βœ— .env already has PRIVATE_KEY. Rerun with --force to overwrite (previous funds will be unreachable)."));
138
+ process.exit(1);
139
+ }
140
+ const pk = generatePrivateKey();
141
+ const acct = privateKeyToAccount(pk);
142
+
143
+ // Also auto-generate CASINO_TOKEN if missing (random 24-hex bearer)
144
+ const hasToken = /^CASINO_TOKEN=/m.test(existing);
145
+ const token = hasToken ? null : "bearer-" + crypto.randomBytes(12).toString("hex");
146
+
147
+ let next = existing;
148
+ next = next.replace(/^PRIVATE_KEY=.*$/m, ""); // strip old key
149
+ if (!next.endsWith("\n") && next.length) next += "\n";
150
+ next += `PRIVATE_KEY=${pk}\n`;
151
+ if (token) next += `CASINO_TOKEN=${token}\n`;
152
+ if (!/^CASINO_URL=/m.test(next)) next += "CASINO_URL=http://127.0.0.1:8000\n";
153
+ fs.writeFileSync(envPath, next, { mode: 0o600 });
154
+
155
+ console.log(chalk.green.bold("βœ“ Fresh burner wallet saved to .env"));
156
+ console.log();
157
+ console.log(chalk.white(" Address: ") + chalk.green(acct.address));
158
+ if (token) console.log(chalk.white(" Token: ") + chalk.cyan(token) + chalk.gray(" (keep secret β€” it owns your casino balance)"));
159
+ console.log();
160
+ console.log(chalk.yellow("Next:"));
161
+ console.log(chalk.gray(` 1. Send β‰₯0.05 USDC on Base to ${chalk.green(acct.address)} (from MetaMask/Coinbase/etc.)`));
162
+ console.log(chalk.gray(" Also send ~$0.001 worth of ETH for gas (or use a gas sponsor)."));
163
+ console.log(chalk.gray(" 2. npx tsx src/index.ts topup 0.01"));
164
+ }
165
+
166
+ async function cmdWatch() {
167
+ while (true) {
168
+ try { await cmdState(); } catch (e) { console.error(chalk.red(String(e))); }
169
+ console.log(chalk.gray("─".repeat(60)));
170
+ await new Promise(r => setTimeout(r, 3000));
171
+ }
172
+ }
173
+
174
+ const [, , cmd, ...rest] = process.argv;
175
+ (async () => {
176
+ switch (cmd) {
177
+ case "balance": return cmdBalance();
178
+ case "topup": return cmdTopup(rest[0] ? Number(rest[0]) : 0.01);
179
+ case "join": return cmdJoin(rest[0], rest[1] || MODEL, rest[2] ? Number(rest[2]) : 10.0);
180
+ case "state": return cmdState();
181
+ case "cashout": return cmdCashout(rest[0] || CASHOUT_WALLET);
182
+ case "watch": return cmdWatch();
183
+ case "wallet":
184
+ if (rest[0] === "new") return cmdWalletNew(rest.includes("--force"));
185
+ console.error(chalk.red("usage: wallet new [--force]")); return;
186
+ default:
187
+ console.log(chalk.white.bold("casino-cli") + chalk.gray(" β€” agent client for the AI Poker/Blackjack Casino"));
188
+ console.log("\nCommands:");
189
+ console.log(" wallet new generate a fresh Base wallet (offline)");
190
+ console.log(" balance check your USDC-denominated balance");
191
+ console.log(" topup [usdc] send USDC on Base + credit balance (default 0.01)");
192
+ console.log(" join <name> [model] [usdc] queue to play (buys in for usdc chips)");
193
+ console.log(" state show the current hand + seats");
194
+ console.log(" cashout [wallet] withdraw balance (to wallet or mock)");
195
+ console.log(" watch poll state every 3s");
196
+ console.log(chalk.gray(`\n Set PRIVATE_KEY + CASINO_TOKEN + CASINO_URL in .env`));
197
+ }
198
+ })().catch(e => { console.error(chalk.red("error:"), e); process.exit(1); });