dankgrinder 4.6.0 → 4.8.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.
@@ -46,6 +46,24 @@ for (let i = 0; i < args.length; i++) {
46
46
  apiUrl = apiUrl || process.env.DANKGRINDER_URL || DEFAULT_URL;
47
47
  if (redisUrl) process.env.REDIS_URL = redisUrl;
48
48
 
49
+ // Keep process alive on transient discord interaction fetch failures.
50
+ process.on('uncaughtException', (err) => {
51
+ const msg = String(err?.message || err || '');
52
+ const stack = String(err?.stack || '');
53
+ if (msg.toLowerCase().includes('fetch failed') && stack.includes('discord.js-selfbot-v13')) {
54
+ console.error(`\n ${C.red}✗ Discord interaction fetch failed (transient). Continuing...${C.r}\n`);
55
+ return;
56
+ }
57
+ throw err;
58
+ });
59
+ process.on('unhandledRejection', (reason) => {
60
+ const msg = String(reason?.message || reason || '');
61
+ if (msg.toLowerCase().includes('fetch failed')) {
62
+ console.error(`\n ${C.red}✗ Unhandled fetch failure (network/transient). Continuing...${C.r}\n`);
63
+ return;
64
+ }
65
+ });
66
+
49
67
  if (!apiKey) {
50
68
  console.error(`\n ${C.red}✗ Missing API key.${C.r}\n`);
51
69
  console.error(` ${C.b}Usage:${C.r} npx dankgrinder --key <YOUR_API_KEY>\n`);
@@ -34,11 +34,10 @@
34
34
  */
35
35
 
36
36
  const {
37
- LOG, c, sleep, humanDelay, getFullText, parseCoins, parseBalance,
38
- getAllButtons, getAllSelectMenus, findButton, findSelectMenuOption,
39
- safeClickButton, isHoldTight, logMsg, dumpMessage, needsItem,
37
+ LOG, c, sleep, humanDelay, getFullText, parseCoins,
38
+ getAllButtons, getAllSelectMenus, findButton,
39
+ safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
- const { buyItem } = require('./shop');
42
41
 
43
42
  // ── Adventure type rotation (cycle through all types each run) ────
44
43
  let lastAdventureIndex = -1;
@@ -63,8 +62,7 @@ async function clickAndRefetch(channel, msg, btn) {
63
62
  LOG.error(`[adventure] Click error: ${e.message}`);
64
63
  return null;
65
64
  }
66
- // Dank Memer edits the message after a button click
67
- await sleep(500);
65
+ await sleep(250);
68
66
  return await refetchMsg(channel, msg.id);
69
67
  }
70
68
 
@@ -172,7 +170,7 @@ async function playAdventureRounds(channel, msg) {
172
170
  const choice = pickSafeChoice(choices);
173
171
  if (choice) {
174
172
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
175
- await sleep(200);
173
+ await sleep(100);
176
174
  const afterChoice = await clickAndRefetch(channel, current, choice);
177
175
  if (afterChoice) {
178
176
  current = afterChoice;
@@ -194,7 +192,7 @@ async function playAdventureRounds(channel, msg) {
194
192
  const nextBtnNow = getNextButton(current);
195
193
  if (nextBtnNow && !nextBtnNow.disabled) {
196
194
  LOG.debug(`[adventure] Clicking Next arrow...`);
197
- await sleep(200);
195
+ await sleep(100);
198
196
  const afterNext = await clickAndRefetch(channel, current, nextBtnNow);
199
197
  if (afterNext) {
200
198
  current = afterNext;
@@ -206,7 +204,7 @@ async function playAdventureRounds(channel, msg) {
206
204
  } else if (nextBtnNow && nextBtnNow.disabled) {
207
205
  // Next is disabled but no choices found — might be loading
208
206
  LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
209
- await sleep(600);
207
+ await sleep(300);
210
208
  const refreshed = await refetchMsg(channel, current.id);
211
209
  if (refreshed) {
212
210
  current = refreshed;
@@ -219,8 +217,7 @@ async function playAdventureRounds(channel, msg) {
219
217
  // No next button at all
220
218
  LOG.debug(`[adventure] No Next button — checking if done`);
221
219
  if (isAdventureDone(current)) break;
222
- // Wait and re-fetch as Dank Memer might still be editing
223
- await sleep(1500);
220
+ await sleep(500);
224
221
  const refreshed = await refetchMsg(channel, current.id);
225
222
  if (refreshed) {
226
223
  current = refreshed;
@@ -314,42 +311,10 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
314
311
  return { result: 'cooldown', coins: 0, nextCooldownSec: cooldownSec + 3 };
315
312
  }
316
313
 
317
- // ── 4) If we need a ticket, try to buy one ─────────────────
314
+ // ── 4) If we need a ticket, skip (too expensive to auto-buy) ──
318
315
  if (needsTicket) {
319
- LOG.warn(`[adventure] Need adventure ticket! Attempting to buy...`);
320
-
321
- // Ticket costs 250,000 coins — check balance first to avoid wasting time in shop
322
- const TICKET_COST = 250000;
323
- let currentBalance = 0;
324
- await channel.send('pls bal');
325
- const balMsg = await waitForDankMemer(8000);
326
- if (balMsg) {
327
- currentBalance = parseBalance(balMsg);
328
- LOG.info(`[adventure] Balance: ${c.yellow}⏣ ${currentBalance.toLocaleString()}${c.reset} (ticket costs ⏣ ${TICKET_COST.toLocaleString()})`);
329
- }
330
-
331
- if (currentBalance < TICKET_COST) {
332
- LOG.warn(`[adventure] Not enough coins for ticket (⏣ ${currentBalance.toLocaleString()} < ⏣ ${TICKET_COST.toLocaleString()}). Grind more first.`);
333
- return { result: `need ticket (⏣ ${currentBalance.toLocaleString()}/${TICKET_COST.toLocaleString()})`, coins: 0, nextCooldownSec: 120 };
334
- }
335
-
336
- const bought = await buyItem({
337
- channel, waitForDankMemer, client,
338
- itemName: 'Adventure Ticket', quantity: 1,
339
- });
340
-
341
- if (!bought) {
342
- LOG.error('[adventure] Could not buy adventure ticket from shop.');
343
- return { result: 'need ticket (buy failed)', coins: 0, nextCooldownSec: 120 };
344
- }
345
-
346
- LOG.success('[adventure] Tickets purchased! Re-running adventure...');
347
- await sleep(1500);
348
-
349
- await channel.send('pls adventure');
350
- response = await waitForDankMemer(12000);
351
- if (!response) return { result: 'no response after ticket buy', coins: 0, nextCooldownSec: null };
352
- logMsg(response, 'adventure-after-buy');
316
+ LOG.warn(`[adventure] No ticket skipping (too expensive to auto-buy)`);
317
+ return { result: 'no ticket', coins: 0, nextCooldownSec: 3600, skipReason: 'no_ticket' };
353
318
  }
354
319
 
355
320
  // ── Check if we're already mid-adventure (no select menu) ──
@@ -45,7 +45,7 @@ async function runDig({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Shovel',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Simple gambling command handlers.
3
- * Covers: coinflip, roulette, slots, snakeeyes
2
+ * Gambling command handlers.
3
+ * Covers: cointoss, roulette, slots, snakeeyes
4
4
  */
5
5
 
6
6
  const {
@@ -8,6 +8,29 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
+ function parseGambleResult(text, cmdName) {
12
+ const net = parseNetCoins(text);
13
+ const lower = text.toLowerCase();
14
+
15
+ if (net > 0) {
16
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
17
+ return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
18
+ }
19
+ if (net < 0) {
20
+ LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
21
+ return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
22
+ }
23
+ const coins = parseCoins(text);
24
+ if (coins > 0) {
25
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
26
+ return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
27
+ }
28
+ if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
29
+ if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
30
+
31
+ return { result: `${cmdName} done`, coins: 0 };
32
+ }
33
+
11
34
  async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
12
35
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
13
36
 
@@ -26,16 +49,29 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
26
49
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
27
50
  }
28
51
 
52
+ // Check for min bet error
53
+ const initText = getFullText(response);
54
+ const initLower = initText.toLowerCase();
55
+ if (initLower.includes("can't bet less than") || initLower.includes('cannot bet less than') || initLower.includes('minimum bet')) {
56
+ const betMatch = initText.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || initText.match(/(\d[\d,]+)/);
57
+ if (betMatch) {
58
+ const minBet = parseInt(betMatch[1].replace(/,/g, ''));
59
+ if (minBet > 0) return { result: `min bet ${minBet}`, coins: 0, newMinBet: minBet };
60
+ }
61
+ return { result: 'min bet error', coins: 0 };
62
+ }
63
+
29
64
  logMsg(response, cmdName);
30
65
  const text = getFullText(response);
31
66
 
32
- // Some gambles have buttons (coinflip: pick heads/tails)
67
+ // For cointoss/gambles with buttons: click a random non-disabled button
33
68
  const buttons = getAllButtons(response);
34
69
  if (buttons.length > 0) {
35
- const btn = buttons.find(b => !b.disabled);
36
- if (btn) {
37
- LOG.info(`[${cmdName}] Clicking "${btn.label}"`);
38
- await humanDelay();
70
+ const clickable = buttons.filter(b => !b.disabled);
71
+ if (clickable.length > 0) {
72
+ const btn = clickable[Math.floor(Math.random() * clickable.length)];
73
+ LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
+ await humanDelay(50, 200);
39
75
  try {
40
76
  const followUp = await safeClickButton(response, btn);
41
77
  if (followUp) {
@@ -50,32 +86,11 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
50
86
  return parseGambleResult(text, cmdName);
51
87
  }
52
88
 
53
- function parseGambleResult(text, cmdName) {
54
- const net = parseNetCoins(text);
55
- const lower = text.toLowerCase();
56
-
57
- if (net > 0) {
58
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
59
- return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
60
- }
61
- if (net < 0) {
62
- LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
63
- return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
64
- }
65
- // Fallback to parseCoins
66
- const coins = parseCoins(text);
67
- if (coins > 0) {
68
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
69
- return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
70
- }
71
- if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
72
- if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
73
-
74
- return { result: `${cmdName} done`, coins: 0 };
75
- }
76
-
77
- async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
78
- return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
89
+ /**
90
+ * Cointoss: send "pls cointoss <bet>", then click Heads or Tails randomly.
91
+ */
92
+ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
93
+ return runGamble({ channel, waitForDankMemer, cmdName: 'cointoss', cmdString: `pls cointoss ${betAmount}` });
79
94
  }
80
95
 
81
96
  async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
@@ -90,4 +105,4 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
90
105
  return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
91
106
  }
92
107
 
93
- module.exports = { runGamble, runCoinflip, runRoulette, runSlots, runSnakeeyes };
108
+ module.exports = { runGamble, runCointoss, runRoulette, runSlots, runSnakeeyes };
@@ -45,7 +45,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
45
45
  const missing = needsItem(text);
46
46
  if (missing) {
47
47
  LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
48
- const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 3 });
48
+ const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
49
49
  if (bought) {
50
50
  LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
51
51
  await sleep(3000);
@@ -65,11 +65,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
65
65
  return { result: `need ${missing} (buy failed)`, coins: 0 };
66
66
  }
67
67
 
68
- // Handle buttons if present
68
+ // Handle buttons if present — pick a random non-disabled button
69
69
  const buttons = getAllButtons(response);
70
70
  if (buttons.length > 0) {
71
- const btn = buttons.find(b => !b.disabled) || buttons[0];
72
- if (btn && !btn.disabled) {
71
+ const clickable = buttons.filter(b => !b.disabled);
72
+ const btn = clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
73
+ if (btn) {
73
74
  LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
75
  await humanDelay();
75
76
  try {
@@ -45,7 +45,7 @@ async function runHunt({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Hunting Rifle',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -16,7 +16,7 @@ const { runScratch } = require('./scratch');
16
16
  const { runBlackjack } = require('./blackjack');
17
17
  const { runTrivia, triviaDB } = require('./trivia');
18
18
  const { runWorkShift } = require('./work');
19
- const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
19
+ const { runCointoss, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
22
  const { runStream } = require('./stream');
@@ -39,7 +39,7 @@ module.exports = {
39
39
  runBlackjack,
40
40
  runTrivia,
41
41
  runWorkShift,
42
- runCoinflip,
42
+ runCointoss,
43
43
  runRoulette,
44
44
  runSlots,
45
45
  runSnakeeyes,
@@ -32,8 +32,12 @@ const ITEM_COSTS = {
32
32
  * @returns {Promise<boolean>} true if purchase succeeded
33
33
  */
34
34
  async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
35
- const MAX_RETRIES = 3;
36
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', '');
35
+ const MAX_RETRIES = 1;
36
+ const searchNames = [
37
+ itemName.toLowerCase(),
38
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', ''),
39
+ itemName.toLowerCase().split(' ').pop(),
40
+ ];
37
41
 
38
42
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
39
43
  LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset} (attempt ${attempt}/${MAX_RETRIES})`);
@@ -120,30 +124,17 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
120
124
  logMsg(response, 'shop-after-nav');
121
125
 
122
126
  // Step 3: Find the Buy button for our item
123
- let buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
127
+ let buyBtn = getAllButtons(response).find(b => {
128
+ if (!b.label) return false;
129
+ const label = b.label.toLowerCase();
130
+ return searchNames.some(s => label.includes(s) || s.includes(label));
131
+ });
124
132
 
125
133
  if (!buyBtn) {
126
- LOG.warn(`No Buy button found for "${itemName}" (searched: "${searchName}")`);
127
- // Log all available buttons for debugging
134
+ LOG.warn(`No Buy button found for "${itemName}"`);
128
135
  const allBtns = getAllButtons(response);
129
136
  if (allBtns.length > 0) {
130
- LOG.debug(`Available buttons: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
131
- }
132
- // Maybe we need to scroll/paginate?
133
- const nextBtn = findButton(response, 'next') || findButton(response, '▶') || findButton(response, '→');
134
- if (nextBtn && !nextBtn.disabled) {
135
- LOG.buy('Clicking next page to find item...');
136
- try {
137
- const nextPage = await safeClickButton(response, nextBtn);
138
- if (nextPage) {
139
- response = nextPage;
140
- logMsg(response, 'shop-page2');
141
- // Try finding button again
142
- buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
143
- }
144
- } catch (e) {
145
- LOG.error(`Page nav failed: ${e.message}`);
146
- }
137
+ LOG.debug(`Available: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
147
138
  }
148
139
 
149
140
  if (!buyBtn) {
package/lib/grinder.js CHANGED
@@ -270,15 +270,32 @@ function log(type, msg, label) {
270
270
  }
271
271
  }
272
272
 
273
- async function fetchConfig() {
274
- try {
275
- const res = await fetch(`${API_URL}/api/grinder/config`, {
276
- headers: { Authorization: `Bearer ${API_KEY}` },
277
- });
278
- const data = await res.json();
279
- if (data.error) { log('error', `Config fetch failed: ${data.error}`); return null; }
280
- return data;
281
- } catch (err) { log('error', `Cannot reach API: ${err.message}`); return null; }
273
+ async function fetchConfig(retries = 3, delayMs = 1500) {
274
+ for (let attempt = 1; attempt <= retries; attempt++) {
275
+ try {
276
+ const controller = new AbortController();
277
+ const t = setTimeout(() => controller.abort(), 10000);
278
+ const res = await fetch(`${API_URL}/api/grinder/config`, {
279
+ headers: { Authorization: `Bearer ${API_KEY}` },
280
+ signal: controller.signal,
281
+ });
282
+ clearTimeout(t);
283
+ const data = await res.json();
284
+ if (data.error) {
285
+ log('error', `Config fetch failed: ${data.error}`);
286
+ return null;
287
+ }
288
+ return data;
289
+ } catch (err) {
290
+ if (attempt < retries) {
291
+ log('warn', `API fetch failed (attempt ${attempt}/${retries}) — retrying...`);
292
+ await new Promise((r) => setTimeout(r, delayMs * attempt));
293
+ } else {
294
+ log('error', `Cannot reach API: ${err.message}`);
295
+ }
296
+ }
297
+ }
298
+ return null;
282
299
  }
283
300
 
284
301
  async function sendLog(accountName, command, response, status) {
@@ -509,6 +526,7 @@ class AccountWorker {
509
526
  this.globalCooldownUntil = 0;
510
527
  this.commandQueue = null;
511
528
  this.lastHealthCheck = Date.now();
529
+ this.doneToday = new Map(); // in-memory dedup: cmd → expiry timestamp
512
530
  }
513
531
 
514
532
  get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
@@ -616,8 +634,8 @@ class AccountWorker {
616
634
  return null;
617
635
  }
618
636
 
619
- async buyItem(itemName, quantity = 10) {
620
- const MAX_RETRIES = 3;
637
+ async buyItem(itemName, quantity = 1) {
638
+ const MAX_RETRIES = 1;
621
639
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
622
640
  this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
623
641
  if (this.account.use_slash) {
@@ -671,12 +689,18 @@ class AccountWorker {
671
689
  }
672
690
  await humanDelay(1000, 2000);
673
691
 
674
- // Find Buy button
692
+ // Find Buy button — match by full name or partial name
675
693
  let buyBtn = null;
676
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '');
694
+ const searchNames = [
695
+ itemName.toLowerCase(),
696
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
697
+ itemName.toLowerCase().split(' ')[0],
698
+ ];
677
699
  for (const row of response.components || []) {
678
700
  for (const comp of row.components || []) {
679
- if (comp.type === 2 && comp.label && comp.label.toLowerCase().includes(searchName)) {
701
+ if (comp.type !== 2 || !comp.label) continue;
702
+ const label = comp.label.toLowerCase();
703
+ if (searchNames.some(s => label.includes(s) || s.includes(label))) {
680
704
  buyBtn = comp; break;
681
705
  }
682
706
  }
@@ -788,7 +812,7 @@ class AccountWorker {
788
812
  case 'dep max': cmdString = `${prefix} dep max`; break;
789
813
  case 'with max': cmdString = `${prefix} with max`; break;
790
814
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
791
- case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
815
+ case 'cointoss': cmdString = `${prefix} cointoss ${betAmount}`; break;
792
816
  case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
793
817
  case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
794
818
  case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
@@ -826,7 +850,7 @@ class AccountWorker {
826
850
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
827
851
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
828
852
  case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
829
- case 'coinflip': cmdResult = await commands.runCoinflip(cmdOpts); break;
853
+ case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
830
854
  case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
831
855
  case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
832
856
  case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
@@ -878,18 +902,24 @@ class AccountWorker {
878
902
  return;
879
903
  }
880
904
 
881
- // Min bet detection — raise bet amount
882
- if (resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
883
- const betMatch = result.match(/⏣\s*([\d,]+)/);
905
+ // Min bet detection — "You can't bet less than 10,000" or "cannot bet less than ⏣ 5,000"
906
+ if (resultLower.includes("can't bet less than") || resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
907
+ const betMatch = result.match(/less than\s*\*?\*?\s*[⏣o]?\s*([\d,]+)/i) || result.match(/(\d[\d,]+)/);
884
908
  if (betMatch) {
885
909
  const minBet = parseInt(betMatch[1].replace(/,/g, ''));
886
910
  if (minBet > 0) {
887
911
  this.account.bet_amount = minBet;
888
- this.log('info', `${cmdName} min bet: ⏣ ${minBet.toLocaleString()}`);
912
+ this.log('info', `${cmdName} min bet raised → ⏣ ${minBet.toLocaleString()}`);
889
913
  }
890
914
  }
891
915
  return;
892
916
  }
917
+ // Also handle newMinBet from gamble handler
918
+ if (cmdResult.newMinBet && cmdResult.newMinBet > (this.account.bet_amount || 0)) {
919
+ this.account.bet_amount = cmdResult.newMinBet;
920
+ this.log('info', `${cmdName} min bet raised → ⏣ ${cmdResult.newMinBet.toLocaleString()}`);
921
+ return;
922
+ }
893
923
 
894
924
  // Premium-only command detection — disable for 24h
895
925
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
@@ -899,41 +929,50 @@ class AccountWorker {
899
929
  return;
900
930
  }
901
931
 
902
- // Already claimed today (daily/weekly) — set long cooldown + mark in Redis
932
+ // Already claimed today (daily/weekly) — set long cooldown + mark done
903
933
  if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
904
934
  this.log('info', `${cmdName} already claimed — waiting`);
905
935
  const timeMatch = result.match(/<t:(\d+):R>/);
936
+ let waitSec;
906
937
  if (timeMatch) {
907
938
  const nextAvail = parseInt(timeMatch[1]) * 1000;
908
- const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
909
- await this.setCooldown(cmdName, waitSec);
910
- if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
939
+ waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
911
940
  } else {
912
- const fallbackSec = cmdName === 'daily' ? 86400 : 604800;
913
- await this.setCooldown(cmdName, fallbackSec);
914
- if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', fallbackSec); } catch {}
941
+ waitSec = cmdName === 'daily' ? 86400 : 604800;
915
942
  }
943
+ await this.setCooldown(cmdName, waitSec);
944
+ this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
945
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
916
946
  return;
917
947
  }
918
948
 
919
- // Scratch level-gate: if level too low, disable for 24h
949
+ // Skip reasons: level too low, no ticket, etc.
920
950
  if (cmdResult.skipReason === 'level') {
921
951
  this.log('warn', `${cmdName} level too low — retry in 24h`);
922
952
  await this.setCooldown(cmdName, 86400);
923
953
  return;
924
954
  }
955
+ if (cmdResult.skipReason === 'no_ticket') {
956
+ this.log('warn', `${cmdName} no ticket — retry in 1h`);
957
+ await this.setCooldown(cmdName, 3600);
958
+ return;
959
+ }
925
960
 
926
961
  const earned = Math.max(0, cmdResult.coins || 0);
927
962
  const spent = Math.max(0, cmdResult.lost || 0);
928
963
  if (earned > 0) this.stats.coins += earned;
929
964
  if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
930
965
 
931
- // Mark daily/drops as done in Redis so we don't re-run this session
932
- if (redis && earned > 0 && cmdName === 'daily') {
933
- try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
966
+ // Mark daily/drops as done so we don't re-run this session
967
+ if (cmdName === 'daily' && earned > 0) {
968
+ const expiry = Date.now() + 86400 * 1000;
969
+ this.doneToday.set('daily', expiry);
970
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
934
971
  }
935
- if (redis && cmdName === 'drops') {
936
- try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
972
+ if (cmdName === 'drops') {
973
+ const expiry = Date.now() + 86400 * 1000;
974
+ this.doneToday.set('drops', expiry);
975
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
937
976
  }
938
977
 
939
978
  if (cmdResult.holdTightReason) {
@@ -991,7 +1030,7 @@ class AccountWorker {
991
1030
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
992
1031
  // Gambling (fast cycle)
993
1032
  { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
994
- { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
1033
+ { key: 'cmd_cointoss', cmd: 'cointoss', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
995
1034
  { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
996
1035
  { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
997
1036
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
@@ -1147,17 +1186,28 @@ class AccountWorker {
1147
1186
  return;
1148
1187
  }
1149
1188
 
1150
- // Skip daily/drops if already done today (Redis marker)
1151
- if (redis && (item.cmd === 'daily' || item.cmd === 'drops')) {
1152
- try {
1153
- const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1154
- if (done) {
1155
- item.nextRunAt = now + 86400 * 1000;
1156
- if (this.commandQueue) this.commandQueue.push(item);
1157
- this.tickTimeout = setTimeout(() => this.tick(), 100);
1158
- return;
1159
- }
1160
- } catch {}
1189
+ // Skip daily/drops if already done today (in-memory + Redis)
1190
+ if (item.cmd === 'daily' || item.cmd === 'drops') {
1191
+ const memExpiry = this.doneToday.get(item.cmd);
1192
+ if (memExpiry && Date.now() < memExpiry) {
1193
+ item.nextRunAt = memExpiry;
1194
+ if (this.commandQueue) this.commandQueue.push(item);
1195
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1196
+ return;
1197
+ }
1198
+ if (redis) {
1199
+ try {
1200
+ const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1201
+ if (done) {
1202
+ const expiry = now + 86400 * 1000;
1203
+ this.doneToday.set(item.cmd, expiry);
1204
+ item.nextRunAt = expiry;
1205
+ if (this.commandQueue) this.commandQueue.push(item);
1206
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1207
+ return;
1208
+ }
1209
+ } catch {}
1210
+ }
1161
1211
  }
1162
1212
 
1163
1213
  this.busy = true;
@@ -1167,10 +1217,11 @@ class AccountWorker {
1167
1217
 
1168
1218
  await this.setCooldown(item.cmd, totalWait);
1169
1219
 
1220
+ // Inter-command delay: 1-3s random (human-like spacing)
1170
1221
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1171
- const globalJitter = 500 + Math.random() * 1000;
1172
- if (timeSinceLastCmd < globalJitter) {
1173
- await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1222
+ const minGap = 1000 + Math.random() * 2000; // 1-3s
1223
+ if (timeSinceLastCmd < minGap) {
1224
+ await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
1174
1225
  }
1175
1226
 
1176
1227
  const prefix = this.account.use_slash ? '/' : 'pls';
@@ -1326,7 +1377,7 @@ class AccountWorker {
1326
1377
  { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
1327
1378
  { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
1328
1379
  { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
1329
- { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1380
+ { key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
1330
1381
  { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1331
1382
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1332
1383
  { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
@@ -1377,7 +1428,7 @@ async function start(apiKey, apiUrl) {
1377
1428
 
1378
1429
  console.log(colorBanner());
1379
1430
  console.log(
1380
- ` ${rgb(139, 92, 246)}v4.6${c.reset}` +
1431
+ ` ${rgb(139, 92, 246)}v4.8.1${c.reset}` +
1381
1432
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1382
1433
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1383
1434
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
@@ -1392,17 +1443,18 @@ async function start(apiKey, apiUrl) {
1392
1443
 
1393
1444
  log('info', `${c.dim}Fetching accounts...${c.reset}`);
1394
1445
 
1395
- const data = await fetchConfig();
1396
- if (!data) {
1446
+ let data = await fetchConfig(4, 2000);
1447
+ while (!data) {
1397
1448
  log('error', `Cannot connect to API`);
1398
- log('error', `Check your API key or API URL and try again.`);
1399
- process.exit(1);
1449
+ log('warn', `Will retry in 10s (check internet/API URL if this repeats).`);
1450
+ await new Promise((r) => setTimeout(r, 10000));
1451
+ data = await fetchConfig(4, 2000);
1400
1452
  }
1401
1453
 
1402
1454
  const { accounts } = data;
1403
1455
  if (!accounts || accounts.length === 0) {
1404
1456
  log('error', 'No active accounts. Add them in the dashboard.');
1405
- process.exit(1);
1457
+ return;
1406
1458
  }
1407
1459
 
1408
1460
  checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}${accounts.length} Account${accounts.length > 1 ? 's' : ''}${c.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.6.0",
3
+ "version": "4.8.1",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "debug": "^4.4.0",
24
- "discord.js-selfbot-v13": "^3.6.1",
24
+ "discord.js-selfbot-v13": "3.5.0",
25
25
  "ioredis": "^5.10.1",
26
26
  "sharp": "^0.34.5"
27
27
  },