nico commited on
Commit Β·
7c735db
1
Parent(s): 47c2ae6
Initial commit: casino-cli
Browse files- .env-example +19 -0
- .gitignore +5 -0
- README.md +56 -0
- package-lock.json +280 -0
- package.json +32 -0
- 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); });
|