@darksol/terminal 0.5.9 → 0.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -15,7 +15,7 @@ import { snipeToken, watchSnipe } from './trading/snipe.js';
15
15
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
16
16
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
17
17
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
18
- import { casinoBet, casinoTables, casinoStats, casinoReceipt } from './services/casino.js';
18
+ import { casinoBet, casinoTables, casinoStats, casinoReceipt, casinoHealth, casinoVerify } from './services/casino.js';
19
19
  import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
20
20
  import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
21
21
  import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
@@ -238,15 +238,26 @@ export function cli(argv) {
238
238
  .description('The Clawsino — on-chain betting');
239
239
 
240
240
  casino
241
- .command('bet <game> [choice]')
242
- .description('Place a bet (coin-flip, dice, hilo, slots)')
243
- .option('-n, --number <n>', 'Number for dice over/under')
244
- .option('-w, --wallet <addr>', 'Wallet address')
245
- .action((game, choice, opts) => casinoBet(game, { choice, ...opts }));
241
+ .command('status')
242
+ .description('House status, balance, games')
243
+ .action(() => casinoHealth());
244
+
245
+ casino
246
+ .command('bet [game]')
247
+ .description('Place a $1 USDC bet (interactive if game omitted)')
248
+ .option('-c, --choice <choice>', 'heads/tails, higher/lower')
249
+ .option('-d, --direction <dir>', 'over/under (dice)')
250
+ .option('-t, --threshold <n>', 'Dice threshold 2-5')
251
+ .option('-w, --wallet <addr>', 'Payout wallet address')
252
+ .action((game, opts) => casinoBet(game, {
253
+ choice: opts.choice,
254
+ direction: opts.direction,
255
+ threshold: opts.threshold ? parseInt(opts.threshold) : undefined,
256
+ }, { wallet: opts.wallet }));
246
257
 
247
258
  casino
248
259
  .command('tables')
249
- .description('View game tables')
260
+ .description('Recent bets')
250
261
  .action(() => casinoTables());
251
262
 
252
263
  casino
@@ -256,9 +267,14 @@ export function cli(argv) {
256
267
 
257
268
  casino
258
269
  .command('receipt <id>')
259
- .description('Verify bet receipt')
270
+ .description('Get bet receipt')
260
271
  .action((id) => casinoReceipt(id));
261
272
 
273
+ casino
274
+ .command('verify <id>')
275
+ .description('Verify bet on-chain')
276
+ .action((id) => casinoVerify(id));
277
+
262
278
  // ═══════════════════════════════════════
263
279
  // CARDS COMMANDS
264
280
  // ═══════════════════════════════════════
@@ -29,7 +29,7 @@ const config = new Conf({
29
29
  services: {
30
30
  type: 'object',
31
31
  default: {
32
- oracle: 'https://acp.darksol.net/oracle',
32
+ oracle: 'https://acp.darksol.net/api/oracle',
33
33
  casino: 'https://casino.darksol.net',
34
34
  cards: 'https://acp.darksol.net',
35
35
  facilitator: 'https://facilitator.darksol.net',
package/src/llm/intent.js CHANGED
@@ -65,8 +65,16 @@ ACTIONS (use the most specific one):
65
65
  - "analyze" — deep analysis of a token
66
66
  - "gas" — gas price check
67
67
  - "cards" — order a prepaid Visa/Mastercard with crypto (e.g. "order a $50 card", "get me a prepaid card")
68
+ - "casino" — play a casino game (coinflip, dice, hilo, slots). All bets are $1 USDC. (e.g. "flip a coin", "bet on heads", "play slots", "roll dice over 3")
68
69
  - "unknown" — can't determine what the user wants
69
70
 
71
+ CASINO GAMES:
72
+ - coinflip: { "choice": "heads" | "tails" } → 1.90x payout
73
+ - dice: { "direction": "over"|"under", "threshold": 2-5 } → variable payout
74
+ - hilo: { "choice": "higher" | "lower" } → ~2.06x payout
75
+ - slots: {} → Match-3: 5.00x, Match-2: 1.50x
76
+ All bets are exactly $1 USDC. House edge: 5%. Results verified on-chain.
77
+
70
78
  CARDS ORDERING:
71
79
  When the user wants to order a prepaid card, you MUST collect:
72
80
  1. amount — ONLY these denominations: $10, $25, $50, $100, $250, $500, $1000
@@ -91,7 +99,7 @@ If they mention AgentMail or "my email", suggest using their configured agent em
91
99
 
92
100
  When parsing, respond with ONLY valid JSON:
93
101
  {
94
- "action": "swap|send|snipe|dca|price|balance|info|analyze|gas|cards|unknown",
102
+ "action": "swap|send|snipe|dca|price|balance|info|analyze|gas|cards|casino|unknown",
95
103
  "tokenIn": "symbol or address (for swaps)",
96
104
  "tokenOut": "symbol or address (for swaps)",
97
105
  "token": "symbol (for send/price/analyze)",
@@ -100,6 +108,8 @@ When parsing, respond with ONLY valid JSON:
100
108
  "email": "delivery email (for cards)",
101
109
  "provider": "card provider (for cards, default: swype)",
102
110
  "ticker": "payment crypto (for cards, default: usdc)",
111
+ "gameType": "casino game: coinflip|dice|hilo|slots",
112
+ "betParams": "casino bet parameters object",
103
113
  "chain": "chain name if specified, null if not",
104
114
  "interval": "for DCA: 1h, 4h, 1d, etc.",
105
115
  "orders": "for DCA: number of orders",
@@ -147,6 +157,7 @@ COMMAND MAPPING:
147
157
  - balance → darksol wallet balance
148
158
  - gas → darksol gas <chain>
149
159
  - cards → darksol cards order -p <provider> -a <amount> -e <email> --ticker <crypto>
160
+ - casino → darksol casino bet <game> -c <choice>
150
161
  - analyze → darksol ai analyze <token>`;
151
162
 
152
163
  // ──────────────────────────────────────────────────
@@ -292,7 +303,7 @@ export async function startChat(opts = {}) {
292
303
  }
293
304
 
294
305
  // Try to detect actionable intent
295
- const actionKeywords = /\b(swap|send|transfer|buy|sell|snipe|dca|price|balance|gas|card|cards|order|prepaid|visa|mastercard)\b/i;
306
+ const actionKeywords = /\b(swap|send|transfer|buy|sell|snipe|dca|price|balance|gas|card|cards|order|prepaid|visa|mastercard|casino|bet|coinflip|coin|flip|dice|slots|hilo|gamble|play)\b/i;
296
307
  const isActionable = actionKeywords.test(input);
297
308
 
298
309
  let result;
@@ -628,6 +639,17 @@ export async function executeIntent(intent, opts = {}) {
628
639
  return { success: true, action: 'gas' };
629
640
  }
630
641
 
642
+ case 'casino': {
643
+ const { casinoBet } = await import('../services/casino.js');
644
+ const gameType = intent.gameType || 'coinflip';
645
+ const betParams = intent.betParams || {};
646
+ // Map common AI outputs
647
+ if (intent.choice) betParams.choice = intent.choice;
648
+ if (intent.direction) betParams.direction = intent.direction;
649
+ if (intent.threshold) betParams.threshold = intent.threshold;
650
+ return await casinoBet(gameType, betParams, { wallet: opts.wallet });
651
+ }
652
+
631
653
  case 'cards': {
632
654
  if (!intent.amount) {
633
655
  info('What denomination? We have $10, $25, $50, $100, $250, $500, $1000');
@@ -1,92 +1,291 @@
1
1
  import { fetchJSON } from '../utils/fetch.js';
2
2
  import fetch from 'node-fetch';
3
3
  import { getServiceURL } from '../config/store.js';
4
+ import { getConfig } from '../config/store.js';
4
5
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error, table } from '../ui/components.js';
6
+ import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
6
7
  import { showSection } from '../ui/banner.js';
7
8
 
8
9
  const getURL = () => getServiceURL('casino') || 'https://casino.darksol.net';
9
10
 
10
- export async function casinoBet(game, opts = {}) {
11
- const spin = spinner(`Placing ${game} bet...`).start();
11
+ // Available games with their bet params
12
+ const GAMES = {
13
+ coinflip: {
14
+ name: '🪙 Coin Flip',
15
+ payout: '1.90x',
16
+ params: { choice: ['heads', 'tails'] },
17
+ desc: 'Call heads or tails',
18
+ },
19
+ dice: {
20
+ name: '🎲 Dice',
21
+ payout: 'variable',
22
+ params: { direction: ['over', 'under'], threshold: [2, 3, 4, 5] },
23
+ desc: 'Over/under a number (2-5)',
24
+ },
25
+ hilo: {
26
+ name: '🃏 Hi-Lo',
27
+ payout: '~2.06x',
28
+ params: { choice: ['higher', 'lower'] },
29
+ desc: 'Higher or lower',
30
+ },
31
+ slots: {
32
+ name: '🎰 Slots',
33
+ payout: '1.50-5.00x',
34
+ params: {},
35
+ desc: 'Match symbols — no params needed',
36
+ },
37
+ };
38
+
39
+ export { GAMES };
40
+
41
+ /**
42
+ * Check casino health + x402 payment info
43
+ */
44
+ export async function casinoHealth() {
45
+ const spin = spinner('Checking casino...').start();
46
+ try {
47
+ const stats = await fetchJSON(`${getURL()}/api/stats`);
48
+ spin.succeed('Casino online');
49
+
50
+ showSection('THE CLAWSINO 🎰');
51
+ kvDisplay([
52
+ ['Status', stats.acceptingBets ? theme.success('● Open') : theme.error('○ Closed')],
53
+ ['House Balance', `$${stats.houseBalanceUsdc || '0'} USDC`],
54
+ ['Total Bets', String(stats.totalBets || 0)],
55
+ ['Total Wagered', `$${stats.totalWageredUsdc || '0'} USDC`],
56
+ ['Total Payouts', `$${stats.totalPayoutsUsdc || '0'} USDC`],
57
+ ['Win Rate', stats.winRate || '0%'],
58
+ ]);
59
+
60
+ console.log('');
61
+ showSection('GAMES');
62
+ for (const [id, g] of Object.entries(GAMES)) {
63
+ console.log(` ${theme.gold(g.name.padEnd(20))} ${theme.dim(g.payout.padEnd(12))} ${g.desc}`);
64
+ }
65
+ console.log('');
66
+ info('All bets are $1 USDC. House edge: 5%.');
67
+ info('Docs: https://casino.darksol.net/docs');
68
+ } catch (err) {
69
+ spin.fail('Casino unreachable');
70
+ error(err.message);
71
+ info('Check: https://casino.darksol.net/docs');
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Place a bet — requires wallet address for payouts
77
+ */
78
+ export async function casinoBet(gameType, betParams = {}, opts = {}) {
79
+ const inquirer = (await import('inquirer')).default;
80
+ const game = GAMES[gameType];
81
+
82
+ // Validate or prompt for game type
83
+ if (!game) {
84
+ if (gameType) warn(`Unknown game: ${gameType}`);
85
+ const { picked } = await inquirer.prompt([{
86
+ type: 'list',
87
+ name: 'picked',
88
+ message: theme.gold('Pick a game:'),
89
+ choices: Object.entries(GAMES).map(([id, g]) => ({ name: `${g.name} ${g.payout} — ${g.desc}`, value: id })),
90
+ }]);
91
+ gameType = picked;
92
+ }
93
+
94
+ const gameInfo = GAMES[gameType];
95
+
96
+ // Collect bet params based on game
97
+ if (gameType === 'coinflip' && !betParams.choice) {
98
+ const { choice } = await inquirer.prompt([{
99
+ type: 'list',
100
+ name: 'choice',
101
+ message: theme.gold('Heads or tails?'),
102
+ choices: ['heads', 'tails'],
103
+ }]);
104
+ betParams.choice = choice;
105
+ }
106
+
107
+ if (gameType === 'dice') {
108
+ if (!betParams.direction) {
109
+ const { direction } = await inquirer.prompt([{
110
+ type: 'list',
111
+ name: 'direction',
112
+ message: theme.gold('Over or under?'),
113
+ choices: ['over', 'under'],
114
+ }]);
115
+ betParams.direction = direction;
116
+ }
117
+ if (!betParams.threshold) {
118
+ const { threshold } = await inquirer.prompt([{
119
+ type: 'list',
120
+ name: 'threshold',
121
+ message: theme.gold('Threshold (2-5):'),
122
+ choices: ['2', '3', '4', '5'],
123
+ }]);
124
+ betParams.threshold = parseInt(threshold);
125
+ }
126
+ }
127
+
128
+ if (gameType === 'hilo' && !betParams.choice) {
129
+ const { choice } = await inquirer.prompt([{
130
+ type: 'list',
131
+ name: 'choice',
132
+ message: theme.gold('Higher or lower?'),
133
+ choices: ['higher', 'lower'],
134
+ }]);
135
+ betParams.choice = choice;
136
+ }
137
+
138
+ // Get wallet address
139
+ let agentWallet = opts.wallet;
140
+ if (!agentWallet) {
141
+ const activeWallet = getConfig('activeWallet');
142
+ if (activeWallet) {
143
+ try {
144
+ const { loadWallet } = await import('../wallet/keystore.js');
145
+ const w = loadWallet(activeWallet);
146
+ agentWallet = w.address;
147
+ } catch {}
148
+ }
149
+ }
150
+ if (!agentWallet) {
151
+ const { addr } = await inquirer.prompt([{
152
+ type: 'input',
153
+ name: 'addr',
154
+ message: theme.gold('Your wallet address (for payouts):'),
155
+ validate: v => v.startsWith('0x') && v.length === 42 ? true : 'Enter a valid 0x address',
156
+ }]);
157
+ agentWallet = addr;
158
+ }
159
+
160
+ // Confirm
161
+ console.log('');
162
+ kvDisplay([
163
+ ['Game', gameInfo.name],
164
+ ['Bet', '$1 USDC'],
165
+ ['Params', JSON.stringify(betParams)],
166
+ ['Payout Wallet', agentWallet],
167
+ ['Payout', gameInfo.payout],
168
+ ]);
169
+ console.log('');
170
+
171
+ const { confirm } = await inquirer.prompt([{
172
+ type: 'confirm',
173
+ name: 'confirm',
174
+ message: theme.gold('Place $1 USDC bet?'),
175
+ default: false,
176
+ }]);
177
+
178
+ if (!confirm) {
179
+ warn('Bet cancelled');
180
+ return;
181
+ }
182
+
183
+ const spin = spinner(`Playing ${gameInfo.name}...`).start();
12
184
  try {
13
185
  const data = await fetchJSON(`${getURL()}/api/bet`, {
14
186
  method: 'POST',
15
187
  headers: { 'Content-Type': 'application/json' },
16
- body: JSON.stringify({
17
- game,
18
- choice: opts.choice,
19
- number: opts.number,
20
- wallet: opts.wallet,
21
- }),
188
+ body: JSON.stringify({ gameType, betParams, agentWallet }),
22
189
  });
23
- spin.succeed('Bet placed');
24
190
 
25
- showSection(`CASINO — ${game.toUpperCase()}`);
191
+ if (data.won) {
192
+ spin.succeed(theme.success(`YOU WON! $${data.payoutAmount} USDC`));
193
+ } else {
194
+ spin.fail('You lost.');
195
+ }
196
+
197
+ console.log('');
198
+ showSection(`${gameInfo.name} RESULT`);
26
199
  kvDisplay([
27
- ['Game', game],
28
- ['Your Call', opts.choice || opts.number || 'N/A'],
29
- ['Result', data.result ? theme.gold.bold(data.result) : 'N/A'],
30
- ['Won', data.won ? theme.success.bold('YES!') : theme.error('No')],
31
- ['Payout', data.payout ? `$${data.payout}` : '$0'],
32
- ['TX', data.txHash || 'N/A'],
200
+ ['Bet ID', data.id || '-'],
201
+ ['Result', data.result || '-'],
202
+ ['Won', data.won ? theme.success('YES! 🎉') : theme.error('No')],
203
+ ['Payout', data.won ? `$${data.payoutAmount} USDC` : '$0'],
204
+ ['Oracle TX', data.oracleTxHash ? data.oracleTxHash.slice(0, 20) + '...' : '-'],
205
+ ['Payout TX', data.payoutTxHash ? data.payoutTxHash.slice(0, 20) + '...' : '-'],
33
206
  ]);
207
+
208
+ if (data.id) {
209
+ console.log('');
210
+ info(`Verify on-chain: darksol casino verify ${data.id}`);
211
+ }
212
+ console.log('');
34
213
  } catch (err) {
35
214
  spin.fail('Bet failed');
36
215
  error(err.message);
216
+ if (err.message.includes('not accepting') || err.message.includes('closed')) {
217
+ info('The casino may be temporarily closed. Check: darksol casino status');
218
+ }
37
219
  }
38
220
  }
39
221
 
222
+ /**
223
+ * Show recent bets
224
+ */
40
225
  export async function casinoTables() {
41
- const spin = spinner('Loading tables...').start();
226
+ const spin = spinner('Loading recent bets...').start();
42
227
  try {
43
228
  const data = await fetchJSON(`${getURL()}/api/tables`);
44
- spin.succeed('Tables loaded');
45
-
46
- showSection('CASINO TABLES');
47
- const tables = data.tables || data;
48
- if (Array.isArray(tables)) {
49
- const rows = tables.map(t => [
50
- theme.gold(t.name || t.game),
51
- t.multiplier || 'N/A',
52
- t.minBet || '$1',
53
- t.status || 'Open',
54
- ]);
55
- table(['Game', 'Multiplier', 'Min Bet', 'Status'], rows);
56
- } else {
57
- kvDisplay(Object.entries(tables).map(([k, v]) => [k, String(v)]));
229
+ spin.succeed('Loaded');
230
+
231
+ const items = data.items || [];
232
+ if (items.length === 0) {
233
+ info('No recent bets');
234
+ return;
58
235
  }
236
+
237
+ showSection('RECENT BETS');
238
+ const rows = items.map(b => [
239
+ b.gameType || '-',
240
+ b.result || '-',
241
+ b.won ? theme.success('Won') : theme.error('Lost'),
242
+ b.payoutAmount ? `$${b.payoutAmount}` : '$0',
243
+ b.agentWallet ? b.agentWallet.slice(0, 8) + '...' : '-',
244
+ ]);
245
+ table(['Game', 'Result', 'Won', 'Payout', 'Wallet'], rows);
59
246
  } catch (err) {
60
- spin.fail('Failed to load tables');
247
+ spin.fail('Failed');
61
248
  error(err.message);
62
249
  }
63
250
  }
64
251
 
252
+ /**
253
+ * Get stats
254
+ */
65
255
  export async function casinoStats() {
66
- const spin = spinner('Loading stats...').start();
256
+ return await casinoHealth();
257
+ }
258
+
259
+ /**
260
+ * Get receipt for a bet
261
+ */
262
+ export async function casinoReceipt(id) {
263
+ const spin = spinner(`Loading receipt ${id}...`).start();
67
264
  try {
68
- const data = await fetchJSON(`${getURL()}/api/stats`);
69
- spin.succeed('Stats loaded');
265
+ const data = await fetchJSON(`${getURL()}/api/receipt/${id}`);
266
+ spin.succeed('Receipt loaded');
70
267
 
71
- showSection('CASINO STATS');
268
+ showSection(`BET RECEIPT — ${id}`);
72
269
  kvDisplay(Object.entries(data).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]));
73
270
  } catch (err) {
74
- spin.fail('Failed to load stats');
271
+ spin.fail('Receipt not found');
75
272
  error(err.message);
76
273
  }
77
274
  }
78
275
 
79
- export async function casinoReceipt(id) {
80
- const spin = spinner(`Loading receipt ${id}...`).start();
276
+ /**
277
+ * Verify a bet on-chain
278
+ */
279
+ export async function casinoVerify(id) {
280
+ const spin = spinner(`Verifying ${id}...`).start();
81
281
  try {
82
- const data = await fetchJSON(`${getURL()}/api/receipt/${id}`);
83
- spin.succeed('Receipt loaded');
282
+ const data = await fetchJSON(`${getURL()}/api/verify/${id}`);
283
+ spin.succeed('Verified');
84
284
 
85
- showSection(`CASINO RECEIPT — ${id}`);
285
+ showSection(`ON-CHAIN VERIFICATION — ${id}`);
86
286
  kvDisplay(Object.entries(data).map(([k, v]) => [k, String(v)]));
87
287
  } catch (err) {
88
- spin.fail('Receipt not found');
288
+ spin.fail('Verification failed');
89
289
  error(err.message);
90
290
  }
91
291
  }
92
-
@@ -19,14 +19,19 @@ export async function facilitatorHealth() {
19
19
  ['Version', data.version || '-'],
20
20
  ['Protocol', data.protocol || 'x402'],
21
21
  ['Fee', data.fee || '0%'],
22
- ['Chains', Array.isArray(data.chains) ? data.chains.join(', ') : (data.chains || 'Base, Polygon')],
22
+ ['Chains', Array.isArray(data.chains) ? data.chains.map(c => c.chain || c).join(', ') : (data.chains || 'Base, Polygon')],
23
23
  ['Status', theme.success('● Online')],
24
24
  ]);
25
25
  if (data.description) {
26
26
  console.log('');
27
27
  console.log(theme.dim(` ${data.description}`));
28
28
  }
29
- if (data.contracts) {
29
+ if (Array.isArray(data.chains)) {
30
+ console.log('');
31
+ for (const c of data.chains) {
32
+ console.log(` ${theme.gold((c.chain || '').padEnd(10))} ${theme.dim(c.facilitator || '')} ${c.status === 'operational' ? theme.success('●') : theme.error('○')} ${c.settlements || 0} settlements, $${c.volumeUSDC || '0'} USDC`);
33
+ }
34
+ } else if (data.contracts) {
30
35
  console.log('');
31
36
  for (const [chain, addr] of Object.entries(data.contracts)) {
32
37
  console.log(` ${theme.gold(chain.padEnd(10))} ${theme.dim(addr)}`);
@@ -1,16 +1,97 @@
1
1
  import { fetchJSON } from '../utils/fetch.js';
2
2
  import fetch from 'node-fetch';
3
- import { getServiceURL } from '../config/store.js';
3
+ import { getServiceURL, getConfig } from '../config/store.js';
4
4
  import { theme } from '../ui/theme.js';
5
- import { spinner, kvDisplay, success, error } from '../ui/components.js';
5
+ import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
6
6
  import { showSection } from '../ui/banner.js';
7
7
 
8
- const getURL = () => getServiceURL('oracle') || 'https://acp.darksol.net/oracle';
8
+ // Oracle lives under acp.darksol.net/api/oracle/
9
+ // Endpoints: health (free), coin/dice/number/shuffle (x402 — $0.05 USDC on Base)
10
+ const getURL = () => {
11
+ const custom = getServiceURL('oracle');
12
+ if (custom) return custom;
13
+ return 'https://acp.darksol.net/api/oracle';
14
+ };
15
+
16
+ function handleX402(response) {
17
+ if (response === 402 || (response && response.status === 402)) {
18
+ warn('Oracle requires x402 payment ($0.05 USDC on Base)');
19
+ info('Use with agent signer: darksol signer start → requests auto-pay');
20
+ info('Or pay manually via facilitator: darksol facilitator');
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ async function oracleRequest(path, opts = {}) {
27
+ const url = `${getURL()}${path}`;
28
+ const resp = await fetch(url, opts);
29
+
30
+ if (resp.status === 402) {
31
+ // Return x402 info so caller can handle
32
+ const paymentHeader = resp.headers.get('payment-required');
33
+ let paymentInfo = null;
34
+ if (paymentHeader) {
35
+ try {
36
+ paymentInfo = JSON.parse(Buffer.from(paymentHeader, 'base64').toString());
37
+ } catch {}
38
+ }
39
+ return { x402: true, paymentInfo, status: 402 };
40
+ }
41
+
42
+ const ct = resp.headers.get('content-type') || '';
43
+ if (!ct.includes('json')) {
44
+ throw new Error(`Oracle returned non-JSON response (${resp.status})`);
45
+ }
46
+ return await resp.json();
47
+ }
48
+
49
+ export async function oracleHealth() {
50
+ const spin = spinner('Checking oracle...').start();
51
+ try {
52
+ const data = await oracleRequest('/health');
53
+ if (data.x402) {
54
+ spin.succeed('Oracle online (health should be free)');
55
+ return;
56
+ }
57
+ spin.succeed('Oracle online');
58
+
59
+ showSection('ORACLE STATUS');
60
+ kvDisplay([
61
+ ['Status', data.status === 'ok' ? theme.success('● Online') : theme.error('○ ' + data.status)],
62
+ ['Contract', data.contract || '-'],
63
+ ['Chain', data.chain || 'base'],
64
+ ['Block', String(data.blockNumber || '-')],
65
+ ]);
66
+ console.log('');
67
+ info('Endpoints require x402 payment ($0.05 USDC on Base)');
68
+ info('Games: coin flip, dice, random number, shuffle');
69
+ info('Docs: https://acp.darksol.net/oracle');
70
+ } catch (err) {
71
+ spin.fail('Oracle unreachable');
72
+ error(err.message);
73
+ }
74
+ }
9
75
 
10
76
  export async function oracleFlip() {
11
77
  const spin = spinner('Flipping coin...').start();
12
78
  try {
13
- const data = await fetchJSON(`${getURL()}/api/coin`);
79
+ const data = await oracleRequest('/coin');
80
+ if (data.x402) {
81
+ spin.info('Payment required');
82
+ handleX402(data);
83
+ if (data.paymentInfo) {
84
+ const accepts = data.paymentInfo.accepts?.[0];
85
+ if (accepts) {
86
+ kvDisplay([
87
+ ['Amount', `$${(parseInt(accepts.amount) / 1e6).toFixed(2)} USDC`],
88
+ ['Network', 'Base'],
89
+ ['Pay To', accepts.payTo || '-'],
90
+ ]);
91
+ }
92
+ }
93
+ return;
94
+ }
14
95
  spin.succeed('Coin flipped');
15
96
  showSection('ORACLE — COIN FLIP');
16
97
  kvDisplay([
@@ -26,7 +107,12 @@ export async function oracleFlip() {
26
107
  export async function oracleDice(sides = 6) {
27
108
  const spin = spinner(`Rolling d${sides}...`).start();
28
109
  try {
29
- const data = await fetchJSON(`${getURL()}/api/dice?sides=${sides}`);
110
+ const data = await oracleRequest(`/dice?sides=${sides}`);
111
+ if (data.x402) {
112
+ spin.info('Payment required');
113
+ handleX402(data);
114
+ return;
115
+ }
30
116
  spin.succeed('Dice rolled');
31
117
  showSection(`ORACLE — D${sides}`);
32
118
  kvDisplay([
@@ -43,7 +129,12 @@ export async function oracleDice(sides = 6) {
43
129
  export async function oracleNumber(min = 1, max = 100) {
44
130
  const spin = spinner(`Generating number ${min}-${max}...`).start();
45
131
  try {
46
- const data = await fetchJSON(`${getURL()}/api/number?min=${min}&max=${max}`);
132
+ const data = await oracleRequest(`/number?min=${min}&max=${max}`);
133
+ if (data.x402) {
134
+ spin.info('Payment required');
135
+ handleX402(data);
136
+ return;
137
+ }
47
138
  spin.succeed('Number generated');
48
139
  showSection('ORACLE — RANDOM NUMBER');
49
140
  kvDisplay([
@@ -60,11 +151,16 @@ export async function oracleNumber(min = 1, max = 100) {
60
151
  export async function oracleShuffle(items) {
61
152
  const spin = spinner('Shuffling...').start();
62
153
  try {
63
- const data = await fetchJSON(`${getURL()}/api/shuffle`, {
154
+ const data = await oracleRequest('/shuffle', {
64
155
  method: 'POST',
65
156
  headers: { 'Content-Type': 'application/json' },
66
157
  body: JSON.stringify({ items }),
67
158
  });
159
+ if (data.x402) {
160
+ spin.info('Payment required');
161
+ handleX402(data);
162
+ return;
163
+ }
68
164
  spin.succeed('Shuffled');
69
165
  showSection('ORACLE — SHUFFLE');
70
166
  console.log(theme.gold(' Result: ') + (data.result || data.value || []).join(', '));
@@ -73,16 +169,3 @@ export async function oracleShuffle(items) {
73
169
  error(err.message);
74
170
  }
75
171
  }
76
-
77
- export async function oracleHealth() {
78
- const spin = spinner('Checking oracle...').start();
79
- try {
80
- const data = await fetchJSON(`${getURL()}/api/health`);
81
- spin.succeed('Oracle online');
82
- showSection('ORACLE STATUS');
83
- kvDisplay(Object.entries(data).map(([k, v]) => [k, String(v)]));
84
- } catch (err) {
85
- spin.fail('Oracle unreachable');
86
- error(err.message);
87
- }
88
- }
@@ -457,7 +457,7 @@ export async function handleCommand(cmd, ws) {
457
457
  return await cmdChatLogs(args, ws);
458
458
  default: {
459
459
  // Fuzzy: if it looks like natural language, route to AI
460
- const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard)\b/i;
460
+ const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard|bet|coinflip|flip|dice|slots|hilo|gamble|play|casino)\b/i;
461
461
  if (nlKeywords.test(cmd)) {
462
462
  return await cmdAI(cmd.split(/\s+/), ws);
463
463
  }
@@ -1121,24 +1121,38 @@ async function executeCardOrder(orderMeta, ws) {
1121
1121
  // ORACLE
1122
1122
  // ══════════════════════════════════════════════════
1123
1123
  async function cmdOracle(args, ws) {
1124
+ ws.sendLine(`${ANSI.gold} ◆ RANDOM ORACLE 🎲${ANSI.reset}`);
1125
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1124
1126
  try {
1125
- const resp = await fetch('https://acp.darksol.net/oracle');
1127
+ const resp = await fetch('https://acp.darksol.net/api/oracle/health');
1128
+ const ct = resp.headers.get('content-type') || '';
1129
+ if (!ct.includes('json')) throw new Error('not json');
1126
1130
  const data = await resp.json();
1127
1131
 
1128
- ws.sendLine(`${ANSI.gold} ORACLE${ANSI.reset}`);
1129
- ws.sendLine(`${ANSI.dim} ${''.repeat(50)}${ANSI.reset}`);
1130
- ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.status || 'unknown'}`);
1131
- ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.blue}acp.darksol.net/oracle${ANSI.reset}`);
1132
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.status === 'ok' ? `${ANSI.green} Online${ANSI.reset}` : `${ANSI.red}○ ${data.status}${ANSI.reset}`}`);
1133
+ ws.sendLine(` ${ANSI.darkGold}Contract${ANSI.reset} ${ANSI.dim}${data.contract || '-'}${ANSI.reset}`);
1134
+ ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${data.chain || 'base'}${ANSI.reset}`);
1135
+ ws.sendLine(` ${ANSI.darkGold}Block${ANSI.reset} ${ANSI.white}${data.blockNumber || '-'}${ANSI.reset}`);
1136
+ ws.sendLine('');
1137
+ ws.sendLine(` ${ANSI.gold}ENDPOINTS${ANSI.reset} ${ANSI.dim}x402-gated ($0.05 USDC on Base)${ANSI.reset}`);
1138
+ ws.sendLine(` ${ANSI.white}🪙 /coin${ANSI.reset} ${ANSI.dim}Fair coin flip${ANSI.reset}`);
1139
+ ws.sendLine(` ${ANSI.white}🎲 /dice${ANSI.reset} ${ANSI.dim}Roll with N sides${ANSI.reset}`);
1140
+ ws.sendLine(` ${ANSI.white}🔢 /number${ANSI.reset} ${ANSI.dim}Random in range${ANSI.reset}`);
1141
+ ws.sendLine(` ${ANSI.white}🔀 /shuffle${ANSI.reset} ${ANSI.dim}Shuffle a list${ANSI.reset}`);
1142
+ ws.sendLine('');
1143
+ ws.sendLine(` ${ANSI.dim}CLI: darksol oracle flip / dice / number / shuffle${ANSI.reset}`);
1144
+ ws.sendLine(` ${ANSI.dim}Docs: https://acp.darksol.net/oracle${ANSI.reset}`);
1132
1145
  ws.sendLine('');
1133
1146
  } catch {
1134
- ws.sendLine(` ${ANSI.dim}Oracle unreachable${ANSI.reset}`);
1147
+ ws.sendLine(` ${ANSI.red}Oracle unreachable${ANSI.reset}`);
1148
+ ws.sendLine(` ${ANSI.dim}Check: https://acp.darksol.net/oracle${ANSI.reset}`);
1135
1149
  ws.sendLine('');
1136
1150
  }
1137
1151
  return {};
1138
1152
  }
1139
1153
 
1140
1154
  async function cmdCasino(args, ws) {
1141
- ws.sendLine(`${ANSI.gold} ◆ CASINO${ANSI.reset}`);
1155
+ ws.sendLine(`${ANSI.gold} ◆ THE CLAWSINO 🎰${ANSI.reset}`);
1142
1156
  ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1143
1157
  try {
1144
1158
  const resp = await fetch('https://casino.darksol.net/api/stats');
@@ -1146,12 +1160,19 @@ async function cmdCasino(args, ws) {
1146
1160
  if (!ct.includes('json')) throw new Error('not json');
1147
1161
  const data = await resp.json();
1148
1162
 
1149
- ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${ANSI.green}● Online${ANSI.reset}`);
1150
- ws.sendLine(` ${ANSI.darkGold}Bankroll${ANSI.reset} ${ANSI.white}$${data.bankrollUsdc || '0'} USDC${ANSI.reset}`);
1151
- ws.sendLine(` ${ANSI.darkGold}Total Bets${ANSI.reset} ${ANSI.white}${data.totalBets || 0}${ANSI.reset}`);
1152
- ws.sendLine(` ${ANSI.darkGold}Win Rate${ANSI.reset} ${ANSI.white}${data.winRate || '0'}%${ANSI.reset}`);
1153
- ws.sendLine(` ${ANSI.darkGold}Endpoint${ANSI.reset} ${ANSI.blue}casino.darksol.net${ANSI.reset}`);
1154
- ws.sendLine(` ${ANSI.darkGold}Docs${ANSI.reset} ${ANSI.blue}casino.darksol.net/docs${ANSI.reset}`);
1163
+ ws.sendLine(` ${ANSI.darkGold}Status${ANSI.reset} ${data.acceptingBets ? `${ANSI.green}● Open${ANSI.reset}` : `${ANSI.red}○ Closed${ANSI.reset}`}`);
1164
+ ws.sendLine(` ${ANSI.darkGold}House${ANSI.reset} ${ANSI.white}$${data.houseBalanceUsdc || '0'} USDC${ANSI.reset}`);
1165
+ ws.sendLine(` ${ANSI.darkGold}Total Bets${ANSI.reset} ${ANSI.white}${data.totalBets || 0}${ANSI.reset}`);
1166
+ ws.sendLine(` ${ANSI.darkGold}Win Rate${ANSI.reset} ${ANSI.white}${data.winRate || '0%'}${ANSI.reset}`);
1167
+ ws.sendLine('');
1168
+ ws.sendLine(` ${ANSI.gold}GAMES${ANSI.reset} ${ANSI.dim}All bets $1 USDC • 5% house edge${ANSI.reset}`);
1169
+ ws.sendLine(` ${ANSI.white}🪙 Coin Flip${ANSI.reset} ${ANSI.dim}1.90x — heads or tails${ANSI.reset}`);
1170
+ ws.sendLine(` ${ANSI.white}🎲 Dice${ANSI.reset} ${ANSI.dim}variable — over/under 2-5${ANSI.reset}`);
1171
+ ws.sendLine(` ${ANSI.white}🃏 Hi-Lo${ANSI.reset} ${ANSI.dim}~2.06x — higher or lower${ANSI.reset}`);
1172
+ ws.sendLine(` ${ANSI.white}🎰 Slots${ANSI.reset} ${ANSI.dim}1.50-5.00x — match symbols${ANSI.reset}`);
1173
+ ws.sendLine('');
1174
+ ws.sendLine(` ${ANSI.dim}Play via CLI: darksol casino bet${ANSI.reset}`);
1175
+ ws.sendLine(` ${ANSI.dim}Or ask AI: "flip a coin" / "bet on heads"${ANSI.reset}`);
1155
1176
  ws.sendLine('');
1156
1177
  } catch {
1157
1178
  ws.sendLine(` ${ANSI.red}○ Casino unreachable${ANSI.reset}`);
@@ -1174,7 +1195,7 @@ async function cmdFacilitator(args, ws) {
1174
1195
  ws.sendLine(` ${ANSI.darkGold}Service${ANSI.reset} ${ANSI.white}${data.service || 'DARKSOL Facilitator'}${ANSI.reset}`);
1175
1196
  ws.sendLine(` ${ANSI.darkGold}Protocol${ANSI.reset} ${ANSI.white}${data.protocol || 'x402'}${ANSI.reset}`);
1176
1197
  ws.sendLine(` ${ANSI.darkGold}Fee${ANSI.reset} ${ANSI.green}${data.fee || '0%'}${ANSI.reset}`);
1177
- ws.sendLine(` ${ANSI.darkGold}Chains${ANSI.reset} ${ANSI.white}${Array.isArray(data.chains) ? data.chains.join(', ') : 'Base, Polygon'}${ANSI.reset}`);
1198
+ ws.sendLine(` ${ANSI.darkGold}Chains${ANSI.reset} ${ANSI.white}${Array.isArray(data.chains) ? data.chains.map(c => `${c.chain} (${c.status})`) .join(', ') : 'Base, Polygon'}${ANSI.reset}`);
1178
1199
  ws.sendLine(` ${ANSI.darkGold}Docs${ANSI.reset} ${ANSI.blue}acp.darksol.net/facilitator${ANSI.reset}`);
1179
1200
  ws.sendLine('');
1180
1201
  } catch {