clockwork-press 0.0.1 → 0.2.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/dist/press.js ADDED
@@ -0,0 +1,744 @@
1
+ // The Press. Every tick: sweep and claim what the launch owes, split what the
2
+ // wallet holds, burn, stash, and move ops only at fair value.
3
+ // Whatever the number is — large or embarrassing — the press posts.
4
+ import { formatUnits, parseUnits } from 'viem';
5
+ import { cfg, launched, napping } from './env.js';
6
+ import { pub, wallet, account } from './chain.js';
7
+ import { erc20Abi, curveAbi } from './abis.js';
8
+ import { CLOCKWORK_ADDRESS } from './config.js';
9
+ import { quoteBuy } from './quote.js';
10
+ import { quoteGmeToToken, swapGmeToToken } from './v4.js';
11
+ import { checkPeg } from './peg.js';
12
+ import { readMarket } from './market.js';
13
+ import { readHolders } from './holders.js';
14
+ import { gmeRank } from './gmerank.js';
15
+ import { readLaunch, unswept, trySweep, owed, claimPair, vestStatus, releaseVest, claimVestTokens, poolIdFor, reason } from './pons.js';
16
+ import { appendPress, readLedger, writeLedger, writeStats, burnGmeTotal, statsPath, countKind, hasTx, dailyRollup, totals } from './ledger.js';
17
+ import { parseAbiItem } from 'viem';
18
+ import { readFileSync } from 'node:fs';
19
+ import { post, postCard, ART } from './telegram.js';
20
+ // two assets, two decimal counts: the pairing asset is whatever the launch is priced in, the launch token is 18.
21
+ const fmtPair = (v, dp = 2) => Number(formatUnits(v, cfg.pairDecimals)).toFixed(dp);
22
+ const fmtTok = (v, dp = 0) => Number(formatUnits(v, 18)).toFixed(dp);
23
+ const pairNum = (v) => Number(formatUnits(v, cfg.pairDecimals));
24
+ const TOTAL_SUPPLY = BigInt(cfg.totalSupply) * 10n ** 18n; // whole tokens at mint, from clockwork.json
25
+ const ZERO = '0x0000000000000000000000000000000000000000';
26
+ // a native pair: the machine pays gas in the same asset it presses, so a little stays behind.
27
+ const GAS_RESERVE = parseUnits('0.005', 18);
28
+ const W = cfg.words;
29
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
30
+ async function waitTx(hash) {
31
+ const rcpt = await pub.waitForTransactionReceipt({ hash });
32
+ if (rcpt.status !== 'success')
33
+ throw new Error(`tx reverted: ${hash}`);
34
+ return hash;
35
+ }
36
+ function prevStats() {
37
+ try {
38
+ return JSON.parse(readFileSync(statsPath(), 'utf8'));
39
+ }
40
+ catch {
41
+ return {};
42
+ }
43
+ }
44
+ function staleStats() {
45
+ const at = prevStats().checkedAt;
46
+ return !at || Date.now() - new Date(at).getTime() > cfg.refreshMin * 60_000;
47
+ }
48
+ // the pairing asset's balance: the chain's own coin when the launch is native, an erc20 otherwise.
49
+ const pairBal = (holder) => cfg.pairNative ? pub.getBalance({ address: holder }) : pub.readContract({ address: cfg.gme, abi: erc20Abi, functionName: 'balanceOf', args: [holder] });
50
+ const tokBal = (holder, asset) => pub.readContract({ address: asset, abi: erc20Abi, functionName: 'balanceOf', args: [holder] });
51
+ // one transfer of the pairing asset, whichever kind it is.
52
+ async function sendPair(to, amount) {
53
+ const w = wallet();
54
+ if (cfg.pairNative)
55
+ return waitTx(await w.sendTransaction({ to, value: amount }));
56
+ return waitTx(await w.writeContract({ address: cfg.gme, abi: erc20Abi, functionName: 'transfer', args: [to, amount] }));
57
+ }
58
+ // The numbers the site reads every run: supply burned, the treasury, the sunk total.
59
+ async function siteNumbers(token, stash) {
60
+ const [supply, stashBal] = await Promise.all([
61
+ pub.readContract({ address: token, abi: erc20Abi, functionName: 'totalSupply' }),
62
+ cfg.stash ? pairBal(stash) : Promise.resolve(0n),
63
+ ]);
64
+ const burnedPct = (Number(TOTAL_SUPPLY - supply) / Number(TOTAL_SUPPLY)) * 100;
65
+ const stashGme = pairNum(stashBal);
66
+ return { burnedPct, stashGme, gmeSunk: stashGme + burnGmeTotal() + Number(cfg.gradSeedGme) };
67
+ }
68
+ // The stash scanner: every transfer of the pairing asset into the stash is a press (a claim landing),
69
+ // found by reading the chain, never by a human pasting a hash. Returns how many were new.
70
+ async function scanStash(fairUsd) {
71
+ if (!cfg.stash || cfg.pairNative)
72
+ return 0; // a native pair leaves no Transfer logs to scan
73
+ const ledger = readLedger();
74
+ const latest = await pub.getBlockNumber();
75
+ const from = ledger.stashScanBlock ? BigInt(ledger.stashScanBlock) + 1n : latest - 200000n; // first run: ~5.5 hours back
76
+ if (from > latest)
77
+ return 0;
78
+ const logs = await pub.getLogs({
79
+ address: cfg.gme,
80
+ event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
81
+ args: { to: cfg.stash }, fromBlock: from, toBlock: latest,
82
+ });
83
+ // the pairing asset that landed in the burn wallet from the same sender in the same window is part of the press too
84
+ // (the 45% for snacks, and whatever else the founder fed the machine); the row says how much, not why.
85
+ const fed = cfg.pressWallet ? await pub.getLogs({
86
+ address: cfg.gme, event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
87
+ args: { to: cfg.pressWallet }, fromBlock: from, toBlock: latest,
88
+ }) : [];
89
+ let added = 0;
90
+ for (const log of logs) {
91
+ const tx = log.transactionHash;
92
+ const amt = log.args.value;
93
+ if (amt === 0n || hasTx(tx))
94
+ continue;
95
+ // the machine's own treasury leg is a slice, never a claim, whether or not its row got written
96
+ if (cfg.pressWallet && same(log.args.from, cfg.pressWallet))
97
+ continue;
98
+ const gme = pairNum(amt);
99
+ const sender = log.args.from.toLowerCase();
100
+ const toBurn = fed.filter((f) => f.args.from.toLowerCase() === sender).reduce((t, f) => t + pairNum(f.args.value), 0);
101
+ const fedNote = toBurn > 0 ? ` ${toBurn.toFixed(2)} ${cfg.pairSymbol} went to the machine wallet in the same ${W.claim}, for ${W.slice}s.` : '';
102
+ const entry = appendPress({
103
+ kind: 'press',
104
+ note: `${W.claim}: ${gme.toFixed(2)} ${cfg.pairSymbol} claimed from fees, into the ${W.treasury}. never sold, never distributed.${fedNote}`,
105
+ burnedGmerald: '0', burnGmeSpent: '0', stashedGme: gme.toFixed(4), opsMovedGme: '0',
106
+ pegStatus: 'n/a', stashTx: tx, gmeUsd: fairUsd && fairUsd > 0 ? fairUsd : undefined,
107
+ });
108
+ await postCard(ART.press, [`${cfg.emoji} ${W.claim} #${entry.k}`, `${W.treasury}: +${gme.toFixed(2)} ${cfg.pairSymbol}. never sold, never distributed.`, fedNote ? fedNote.trim() : null, `the ${W.treasury}: ${Math.round(totals().stashedGme).toLocaleString('en-US')} ${cfg.pairSymbol}`]);
109
+ added++;
110
+ }
111
+ const l2 = readLedger();
112
+ l2.stashScanBlock = latest.toString();
113
+ writeLedger(l2);
114
+ return added;
115
+ }
116
+ // The minimum interval. The primary clock fires at :00/:15/:30/:45 and the backup github cron three
117
+ // minutes later, often late. A tick is refused only when a run already worked in the same slot (the
118
+ // backup leaves when the primary ran; a delayed backup at :12 still belongs to the :00 slot) or within
119
+ // half a cadence, five minutes at most (a primary that fired a second early and the backup three minutes
120
+ // after it are one tick, not two). The last run is the newest of stats.lastRunAt and the last ledger row,
121
+ // so a slice that wrote its row counts even when the stats write was lost. A timestamp in the future is
122
+ // a bad clock, not a recent run.
123
+ function tooSoon() {
124
+ const stamps = [prevStats().lastRunAt, readLedger().presses.at(-1)?.ts].filter(Boolean).map((t) => new Date(t).getTime()).filter((t) => !Number.isNaN(t));
125
+ if (!stamps.length)
126
+ return null;
127
+ const at = Math.max(...stamps), now = Date.now();
128
+ const min = (now - at) / 60_000;
129
+ if (min < 0)
130
+ return null;
131
+ const slot = (t) => Math.floor(t / (cfg.cadenceMin * 60_000));
132
+ return slot(now) === slot(at) || min < Math.min(5, cfg.cadenceMin / 2) ? Math.round(min) : null;
133
+ }
134
+ // lastRunAt is written the moment a run commits to doing work (the first signed transaction), on top of
135
+ // whatever stats are there. a napping tick never writes it, so the stats file does not change every tick.
136
+ function markRun(at) {
137
+ const prev = prevStats();
138
+ writeStats({ presses: 0, burnedPct: '0.00', stashGme: '0', gmeSunk: '0', status: 'running', updatedAt: null, ...prev, lastRunAt: at });
139
+ }
140
+ // the legs of the slice in flight. saved after every transaction lands, cleared when the row is written,
141
+ // so a run that dies between two legs leaves its hashes for the next run to record.
142
+ function saveLegs(legs) {
143
+ if (cfg.dry)
144
+ return;
145
+ const l = readLedger();
146
+ l.meta = { ...(l.meta ?? {}) };
147
+ if (legs)
148
+ l.meta.pendingLegs = legs;
149
+ else
150
+ delete l.meta.pendingLegs;
151
+ writeLedger(l);
152
+ }
153
+ function recoverLegs() {
154
+ const legs = readLedger().meta?.pendingLegs;
155
+ if (!legs)
156
+ return;
157
+ const sent = [legs.burnTx, legs.stashTx, legs.serviceTx, legs.opsTx].filter(Boolean).length;
158
+ if (sent) {
159
+ // whatever the interrupted run bought and did not burn still sits in the wallet; this run burns it below
160
+ const entry = appendPress({
161
+ kind: 'snack', note: `${legs.note ?? 'pressed'} · the run stopped after ${sent} of its legs at ${legs.startedAt.slice(0, 16).replace('T', ' ')} utc; recorded on the next tick`,
162
+ burnedGmerald: legs.burnedGmerald ?? '0', burnGmeSpent: legs.burnGmeSpent ?? '0', stashedGme: legs.stashedGme ?? '0', opsMovedGme: legs.opsMovedGme ?? '0',
163
+ pegStatus: legs.pegStatus ?? 'n/a', burnTx: legs.burnTx, stashTx: legs.stashTx, opsTx: legs.opsTx, serviceTx: legs.serviceTx, serviceGme: legs.serviceGme ?? '0', gmeUsd: legs.gmeUsd,
164
+ });
165
+ console.log(`[press] recovered ${W.slice} #${entry.k} from a run that stopped mid-way (${sent} legs landed)`);
166
+ }
167
+ saveLegs(undefined);
168
+ }
169
+ // a read the run can live without: log the reason, hand back the fallback, carry on to the burn.
170
+ async function tryRead(what, fn, fallback) {
171
+ try {
172
+ return await fn();
173
+ }
174
+ catch (e) {
175
+ console.log(`[pons] ${what} read failed: ${reason(e)}`);
176
+ return fallback;
177
+ }
178
+ }
179
+ async function watchRecipient(s, me) {
180
+ const now = new Date().toISOString();
181
+ if (cfg.claimMode !== 'auto')
182
+ return { elsewhere: false, feeRecipient: { address: s.recipient, ok: true }, pendingRecipient: null };
183
+ const prev = prevStats().feeRecipient;
184
+ const ledger = readLedger();
185
+ const meta = ledger.meta ?? {};
186
+ let dirty = false;
187
+ const ok = same(s.recipient, me);
188
+ let feeRecipient;
189
+ if (ok) {
190
+ feeRecipient = { address: s.recipient, ok: true };
191
+ if (meta.alertedRecipient) {
192
+ if (cfg.dry)
193
+ console.log('[press] dry: would post "fees point at the machine again"');
194
+ else {
195
+ await post(`${cfg.emoji} fees point at the machine again.`);
196
+ delete meta.alertedRecipient;
197
+ dirty = true;
198
+ }
199
+ }
200
+ }
201
+ else {
202
+ // keep the earliest since across runs, so the row says how long it has been
203
+ const since = prev && !prev.ok && same(prev.address, s.recipient) && prev.since ? prev.since : now;
204
+ feeRecipient = { address: s.recipient, ok: false, since };
205
+ console.log(`[press] fees point elsewhere: ${s.recipient} since ${since}; the machine burns what it holds and claims nothing new`);
206
+ if (!meta.alertedRecipient || !same(meta.alertedRecipient, s.recipient)) {
207
+ const lines = ['fees no longer point at the machine', `since ${since.slice(0, 16).replace('T', ' ')} utc creator fees go to ${s.recipient}`, 'the machine burns what it holds and claims nothing new.', 'if this was you, nothing to do. if not, open pons.'];
208
+ if (cfg.dry)
209
+ console.log(`[press] dry: would post the alert:\n${lines.join('\n')}`);
210
+ else {
211
+ await postCard('', lines);
212
+ meta.alertedRecipient = s.recipient;
213
+ dirty = true;
214
+ }
215
+ }
216
+ }
217
+ let pendingRecipient = null;
218
+ if (s.pending) {
219
+ pendingRecipient = { to: s.pending.to, effectiveAt: s.pending.effectiveAt };
220
+ const when = new Date(s.pending.effectiveAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
221
+ console.log(`[press] a fee-recipient change is pending: ${s.pending.to} from ${when} utc`);
222
+ if (!meta.alertedPending || !same(meta.alertedPending, s.pending.to)) {
223
+ const lines = ['a fee-recipient change is pending', `from ${when} utc creator fees would go to ${s.pending.to}`, 'nothing has moved yet; this is the advance notice.', 'if this was you, nothing to do. if not, open pons.'];
224
+ if (cfg.dry)
225
+ console.log(`[press] dry: would post the alert:\n${lines.join('\n')}`);
226
+ else {
227
+ await postCard('', lines);
228
+ meta.alertedPending = s.pending.to;
229
+ dirty = true;
230
+ }
231
+ }
232
+ }
233
+ else if (meta.alertedPending) {
234
+ delete meta.alertedPending;
235
+ dirty = true;
236
+ }
237
+ if (dirty && !cfg.dry) {
238
+ ledger.meta = meta;
239
+ writeLedger(ledger);
240
+ }
241
+ return { elsewhere: !ok, feeRecipient, pendingRecipient };
242
+ }
243
+ // a run that dies after it committed to work leaves lastRunAt and a status behind, so the interval guard
244
+ // and the site both see it; the workflow commits data/ even when the step failed.
245
+ export async function runPress() {
246
+ const runAt = new Date().toISOString();
247
+ let committed = false;
248
+ // called before the first signed transaction, once. a napping or dry tick never gets here.
249
+ const commit = () => { if (committed || cfg.dry)
250
+ return; committed = true; markRun(runAt); };
251
+ try {
252
+ await pressOnce(runAt, commit);
253
+ }
254
+ catch (e) {
255
+ const why = reason(e);
256
+ if (!cfg.dry) {
257
+ const prev = prevStats();
258
+ writeStats({ presses: 0, burnedPct: '0.00', stashGme: '0', gmeSunk: '0', updatedAt: null, ...prev, status: `failed: ${why}`.slice(0, 160), checkedAt: new Date().toISOString(), ...(committed ? { lastRunAt: runAt } : {}) });
259
+ }
260
+ console.log(`[press] the run failed: ${why}`);
261
+ throw e;
262
+ }
263
+ }
264
+ async function pressOnce(runAt, commit) {
265
+ const soon = cfg.dry ? null : tooSoon();
266
+ if (soon !== null) {
267
+ console.log(`[press] ran ${soon} min ago, the next tick is not due; leaving`);
268
+ return;
269
+ }
270
+ // The peg reading is free (feed + dexscreener) and worth publishing before
271
+ // launch: the site shows the machine's sensor is real while the machine naps.
272
+ const peg = await checkPeg();
273
+ console.log(`[press] peg: ${peg.status} — ${peg.note}`);
274
+ const pegOut = { status: peg.status, premiumBps: peg.premiumBps, tokenUsd: peg.tokenUsd, fairUsd: peg.fairUsd, note: peg.note };
275
+ const holders = await readHolders();
276
+ if (holders)
277
+ console.log(`[holders] ${holders}`);
278
+ try {
279
+ await gmeRank();
280
+ }
281
+ catch (e) {
282
+ console.log(`[gme-rank] skipped: ${e.shortMessage || e.message}`);
283
+ }
284
+ if (!launched()) {
285
+ if (!cfg.dry) {
286
+ writeStats({ presses: 0, burnedPct: '0.00', stashGme: '0', gmeSunk: '0', status: 'napping til launch', updatedAt: null, checkedAt: new Date().toISOString(), peg: pegOut });
287
+ }
288
+ console.log('[press] not launched yet — wrote the peg check only. exiting cleanly.');
289
+ return;
290
+ }
291
+ const token = cfg.token;
292
+ const stash = (cfg.stash || ZERO);
293
+ // 0. Where is the launch — curve or pool — and where do its fees point?
294
+ const s = await readLaunch();
295
+ console.log(`[press] launch: ${s.phaseWord}${s.graduationPct != null ? `, ${s.graduationPct.toFixed(1)}% to graduation` : ''}; fees go to ${s.recipient}`);
296
+ // a slice that stopped between legs last time is recorded first, before the stash scan can mistake it for a claim
297
+ if (!cfg.dry)
298
+ recoverLegs();
299
+ const newPresses = cfg.dry ? 0 : await scanStash(peg.fairUsd);
300
+ if (newPresses)
301
+ console.log(`[press] stash scan: ${newPresses} new press(es) logged from the chain`);
302
+ const mustWrite = () => newPresses > 0 || staleStats();
303
+ // what every stats write carries this run; the recipient watch and unswept are filled in once known.
304
+ // the recipient row is red whenever an auto-mode machine is not where the fees point, key or no key.
305
+ // lastRunAt is whatever the file holds: markRun sets it when a run commits to work, never a napping tick.
306
+ const common = () => ({
307
+ presses: countKind('press'), snacks: countKind('snack'), checkedAt: new Date().toISOString(), peg: pegOut,
308
+ cadenceMin: cfg.cadenceMin, days: dailyRollup(), holders, lastRunAt: prevStats().lastRunAt,
309
+ phase: s.phaseWord, graduationPct: s.graduationPct,
310
+ feeRecipient: { address: s.recipient, ok: cfg.claimMode !== 'auto' || same(s.recipient, cfg.pressWallet) }, pendingRecipient: null,
311
+ });
312
+ const lastTs = () => readLedger().presses.at(-1)?.ts ?? null;
313
+ if (!cfg.key) {
314
+ // Launched, but the bot has no key yet: the founder presses by hand. Refresh
315
+ // what the site reads (supply, stash, peg) and leave without signing anything.
316
+ const [n, u] = await Promise.all([siteNumbers(token, stash), unswept(s)]);
317
+ if (u.total > 0n)
318
+ console.log(`[press] ${fmtPair(u.total, 4)} ${cfg.pairSymbol} of fees wait on the ${s.phaseWord}, unswept`);
319
+ if (!cfg.dry && mustWrite()) {
320
+ writeStats({
321
+ ...common(), burnedPct: n.burnedPct.toFixed(2), stashGme: n.stashGme.toFixed(2), gmeSunk: n.gmeSunk.toFixed(2),
322
+ unswept: fmtPair(u.total, 4), status: 'pressing by hand until the bot wakes', updatedAt: lastTs(),
323
+ });
324
+ }
325
+ console.log(`[press] no MACHINE_WALLET_KEY — refreshed the numbers (stash ${n.stashGme.toFixed(2)} ${cfg.pairSymbol}) and left. the founder presses by hand.`);
326
+ return;
327
+ }
328
+ const me = account().address;
329
+ const w = wallet();
330
+ // 0b. The recipient watch, then the claim block.
331
+ const watch = await watchRecipient(s, me);
332
+ const mine = same(s.recipient, me);
333
+ const minPress = parseUnits(cfg.minPressGme, cfg.pairDecimals);
334
+ let unsweptOut = '0';
335
+ let escrowOut = '0';
336
+ // 1. Fees reach the escrow only after a sweep: on the curve they accrue as balances on the curve, in
337
+ // the pool as pendings on the hook. The numbers are read in every mode; only auto mode sweeps,
338
+ // claims and releases (manual: the founder claims by hand and the machine presses what it is sent).
339
+ // None of these reads may end the run: the wallet can still burn what it holds without them.
340
+ const auto = cfg.claimMode === 'auto';
341
+ const u = await unswept(s);
342
+ if (u.total > 0n) {
343
+ unsweptOut = fmtPair(u.total, 4);
344
+ if (!mine)
345
+ console.log(`[press] ${unsweptOut} ${cfg.pairSymbol} of fees unswept, and they are not ours to sweep`);
346
+ else if (!auto)
347
+ console.log(`[press] ${unsweptOut} ${cfg.pairSymbol} of fees wait on the ${s.phaseWord}, unswept (claim mode manual: the founder sweeps and claims)`);
348
+ else {
349
+ const r = await trySweep(s, { onSend: commit });
350
+ if (r.swept) {
351
+ console.log(`[press] swept ${unsweptOut} ${cfg.pairSymbol} of fees into the escrow${r.tx ? `: ${cfg.explorer}/tx/${r.tx}` : ''}`);
352
+ unsweptOut = '0';
353
+ }
354
+ else
355
+ console.log(`[press] ${unsweptOut} ${cfg.pairSymbol} of fees still waiting on the ${s.phaseWord}: ${r.reason ?? 'sweep held'}`);
356
+ }
357
+ }
358
+ if (u.tokenSide > 0n)
359
+ console.log(`[press] ${fmtTok(u.tokenSide)} ${cfg.tokenSymbol} of fees wait on the hook too; only the pons operator converts those`);
360
+ const o = await tryRead('escrow', () => owed(me), { pair: 0n, token: 0n });
361
+ // a claim costs gas and posts a card, so the machine waits for the claim floor, and claims whatever waits
362
+ // at least once every `atLeastEveryHours` so small fees never sit forever
363
+ const claimFloor = parseUnits(String(cfg.claimFloor), cfg.pairDecimals);
364
+ const lastClaimTs = readLedger().presses.filter((p) => (p.kind ?? 'press') === 'press').at(-1)?.ts;
365
+ const hoursSinceClaim = lastClaimTs ? (Date.now() - new Date(lastClaimTs).getTime()) / 3_600_000 : Infinity;
366
+ const claimDue = o.pair >= minPress && (o.pair >= claimFloor || hoursSinceClaim >= cfg.claimEveryHours);
367
+ escrowOut = fmtPair(o.pair, 4);
368
+ if (o.pair >= minPress && !auto)
369
+ console.log(`[press] the escrow holds ${fmtPair(o.pair, 4)} ${cfg.pairSymbol} for the machine (claim mode manual: not claimed by the machine)`);
370
+ else if (o.pair >= minPress && !claimDue)
371
+ console.log(`[press] the escrow holds ${fmtPair(o.pair, 4)} ${cfg.pairSymbol} for the machine; claims at ${cfg.claimFloor} ${cfg.pairSymbol} or after ${cfg.claimEveryHours}h (last claim ${hoursSinceClaim === Infinity ? 'never' : hoursSinceClaim.toFixed(1) + 'h ago'})`);
372
+ else if (claimDue) {
373
+ const got = pairNum(o.pair);
374
+ commit();
375
+ const tx = await claimPair();
376
+ if (!tx)
377
+ console.log(`[press] dry: would claim ${got.toFixed(4)} ${cfg.pairSymbol} from the fee escrow`);
378
+ else {
379
+ const entry = appendPress({ kind: 'press', note: `${W.claim}: ${got.toFixed(2)} ${cfg.pairSymbol} claimed from the fee escrow.`, burnedGmerald: '0', burnGmeSpent: '0', stashedGme: '0', opsMovedGme: '0', pegStatus: 'n/a', stashTx: tx, gmeUsd: peg.fairUsd && peg.fairUsd > 0 ? peg.fairUsd : undefined, claimedGme: got.toFixed(4) });
380
+ if (cfg.cards.claim)
381
+ await postCard(ART.press, [`${cfg.emoji} ${W.claim} #${entry.k}`, `${got.toFixed(2)} ${cfg.pairSymbol} claimed from the fee escrow.`, `${Number(cfg.burnBps) / 100}% ${W.slice}s · ${Number(cfg.stashBps) / 100}% ${W.treasury} · ${Number(cfg.opsBps) / 100}% ops.`]);
382
+ console.log(`[press] claimed ${got.toFixed(4)} ${cfg.pairSymbol} from the escrow`);
383
+ }
384
+ }
385
+ else if (o.pair > 0n)
386
+ console.log(`[press] the escrow holds ${fmtPair(o.pair, 4)} ${cfg.pairSymbol} for the machine, under the ${fmtPair(minPress)} floor`);
387
+ // 1b. The buyback vest as a burn feed: a released vest credits the escrow under the launch token;
388
+ // claimed into the wallet, it goes into the burn below with everything else. The vault pays only
389
+ // its beneficiaries, and the recipient is the beneficiary: when fees point elsewhere the vest is
390
+ // not ours to release, and a refusal from the vault or the escrow is a log line, not a dead tick.
391
+ let vested = 0n;
392
+ if (cfg.vestMode === 'burn' && s.buybackEnabled && auto && mine) {
393
+ const v = await tryRead('vault', () => vestStatus(s), { releasable: 0n, escrowToken: o.token });
394
+ let credited = o.token; // what the escrow holds for the machine under the launch token, after any release
395
+ if (v.releasable > 0n) {
396
+ commit();
397
+ try {
398
+ const tx = await releaseVest();
399
+ if (tx) {
400
+ // the vault splits a release between the creator and the protocol; the escrow says what our share is
401
+ credited = (await tryRead('escrow', () => owed(me), { pair: 0n, token: credited })).token;
402
+ console.log(`[press] released the buyback vest (${fmtTok(v.releasable)} ${cfg.tokenSymbol} vault-wide); ${fmtTok(credited)} ${cfg.tokenSymbol} now credited to the machine`);
403
+ }
404
+ else
405
+ console.log(`[press] dry: would release the buyback vest, up to ${fmtTok(v.releasable)} ${cfg.tokenSymbol} vault-wide, the machine's share of it credited to the escrow`);
406
+ }
407
+ catch (e) {
408
+ console.log(`[press] the vest was not released: ${reason(e)}`);
409
+ }
410
+ }
411
+ if (credited > 0n) {
412
+ try {
413
+ const tx = await claimVestTokens();
414
+ if (tx) {
415
+ vested = credited;
416
+ console.log(`[press] claimed ${fmtTok(vested)} ${cfg.tokenSymbol} of vested buybacks; they burn with this ${W.slice}`);
417
+ }
418
+ else {
419
+ vested = credited;
420
+ console.log(`[press] dry: would claim ${fmtTok(vested)} ${cfg.tokenSymbol} of vested buybacks and burn them`);
421
+ }
422
+ }
423
+ catch (e) {
424
+ console.log(`[press] the vested tokens were not claimed: ${reason(e)}`);
425
+ }
426
+ }
427
+ }
428
+ const watched = () => ({ ...common(), feeRecipient: watch.feeRecipient, pendingRecipient: watch.pendingRecipient, unswept: unsweptOut, escrowOwed: escrowOut, claimFloor: cfg.claimFloor });
429
+ // 2. The float. Everything the wallet holds is pressed; held-back ops
430
+ // slices from earlier presses roll in naturally. A native pair keeps its gas money.
431
+ let floatBal = await pairBal(me);
432
+ if (cfg.pairNative)
433
+ floatBal = floatBal > GAS_RESERVE ? floatBal - GAS_RESERVE : 0n;
434
+ const nap = napping();
435
+ if (nap.yes) {
436
+ // Napping: no slice, no post. Presses are still logged above; the numbers refresh hourly.
437
+ if (!cfg.dry && mustWrite()) {
438
+ const n = await siteNumbers(token, stash);
439
+ writeStats({
440
+ ...watched(), queuedGme: formatUnits(floatBal, cfg.pairDecimals), sliceGme: '0', slicesLeft: 0,
441
+ burnedPct: n.burnedPct.toFixed(2), stashGme: n.stashGme.toFixed(2), gmeSunk: n.gmeSunk.toFixed(2),
442
+ status: watch.elsewhere ? 'paused: fees point elsewhere' : `napping until ${nap.until.slice(11, 16)} utc`, updatedAt: lastTs(),
443
+ });
444
+ }
445
+ console.log(`[press] napping until ${nap.until} — ${fmtPair(floatBal, 2)} ${cfg.pairSymbol} waits in the machine wallet.`);
446
+ return;
447
+ }
448
+ // The TWAP: each press works a fixed fraction of the float (default 1/6), so a
449
+ // weekly top-up drips into the pool over days instead of landing in one fill.
450
+ // Slice mode (the 15-minute TWAP): a fixed amount per run, and the tail is
451
+ // folded into the last slice rather than left as dust.
452
+ // Dip mode: read the market once (independent of our RPC) and scale the slice.
453
+ const market = await readMarket(poolIdFor(token));
454
+ const m = cfg.dipMultiplier;
455
+ let dip = '', why = '';
456
+ if (cfg.dipMode && market) {
457
+ // vsHighBps is 0 at a fresh high and negative below it. A dip: 10%+ under the recent
458
+ // high → the multiplier (2x by default). Hot: a fresh high with the last hour up 10%+ → divided by it.
459
+ const h1 = market.h1;
460
+ if (market.vsHighBps <= -cfg.dipBandBps) {
461
+ dip = 'more';
462
+ why = `${m === 2n ? 'double' : `${m}x`}: price ${(-market.vsHighBps / 100).toFixed(1)}% under its recent high`;
463
+ }
464
+ else if (market.vsHighBps === 0 && h1 >= cfg.dipBandBps / 100) {
465
+ dip = 'less';
466
+ why = `${m === 2n ? 'half' : `1/${m}`}: a fresh high, up ${h1.toFixed(1)}% in the hour`;
467
+ }
468
+ }
469
+ if (market)
470
+ console.log(`[press] market: ${market.priceGme.toExponential(3)} ${cfg.pairSymbol} per token, ${(market.vsHighBps / 100).toFixed(1)}% vs its recent high${why ? ` → ${why}` : ''}`);
471
+ let base = parseUnits(cfg.pressSliceGme, cfg.pairDecimals);
472
+ if (cfg.pressSliceUsd > 0 && peg.tokenUsd && peg.tokenUsd > 0) {
473
+ const floor = parseUnits((cfg.pressSliceUsd / peg.tokenUsd).toFixed(6), cfg.pairDecimals);
474
+ if (floor > base) {
475
+ base = floor;
476
+ console.log(`[press] slice floor: $${cfg.pressSliceUsd} = ${fmtPair(floor, 2)} ${cfg.pairSymbol} at $${peg.tokenUsd.toFixed(2)}`);
477
+ }
478
+ }
479
+ let slice = base;
480
+ if (cfg.pressSpreadHours > 0 && floatBal > 0n) {
481
+ // Batch = everything since the last press. Ticks in the window minus snacks already done.
482
+ const all = readLedger().presses;
483
+ const lastPressIdx = all.map((p) => p.kind ?? 'press').lastIndexOf('press');
484
+ const lastPress = lastPressIdx >= 0 ? all[lastPressIdx] : undefined;
485
+ const done = lastPress ? all.slice(lastPressIdx + 1).filter((p) => p.kind === 'snack').length : 0;
486
+ const total = Math.max(1, Math.round((cfg.pressSpreadHours * 60) / cfg.cadenceMin));
487
+ // A batch that already used its window (or a press the scanner has not seen yet) is treated
488
+ // as a fresh batch, never as "one tick left".
489
+ const left = done >= total ? total : total - done;
490
+ slice = floatBal / BigInt(left);
491
+ console.log(`[press] spread: ${cfg.pressSpreadHours}h = ${total} ticks, ${done} snacks done since the last press, ${left} left → ${fmtPair(slice, 2)} ${cfg.pairSymbol} each`);
492
+ }
493
+ // the dip scales whatever rule produced the slice, the spread included, and never more than the float
494
+ if (dip === 'more')
495
+ slice = slice * m;
496
+ else if (dip === 'less')
497
+ slice = slice / m;
498
+ let gmeBal = slice > 0n ? (floatBal < slice ? floatBal : slice) : (floatBal * cfg.pressFractionBps) / cfg.BPS;
499
+ if (slice > 0n && floatBal > gmeBal && floatBal - gmeBal < slice / 2n)
500
+ gmeBal = floatBal;
501
+ console.log(`[press] float: ${fmtPair(floatBal, 4)} ${cfg.pairSymbol}, pressing ${fmtPair(gmeBal, 4)} (${s.phaseWord}, dry=${cfg.dry})`);
502
+ // Rule: nothing of the launch token ever sits in this wallet. Whatever is here gets burned
503
+ // on this run, whether or not there is anything to press (a slice burns its own buy
504
+ // plus anything left over, vested buybacks included; a sweep burns leftovers alone).
505
+ // Between the curve and the pool (swept) or after a rescue there is no venue to buy in, so the
506
+ // whole slice waits rather than splitting the treasury and service legs off a burn that cannot happen.
507
+ const heldTok = await tokBal(me, token);
508
+ const noVenue = s.phase === 1 || s.phase === 3;
509
+ const sweepOnly = (gmeBal < minPress || noVenue) && heldTok > 0n;
510
+ if (noVenue && !sweepOnly) {
511
+ const where = s.phase === 3 ? 'the launch was rescued; there is no pool to burn in' : 'the curve is swept and the pool is not open yet';
512
+ if (!cfg.dry && mustWrite()) {
513
+ const n = await siteNumbers(token, stash);
514
+ writeStats({
515
+ ...watched(), queuedGme: formatUnits(floatBal, cfg.pairDecimals), sliceGme: '0', slicesLeft: 0,
516
+ burnedPct: n.burnedPct.toFixed(2), stashGme: n.stashGme.toFixed(2), gmeSunk: n.gmeSunk.toFixed(2),
517
+ status: `holding: ${where}`, updatedAt: lastTs(),
518
+ });
519
+ }
520
+ console.log(`[press] holding the whole slice: ${where}. ${fmtPair(floatBal, 2)} ${cfg.pairSymbol} waits in the machine wallet.`);
521
+ return;
522
+ }
523
+ if (gmeBal < minPress && !sweepOnly) {
524
+ // Nothing to press: no ledger row, no post. Between claims the machine naps quietly,
525
+ // and the site's numbers get a refresh about once an hour.
526
+ if (!cfg.dry && mustWrite()) {
527
+ const n = await siteNumbers(token, stash);
528
+ writeStats({
529
+ ...watched(), queuedGme: '0',
530
+ burnedPct: n.burnedPct.toFixed(2), stashGme: n.stashGme.toFixed(2), gmeSunk: n.gmeSunk.toFixed(2),
531
+ status: watch.elsewhere ? 'paused: fees point elsewhere' : 'napping between claims', updatedAt: lastTs(),
532
+ });
533
+ }
534
+ console.log('[press] nothing to press — the machine napped (no row, no post).');
535
+ return;
536
+ }
537
+ let note = why ? `pressed · ${why}` : 'pressed';
538
+ let burnedGmerald = '0';
539
+ let burnGmeSpent = '0';
540
+ let stashedGme = '0';
541
+ let opsMovedGme = '0';
542
+ let burnTx;
543
+ let stashTx;
544
+ let opsTx;
545
+ let serviceTx;
546
+ let serviceGme = '0';
547
+ const pegStatus = `${peg.status}${peg.premiumBps != null ? ` (${peg.premiumBps >= 0 ? '+' : ''}${(peg.premiumBps / 100).toFixed(2)}%)` : ''}`;
548
+ const gmeUsd = peg.fairUsd && peg.fairUsd > 0 ? peg.fairUsd : undefined;
549
+ // after each leg lands its hash is on disk, so a run that dies before the row is written is not lost
550
+ const legs = () => saveLegs({ startedAt: runAt, note, burnedGmerald, burnGmeSpent, stashedGme, opsMovedGme, pegStatus, burnTx, stashTx, opsTx, serviceTx, serviceGme, gmeUsd });
551
+ if (sweepOnly) {
552
+ note = 'burned what was waiting in the burn wallet';
553
+ }
554
+ else {
555
+ const burnAmt = (gmeBal * cfg.burnBps) / cfg.BPS;
556
+ const stashAmt = (gmeBal * cfg.stashBps) / cfg.BPS;
557
+ const serviceAmt = (gmeBal * cfg.serviceBps) / cfg.BPS;
558
+ // no ops share: the rounding dust stays in the float instead of going anywhere
559
+ const opsAmt = cfg.opsBps === 0n || !cfg.ops ? 0n : gmeBal - burnAmt - stashAmt - serviceAmt;
560
+ // 3. Burn leg: the pairing asset -> the launch token in our own venue, then burn. On the curve
561
+ // this is a direct buy; in the pool it is the v4 route. A native pair has no burn leg yet.
562
+ if (cfg.pairNative) {
563
+ console.log('[press] native-pair launches: the burn leg is next; claims and the treasury share still run');
564
+ note = 'pressed — burn leg held (native pair: the burn leg is next)';
565
+ }
566
+ else if (s.phase === 0) {
567
+ const q = await quoteBuy(s.curve, burnAmt, me);
568
+ const minOut = (q.tokensOut * (cfg.BPS - cfg.slippageBps)) / cfg.BPS;
569
+ if (cfg.dry) {
570
+ console.log(`[press] dry: would buy ~${fmtTok(q.tokensOut)} tokens with ${fmtPair(q.spent, 4)} ${cfg.pairSymbol} and burn it`);
571
+ }
572
+ else {
573
+ commit();
574
+ await waitTx(await w.writeContract({
575
+ address: cfg.gme, abi: erc20Abi, functionName: 'approve', args: [s.curve, burnAmt],
576
+ }));
577
+ burnTx = await waitTx(await w.writeContract({
578
+ address: s.curve, abi: curveAbi, functionName: 'buy', args: [burnAmt, minOut, me],
579
+ }));
580
+ burnGmeSpent = formatUnits(burnAmt, cfg.pairDecimals);
581
+ legs();
582
+ }
583
+ }
584
+ else if (s.phase === 2 && cfg.v4SwapEnabled) {
585
+ const quoted = await quoteGmeToToken(token, burnAmt);
586
+ const minOut = (quoted * (cfg.BPS - cfg.slippageBps)) / cfg.BPS;
587
+ // Sanity: the quote comes from our RPC; the market read comes from dexscreener. If they
588
+ // disagree by more than QUOTE_SANITY_BPS, something is lying and the slice is held.
589
+ if (market && market.priceGme > 0) {
590
+ const expected = pairNum(burnAmt) / market.priceGme;
591
+ const gapBps = Math.round((Number(formatUnits(quoted, 18)) / expected - 1) * 10000);
592
+ if (Math.abs(gapBps) > cfg.quoteSanityBps)
593
+ throw new Error(`quote ${fmtTok(quoted)} vs market ${expected.toFixed(0)} tokens (${gapBps} bps apart); holding this slice`);
594
+ }
595
+ if (cfg.dry) {
596
+ console.log(`[press] dry: would swap ${fmtPair(burnAmt, 4)} ${cfg.pairSymbol} -> ~${fmtTok(quoted)} tokens on the v4 pool and burn it`);
597
+ }
598
+ else {
599
+ commit();
600
+ const swapTx = await swapGmeToToken(token, burnAmt, minOut);
601
+ console.log(`[press] swapped: ${cfg.explorer}/tx/${swapTx}`);
602
+ burnGmeSpent = formatUnits(burnAmt, cfg.pairDecimals);
603
+ legs();
604
+ }
605
+ }
606
+ else {
607
+ // phase 2 with the v4 swap off (phases 1 and 3 held the whole slice above)
608
+ note = 'pressed — burn leg held (pool phase, V4_SWAP_ENABLED=0: swap+burn manually)';
609
+ }
610
+ // 4. Stash leg: already the pairing asset. One transfer, nothing to convert.
611
+ if (stashAmt === 0n) {
612
+ // burn-only wallet: nothing to stash here
613
+ }
614
+ else if (cfg.dry) {
615
+ console.log(`[press] dry: would stash ${fmtPair(stashAmt, 4)} ${cfg.pairSymbol}`);
616
+ }
617
+ else {
618
+ commit();
619
+ stashTx = await sendPair(stash, stashAmt);
620
+ stashedGme = formatUnits(stashAmt, cfg.pairDecimals);
621
+ legs();
622
+ }
623
+ // 4b. Service leg: the clockwork share. One transfer, on-chain, every slice.
624
+ if (serviceAmt > 0n) {
625
+ // belt and braces: validate() already refuses a share with no wallet to send it to
626
+ if (same(CLOCKWORK_ADDRESS, ZERO))
627
+ throw new Error('the service wallet is not pinned in this build of clockwork-press; update the package');
628
+ if (cfg.dry)
629
+ console.log(`[press] dry: would send ${fmtPair(serviceAmt, 4)} ${cfg.pairSymbol} to clockwork`);
630
+ else {
631
+ commit();
632
+ serviceTx = await sendPair(CLOCKWORK_ADDRESS, serviceAmt);
633
+ serviceGme = formatUnits(serviceAmt, cfg.pairDecimals);
634
+ legs();
635
+ }
636
+ }
637
+ // 5. Ops leg: moves only at or above fair value. Unchecked = held.
638
+ const opsOk = peg.status === 'at-or-above' || cfg.opsForce;
639
+ if (opsOk && opsAmt > 0n) {
640
+ if (cfg.dry) {
641
+ console.log(`[press] dry: would move ${fmtPair(opsAmt, 4)} ${cfg.pairSymbol} to ops`);
642
+ }
643
+ else {
644
+ commit();
645
+ opsTx = await sendPair(cfg.ops, opsAmt);
646
+ opsMovedGme = formatUnits(opsAmt, cfg.pairDecimals);
647
+ legs();
648
+ }
649
+ }
650
+ else if (opsAmt > 0n) {
651
+ console.log(`[press] ops slice held (${fmtPair(opsAmt, 4)} ${cfg.pairSymbol}): peg ${peg.status}`);
652
+ }
653
+ }
654
+ // 3b. The burn itself: whatever the wallet holds now — this run's buy, vested buybacks, anything
655
+ // that was already waiting — goes in one burn tx, so nothing of the token ever sits here.
656
+ if (cfg.dry) {
657
+ if (heldTok > 0n)
658
+ console.log(`[press] dry: would burn ${fmtTok(heldTok)} ${cfg.tokenSymbol} already held${vested > 0n ? ` (of which ${fmtTok(vested)} vested buybacks)` : ''}`);
659
+ console.log('[press] dry run complete — no ledger, no stats, no post.');
660
+ return;
661
+ }
662
+ const toBurn = await tokBal(me, token);
663
+ if (toBurn > 0n) {
664
+ commit();
665
+ const tx = await waitTx(await w.writeContract({ address: token, abi: erc20Abi, functionName: 'burn', args: [toBurn] }));
666
+ burnTx ??= tx;
667
+ burnedGmerald = fmtTok(toBurn);
668
+ // Tokens that were already waiting in the wallet (vested buybacks, a manual buy, a top-up) go into the
669
+ // same burn; say so, so the row does not read like a miracle fill.
670
+ if (!sweepOnly && heldTok > 0n)
671
+ note = `${note} · plus ${fmtTok(heldTok)} that was already waiting in the burn wallet${vested > 0n ? ` (${fmtTok(vested)} of it vested buybacks)` : ''}`;
672
+ legs();
673
+ }
674
+ // 6. Write the record the site reads.
675
+ let remaining = await pairBal(me);
676
+ if (cfg.pairNative)
677
+ remaining = remaining > GAS_RESERVE ? remaining - GAS_RESERVE : 0n;
678
+ // Slices left: on the spread, the ticks left in the window; otherwise remaining / slice.
679
+ let slicesLeft = slice > 0n ? Number((remaining + slice - 1n) / slice) : 0;
680
+ let nextSlice = slice;
681
+ if (cfg.pressSpreadHours > 0) {
682
+ const all = readLedger().presses;
683
+ const idx = all.map((p) => p.kind ?? 'press').lastIndexOf('press');
684
+ const done = idx >= 0 ? all.slice(idx + 1).filter((p) => p.kind === 'snack').length : 0;
685
+ const total = Math.max(1, Math.round((cfg.pressSpreadHours * 60) / cfg.cadenceMin));
686
+ slicesLeft = remaining >= minPress ? Math.max(1, total - done) : 0;
687
+ nextSlice = slicesLeft > 0 ? remaining / BigInt(slicesLeft) : 0n;
688
+ }
689
+ const entry = appendPress({
690
+ kind: 'snack',
691
+ note,
692
+ burnedGmerald,
693
+ burnGmeSpent: Number(burnGmeSpent).toFixed(4),
694
+ stashedGme: Number(stashedGme).toFixed(4),
695
+ opsMovedGme: Number(opsMovedGme).toFixed(4),
696
+ pegStatus,
697
+ burnTx,
698
+ stashTx,
699
+ opsTx,
700
+ serviceTx,
701
+ serviceGme: Number(serviceGme).toFixed(4),
702
+ gmeUsd,
703
+ });
704
+ saveLegs(undefined); // the row is written; nothing is in flight
705
+ const n = await siteNumbers(token, stash);
706
+ const burnedPct = n.burnedPct;
707
+ let curveOut;
708
+ if (s.phase === 0) {
709
+ const [raised, threshold] = await Promise.all([
710
+ pub.readContract({ address: s.curve, abi: curveAbi, functionName: 'realQuoteReserve' }),
711
+ pub.readContract({ address: s.curve, abi: curveAbi, functionName: 'graduationThreshold' }),
712
+ ]);
713
+ curveOut = { raisedGme: fmtPair(raised, 2), thresholdGme: fmtPair(threshold, 0), pct: threshold > 0n ? Math.min(100, Number((raised * 10000n) / threshold) / 100) : 0 };
714
+ }
715
+ // the burn counter before this snack, so a whole percent crossing gets its own post
716
+ const prevPct = Number(prevStats().burnedPct) || 0;
717
+ writeStats({
718
+ ...watched(), queuedGme: formatUnits(remaining, cfg.pairDecimals), sliceGme: fmtPair(nextSlice, 2), slicesLeft,
719
+ burnedPct: burnedPct.toFixed(2),
720
+ stashGme: n.stashGme.toFixed(2),
721
+ gmeSunk: n.gmeSunk.toFixed(2),
722
+ status: watch.elsewhere ? 'paused: fees point elsewhere' : remaining >= minPress ? `snacking. ${slicesLeft} slice${slicesLeft === 1 ? '' : 's'} of ~${fmtPair(nextSlice, 0)} ${cfg.pairSymbol} to go` : 'napping between claims',
723
+ updatedAt: entry.ts,
724
+ checkedAt: entry.ts,
725
+ curve: curveOut,
726
+ });
727
+ // 7. Say it happened. Both hashes or it didn't.
728
+ const gone = Number(String(burnedGmerald).replace(/,/g, ''));
729
+ if (cfg.cards.slice)
730
+ await postCard(ART.snack, [
731
+ `${cfg.emoji} ${W.slice} #${entry.k}`,
732
+ burnTx ? `burned ${gone.toLocaleString('en-US')} ${cfg.tokenSymbol}${Number(burnGmeSpent) > 0 ? ` with ${Number(burnGmeSpent).toFixed(2)} ${cfg.pairSymbol}` : ''}` : null,
733
+ stashTx ? `${W.treasury}: +${entry.stashedGme} ${cfg.pairSymbol}` : null,
734
+ serviceTx ? `clockwork: ${Number(serviceGme).toFixed(2)} ${cfg.pairSymbol}` : null,
735
+ opsTx ? `ops: ${entry.opsMovedGme} ${cfg.pairSymbol}` : null,
736
+ note !== 'pressed' ? note.replace('pressed · ', '') : null,
737
+ `this ${W.slice}: ${(gone / (cfg.totalSupply / 100)).toFixed(3)}% of supply`,
738
+ `burned so far: ${burnedPct.toFixed(2)}% of supply, forever`,
739
+ remaining >= minPress ? `${slicesLeft} more to go, one every ${cfg.cadenceMin} min` : 'that was the last one until the next claim',
740
+ ]);
741
+ if (cfg.cards.percent && Math.floor(burnedPct) > Math.floor(prevPct) && prevPct > 0)
742
+ await postCard(ART.burn, [`\u{1F525} ${Math.floor(burnedPct)}% of ${cfg.tokenSymbol} supply is gone.`, `${burnedPct.toFixed(2)}% exactly, bought with fees and burned.${cfg.siteUrl ? ` ${cfg.siteUrl}` : ''}`]);
743
+ console.log(`[press] snack #${entry.k} complete.`);
744
+ }