clockwork-press 0.2.1 → 0.2.3

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/env.js CHANGED
@@ -34,7 +34,7 @@ export const cfg = {
34
34
  // a native launch is quoted in eth: the pair address is the zero address and the escrow keeps it on its native ledger.
35
35
  gme: c.pair.address, pair: c.pair.address, pairSymbol: pairNative ? 'ETH' : c.pair.symbol, pairDecimals: pairNative ? 18 : c.pair.decimals ?? 18, pairNative,
36
36
  token: c.token.address, tokenSymbol: c.token.symbol, tokenName: c.token.name, totalSupply: c.token.supply ?? 1_000_000_000,
37
- escrow: (c.launch.feeEscrow || ''), locker: (c.launch.locker || ''), claimMode: c.claim.mode, claimFloor: Number(c.claim.floor ?? 5), claimEveryHours: Number(c.claim.atLeastEveryHours ?? 24),
37
+ escrow: (c.launch.feeEscrow || ''), locker: (c.launch.locker || ''), claimMode: c.claim.mode, claimFloor: Number(c.claim.floor ?? 5), lowGasEth: Number(c.alerts?.lowGasEth ?? 0.005), claimEveryHours: Number(c.claim.atLeastEveryHours ?? 24),
38
38
  hook: (c.launch.hook || '0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044'), vault: (c.launch.vault || '0x42df2a798f82289E177311362e8f5ccC45c1219c'),
39
39
  vestMode: c.buyback?.vest ?? 'burn', paceWord: pace,
40
40
  // ops is '' when the client has no ops wallet; the ops leg then holds (and validate() refuses opsBps > 0 without one)
package/dist/index.js CHANGED
@@ -37,6 +37,10 @@ else if (job === 'log') {
37
37
  const { runLog } = await import('./jobs/log.js');
38
38
  await runLog();
39
39
  }
40
+ else if (job === 'dividends') {
41
+ const { runDividends } = await import('./jobs/dividends.js');
42
+ await runDividends();
43
+ }
40
44
  else if (job === 'holders') {
41
45
  const { readHolders } = await import('./holders.js');
42
46
  console.log(`holders: ${await readHolders()}`);
@@ -50,7 +54,7 @@ else if (job === 'launchcheck') {
50
54
  await runLaunchCheck();
51
55
  }
52
56
  else {
53
- console.error(`unknown job: ${job} (use init | press | dry | doctor | claimcheck | holders | tgcheck | log | buys | quotecheck | launchcheck | handback <address>)`);
57
+ console.error(`unknown job: ${job} (use init | press | dry | doctor | claimcheck | holders | tgcheck | log | buys | quotecheck | launchcheck | dividends | handback <address>)`);
54
58
  process.exit(1);
55
59
  }
56
60
  export {};
@@ -0,0 +1,293 @@
1
+ // clockwork dividends: the holder side. some memestocks pay their holders in the stock. this job reads every
2
+ // payout a known distributor made, groups them into drops, prices each drop at the time it landed, and writes
3
+ // a public dataset the statement page and the leaderboard read: who pays, how much, to whom, worth what.
4
+ // read-only; no key involved. the registry of distributors grows by hand and, later, from the airdrop signature.
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { createPublicClient, defineChain, fallback, formatUnits, http, parseAbi, parseAbiItem } from 'viem';
8
+ // this job runs anywhere, with no machine config and no key: its own client on the public rpc (RPC_URL to override)
9
+ const chain = defineChain({ id: 4663, name: 'Robinhood Chain', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [process.env.RPC_URL || 'https://rpc.mainnet.chain.robinhood.com'] } } });
10
+ const o = { retryCount: 6, retryDelay: 1500, timeout: 30_000 };
11
+ const pub = createPublicClient({ chain, transport: process.env.RPC_URL_2 ? fallback([http(chain.rpcUrls.default.http[0], o), http(process.env.RPC_URL_2, o)]) : http(chain.rpcUrls.default.http[0], o) });
12
+ const TOKENS = {
13
+ GME: { address: '0x1b0E319c6A659F002271B69dB8A7df2F911c153E', decimals: 18, feed: '0x27C71df6A64fB476468EdF256CF72c038baB5B67' },
14
+ USDG: { address: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168', decimals: 6 },
15
+ };
16
+ const DEFAULT_REGISTRY = [
17
+ { address: '0xe311d712bd0669896bcb47f38845ad71a641b8be', name: 'GameStop token, Diamond Drop', token: 'GME', memeToken: '0xc2362AfF2A2a4CC1f48cF3Dab2C4e2605eb94BA3', memeSymbol: 'GME (memecoin)', site: 'https://thegameneverstopped.com', note: 'hold $50+, lowest balance since the last drop is the base, a sell disqualifies, a streak multiplier to 2x' },
18
+ ];
19
+ const ev = parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)');
20
+ const feedAbi = parseAbi(['function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', 'function getRoundData(uint80 roundId) view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', 'function decimals() view returns (uint8)']);
21
+ const GAP_BLOCKS = 70000n; // payouts closer than this belong to the same drop (about two hours at ten blocks a second)
22
+ const CHUNK = 10000000n;
23
+ const readJson = (p, d) => { try {
24
+ return JSON.parse(readFileSync(p, 'utf8'));
25
+ }
26
+ catch {
27
+ return d;
28
+ } };
29
+ const writeJson = (p, v) => writeFileSync(p, JSON.stringify(v, null, 1) + '\n');
30
+ // the usd price of the pairing asset at a moment, from the chainlink feed's history: a binary search over the
31
+ // current phase's rounds, cached per job run. null when the token has no feed.
32
+ async function priceAt(sym, ts, cache) {
33
+ const t = TOKENS[sym];
34
+ if (!t?.feed)
35
+ return sym === 'USDG' ? 1 : null;
36
+ const key = `${sym}:${Math.floor(ts / 600)}`;
37
+ if (cache.has(key))
38
+ return cache.get(key);
39
+ try {
40
+ const dec = Number(await pub.readContract({ address: t.feed, abi: feedAbi, functionName: 'decimals' }));
41
+ const latest = await pub.readContract({ address: t.feed, abi: feedAbi, functionName: 'latestRoundData' });
42
+ const latestId = latest[0];
43
+ const phase = latestId >> 64n;
44
+ let lo = 1n, hi = latestId & ((1n << 64n) - 1n);
45
+ const at = async (agg) => pub.readContract({ address: t.feed, abi: feedAbi, functionName: 'getRoundData', args: [(phase << 64n) | agg] });
46
+ let best = Number(latest[1]) / 10 ** dec;
47
+ for (let i = 0; i < 40 && lo <= hi; i++) {
48
+ const mid = (lo + hi) / 2n;
49
+ let r;
50
+ try {
51
+ r = await at(mid);
52
+ }
53
+ catch {
54
+ lo = mid + 1n;
55
+ continue;
56
+ }
57
+ const upd = Number(r[3]);
58
+ if (!upd) {
59
+ lo = mid + 1n;
60
+ continue;
61
+ }
62
+ if (upd <= ts) {
63
+ best = Number(r[1]) / 10 ** dec;
64
+ lo = mid + 1n;
65
+ }
66
+ else
67
+ hi = mid - 1n;
68
+ }
69
+ cache.set(key, best);
70
+ return best;
71
+ }
72
+ catch {
73
+ cache.set(key, null);
74
+ return null;
75
+ }
76
+ }
77
+ async function marketCap(token) {
78
+ if (!token)
79
+ return null;
80
+ try {
81
+ const r = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${token}`);
82
+ const j = await r.json();
83
+ const pairs = (j?.pairs || []).filter((p) => String(p.chainId).toLowerCase().includes('robinhood'));
84
+ const best = pairs.sort((a, b) => Number(b.liquidity?.usd || 0) - Number(a.liquidity?.usd || 0))[0];
85
+ const cap = best?.marketCap ?? best?.fdv;
86
+ return cap ? Number(cap) : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ export async function runDividends() {
93
+ const dir = resolve(process.env.DIVIDENDS_DIR || 'data/dividends');
94
+ mkdirSync(join(dir, 'drops'), { recursive: true });
95
+ mkdirSync(join(dir, 'wallets'), { recursive: true });
96
+ const registry = readJson(join(dir, 'distributors.json'), DEFAULT_REGISTRY);
97
+ if (!existsSync(join(dir, 'distributors.json')))
98
+ writeJson(join(dir, 'distributors.json'), registry);
99
+ const state = readJson(join(dir, 'state.json'), {});
100
+ const index = readJson(join(dir, 'index.json'), { updatedAt: '', tokens: TOKENS, distributors: [], drops: [] });
101
+ const head = await pub.getBlockNumber();
102
+ const priceCache = new Map();
103
+ const walletShards = {};
104
+ const shardOf = (a) => a.slice(2, 4).toLowerCase();
105
+ const loadShard = (s) => (walletShards[s] ??= readJson(join(dir, 'wallets', `${s}.json`), {}));
106
+ let newDrops = 0;
107
+ for (const d of registry) {
108
+ const tok = TOKENS[d.token];
109
+ if (!tok) {
110
+ console.log(`[dividends] ${d.name}: unknown token ${d.token}, skipped`);
111
+ continue;
112
+ }
113
+ const key = `${d.address.toLowerCase()}:${d.token}`;
114
+ const fromBlock = BigInt(state[key] ?? 0) + (state[key] ? 1n : 0n);
115
+ if (fromBlock > head)
116
+ continue;
117
+ const logs = [];
118
+ for (let a = fromBlock; a <= head; a += CHUNK) {
119
+ const b = a + CHUNK - 1n > head ? head : a + CHUNK - 1n;
120
+ try {
121
+ const got = await pub.getLogs({ address: tok.address, event: ev, args: { from: d.address }, fromBlock: a, toBlock: b });
122
+ for (const l of got)
123
+ logs.push({ block: l.blockNumber, tx: l.transactionHash, to: String(l.args.to).toLowerCase(), value: l.args.value });
124
+ }
125
+ catch (e) {
126
+ console.log(`[dividends] ${d.name}: chunk ${a}-${b} failed: ${(e.shortMessage || e.message).split('\n')[0]}`);
127
+ throw e;
128
+ }
129
+ }
130
+ logs.sort((x, y) => (x.block < y.block ? -1 : x.block > y.block ? 1 : 0));
131
+ // group into drops by block gaps
132
+ const groups = [];
133
+ for (const l of logs) {
134
+ const g = groups[groups.length - 1];
135
+ if (g && l.block - g[g.length - 1].block <= GAP_BLOCKS)
136
+ g.push(l);
137
+ else
138
+ groups.push([l]);
139
+ }
140
+ for (const g of groups) {
141
+ const first = g[0].block, last = g[g.length - 1].block;
142
+ const id = `${d.address.slice(2, 10)}-${first}`;
143
+ if (index.drops.some((x) => x.id === id))
144
+ continue;
145
+ const blk = await pub.getBlock({ blockNumber: first });
146
+ const ts = Number(blk.timestamp);
147
+ // a payout is a batch: many transfers in one transaction. a transaction that pays one or two wallets is the
148
+ // distributor moving its own money (a prize, a refund, a top-up), not a dividend, and stays out of the ledger.
149
+ const perTx = {};
150
+ for (const l of g)
151
+ (perTx[l.tx] ??= new Set()).add(l.to);
152
+ const paid = g.filter((l) => perTx[l.tx].size >= 3);
153
+ const byTo = {};
154
+ for (const l of paid) {
155
+ if (l.value === 0n)
156
+ continue;
157
+ byTo[l.to] = (byTo[l.to] ?? 0n) + l.value;
158
+ }
159
+ const recips = Object.entries(byTo).filter(([, v]) => Number(formatUnits(v, tok.decimals)) >= 0.0001);
160
+ if (recips.length < 3)
161
+ continue; // three or fewer wallets is a transfer, not a drop
162
+ const total = recips.reduce((t, [, v]) => t + v, 0n);
163
+ const totalNum = Number(formatUnits(total, tok.decimals));
164
+ const price = await priceAt(d.token, ts, priceCache);
165
+ const drop = { id, distributor: d.address.toLowerCase(), name: d.name, token: tok.address, symbol: d.token, firstBlock: Number(first), lastBlock: Number(last), ts: new Date(ts * 1000).toISOString(), txs: new Set(paid.map((l) => l.tx)).size, recipients: recips.length, total: totalNum.toFixed(6), priceUsd: price, usd: price ? Number((totalNum * price).toFixed(2)) : null };
166
+ index.drops.push(drop);
167
+ newDrops++;
168
+ writeJson(join(dir, 'drops', `${id}.json`), recips.map(([to, v]) => [to, Number(formatUnits(v, tok.decimals)).toFixed(6)]));
169
+ for (const [to, v] of recips) {
170
+ const s = loadShard(shardOf(to));
171
+ (s[to] ??= []).push({ d: id, a: Number(formatUnits(v, tok.decimals)).toFixed(6) });
172
+ }
173
+ console.log(`[dividends] ${d.name}: drop ${drop.ts.slice(0, 16)} · ${totalNum.toFixed(2)} ${d.token} to ${recips.length} wallets${price ? ` · about $${(totalNum * price).toFixed(0)}` : ''}`);
174
+ }
175
+ state[key] = Number(head);
176
+ }
177
+ for (const [s, v] of Object.entries(walletShards))
178
+ writeJson(join(dir, 'wallets', `${s}.json`), v);
179
+ index.drops.sort((a, b) => a.ts.localeCompare(b.ts));
180
+ const cutoff = Date.now() - 30 * 86_400_000;
181
+ index.distributors = [];
182
+ for (const d of registry) {
183
+ const mine = index.drops.filter((x) => x.distributor === d.address.toLowerCase());
184
+ const uniq = new Set();
185
+ for (const x of mine) {
186
+ for (const [to] of readJson(join(dir, 'drops', `${x.id}.json`), []))
187
+ uniq.add(to);
188
+ }
189
+ const usd = mine.reduce((t, x) => t + (x.usd ?? 0), 0);
190
+ const usd30 = mine.filter((x) => new Date(x.ts).getTime() >= cutoff).reduce((t, x) => t + (x.usd ?? 0), 0);
191
+ const cap = await marketCap(d.memeToken);
192
+ index.distributors.push({ ...d, drops: mine.length, total: mine.reduce((t, x) => t + Number(x.total), 0).toFixed(4), usd: Number(usd.toFixed(2)), recipients: uniq.size, last: mine.at(-1)?.ts ?? null, last30dUsd: Number(usd30.toFixed(2)), marketCapUsd: cap, per100Usd30d: cap ? Number(((usd30 / cap) * 100).toFixed(4)) : null });
193
+ }
194
+ // every wallet ever paid, ranked: the leaderboard reads the top of it, a statement finds its own line for a rank
195
+ const priceOf = {};
196
+ for (const x of index.drops)
197
+ priceOf[x.id] = x.priceUsd;
198
+ const totals = {};
199
+ const tsOf = {};
200
+ for (const x of index.drops)
201
+ tsOf[x.id] = x.ts;
202
+ const shards = new Set();
203
+ for (const x of index.drops)
204
+ for (const [to] of readJson(join(dir, 'drops', `${x.id}.json`), []))
205
+ shards.add(shardOf(to));
206
+ for (const sh of shards) {
207
+ for (const [w, rows] of Object.entries(loadShard(sh))) {
208
+ for (const r of rows) {
209
+ const t = (totals[w] ??= { gme: 0, usd: 0, drops: 0, first: tsOf[r.d] ?? '', last: '' });
210
+ t.gme += Number(r.a);
211
+ t.usd += priceOf[r.d] ? Number(r.a) * priceOf[r.d] : 0;
212
+ t.drops++;
213
+ const ts = tsOf[r.d] ?? '';
214
+ if (ts && (!t.first || ts < t.first))
215
+ t.first = ts;
216
+ if (ts > t.last)
217
+ t.last = ts;
218
+ }
219
+ }
220
+ }
221
+ // in a row: counting back from each distributor's newest drop, how many paid this wallet without a miss
222
+ const paidIn = {};
223
+ for (const sh of shards)
224
+ for (const [w, rows] of Object.entries(loadShard(sh)))
225
+ paidIn[w] = new Set(rows.map((r) => r.d));
226
+ // a round is a distributor's drops on one utc day; paid in any drop of the day counts as paid in the round
227
+ const rounds = {};
228
+ for (const x of index.drops) {
229
+ const r = (rounds[x.distributor] ??= []);
230
+ const day = x.ts.slice(0, 10);
231
+ const last = r[r.length - 1];
232
+ if (last && last.day === day)
233
+ last.push(x.id);
234
+ else {
235
+ const g = [x.id];
236
+ g.day = day;
237
+ r.push(g);
238
+ }
239
+ }
240
+ const streakOf = (w) => Math.max(0, ...Object.values(rounds).map((rs) => { let n = 0; for (let i = rs.length - 1; i >= 0; i--) {
241
+ if (rs[i].some((id) => paidIn[w]?.has(id)))
242
+ n++;
243
+ else
244
+ break;
245
+ } return n; }));
246
+ const holders = Object.entries(totals).sort((a, b) => b[1].usd - a[1].usd || b[1].gme - a[1].gme).map(([w, t]) => [w, t.gme.toFixed(6), Number(t.usd.toFixed(2)), t.drops, t.first.slice(0, 10), t.last.slice(0, 10), streakOf(w)]);
247
+ writeJson(join(dir, 'holders.json'), { updatedAt: new Date().toISOString(), count: holders.length, rows: holders });
248
+ // the page loads the top of the book and a wallet's rank from small shards; the full ranking only on request
249
+ writeJson(join(dir, 'top.json'), { updatedAt: new Date().toISOString(), count: holders.length, rows: holders.slice(0, 500) });
250
+ mkdirSync(join(dir, 'ranks'), { recursive: true });
251
+ const rankShards = {};
252
+ holders.forEach((r, i) => { (rankShards[shardOf(String(r[0]))] ??= {})[String(r[0])] = i + 1; });
253
+ for (const [sh, m] of Object.entries(rankShards))
254
+ writeJson(join(dir, 'ranks', `${sh}.json`), m);
255
+ // and one ranked book per distributor, so a token's own page ranks its own holders
256
+ for (const d of registry) {
257
+ const dl = d.address.toLowerCase();
258
+ const mine = new Set(index.drops.filter((x) => x.distributor === dl).map((x) => x.id));
259
+ if (!mine.size)
260
+ continue;
261
+ const per = {};
262
+ for (const sh of shards)
263
+ for (const [w, rows] of Object.entries(loadShard(sh)))
264
+ for (const r of rows) {
265
+ if (!mine.has(r.d))
266
+ continue;
267
+ const t = (per[w] ??= { gme: 0, usd: 0, drops: 0, first: '', last: '' });
268
+ t.gme += Number(r.a);
269
+ t.usd += priceOf[r.d] ? Number(r.a) * priceOf[r.d] : 0;
270
+ t.drops++;
271
+ const ts = tsOf[r.d] ?? '';
272
+ if (ts && (!t.first || ts < t.first))
273
+ t.first = ts;
274
+ if (ts > t.last)
275
+ t.last = ts;
276
+ }
277
+ const rs = rounds[dl] ?? [];
278
+ const streak = (w) => { let n = 0; for (let i = rs.length - 1; i >= 0; i--) {
279
+ if (rs[i].some((id) => paidIn[w]?.has(id)))
280
+ n++;
281
+ else
282
+ break;
283
+ } return n; };
284
+ const rows = Object.entries(per).sort((a, b) => b[1].usd - a[1].usd || b[1].gme - a[1].gme).map(([w, t]) => [w, t.gme.toFixed(6), Number(t.usd.toFixed(2)), t.drops, t.first.slice(0, 10), t.last.slice(0, 10), streak(w)]);
285
+ writeJson(join(dir, `holders-${dl.slice(2, 10)}.json`), { updatedAt: new Date().toISOString(), distributor: dl, count: rows.length, rows });
286
+ }
287
+ index.distributors.sort((a, b) => b.usd - a.usd);
288
+ index.tokens = TOKENS;
289
+ index.updatedAt = new Date().toISOString();
290
+ writeJson(join(dir, 'index.json'), index);
291
+ writeJson(join(dir, 'state.json'), state);
292
+ console.log(`[dividends] ${index.drops.length} drops from ${registry.length} distributor(s), ${newDrops} new, through block ${head}`);
293
+ }
package/dist/jobs/init.js CHANGED
@@ -56,10 +56,10 @@ export async function runInit() {
56
56
  split: house ? { burnBps: 5000, treasuryBps: 5000, opsBps: 0, serviceBps: 0 } : { burnBps: 4500, treasuryBps: 4500, opsBps: 0, serviceBps: 1000 },
57
57
  schedule: { cadenceMin: 15, weekends: true, napUntil: null },
58
58
  slices: { rule: { kind: 'pace', pace: 'steady' }, dip: { on: false, bandBps: 1000, multiplier: 2 }, quoteSanityBps: 1500, maxSlippageBps: 300 },
59
- burn: { method: 'burn' }, buyback: { vest: 'burn' }, claim: { mode: 'auto', floor: pair.symbol === 'ETH' || pair.symbol === 'WETH' ? 0.05 : 5, atLeastEveryHours: 24 },
60
- telegram: { mode: 'shared', cards: { claim: true, slice: true, percent: true, buys: false }, buyFloorUsd: 50 },
59
+ burn: { method: 'burn' }, buyback: { vest: 'burn' }, claim: { mode: 'auto', floor: ['ETH', 'WETH'].includes(String(pair.symbol)) ? 0.05 : ['USDG', 'USDC', 'USDT', 'DAI', 'USD1'].includes(String(pair.symbol).toUpperCase()) ? 100 : 5, atLeastEveryHours: 24 },
60
+ telegram: { mode: 'own', chatId: '', cards: { claim: true, slice: true, percent: true, buys: false }, buyFloorUsd: 50 }, // own bot until the shared clockwork bot exists; chatId from `clockwork tgcheck`
61
61
  site: { enabled: true, modules: ['treasury', 'burns', 'holders', 'seats', 'wallet', 'rank', 'ledger'], seatThreshold: 250000 },
62
- brand: { name: tName, treasuryWord: 'treasury', burnWord: 'burn', accent: '#3DFF8E' },
62
+ brand: { name: tName, treasuryWord: 'treasury', burnWord: 'burn', accent: '#0B6E4F', avatar: '' },
63
63
  };
64
64
  writeFileSync(out, JSON.stringify(cfg, null, 1) + '\n');
65
65
  console.log(`wrote ${out}: ${tName} (${tSym}) paired with ${pair.symbol}${native ? ' (native)' : ` at ${pair.address}`}, ${PHASES[Number(lt.phase)] ?? `phase ${lt.phase}`}, buybacks ${lt.buybackEnabled ? 'on' : 'off'}.`);
package/dist/press.js CHANGED
@@ -108,6 +108,31 @@ async function scanStash(fairUsd) {
108
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
109
  added++;
110
110
  }
111
+ // the treasury-outflow watch: the treasury only grows; any transfer out of it is news, posted once per hash
112
+ try {
113
+ const outs = await pub.getLogs({
114
+ address: cfg.gme, event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
115
+ args: { from: cfg.stash }, fromBlock: from, toBlock: latest,
116
+ });
117
+ for (const log of outs) {
118
+ const amt = log.args.value;
119
+ if (amt === 0n)
120
+ continue;
121
+ const l = readLedger();
122
+ l.meta = { ...(l.meta ?? {}) };
123
+ const seen = l.meta.treasuryOutflows ?? [];
124
+ if (seen.some((o) => o.tx === log.transactionHash))
125
+ continue;
126
+ const rec = { tx: log.transactionHash, amount: Number(formatUnits(amt, cfg.pairDecimals)).toFixed(4), to: String(log.args.to), at: new Date().toISOString() };
127
+ l.meta.treasuryOutflows = [...seen, rec].slice(-20);
128
+ writeLedger(l);
129
+ console.log(`[press] the ${W.treasury} moved: ${rec.amount} ${cfg.pairSymbol} left it to ${rec.to.slice(0, 6)}…${rec.to.slice(-4)} (${rec.tx})`);
130
+ await postCard('', [`${cfg.emoji} the ${W.treasury} moved`, `${rec.amount} ${cfg.pairSymbol} left the ${W.treasury} to ${rec.to.slice(0, 6)}…${rec.to.slice(-4)}.`, `if this was the founder, nothing to do. if not, look now.`]);
131
+ }
132
+ }
133
+ catch (e) {
134
+ console.log(`[press] outflow watch skipped: ${e.shortMessage || e.message}`);
135
+ }
111
136
  const l2 = readLedger();
112
137
  l2.stashScanBlock = latest.toString();
113
138
  writeLedger(l2);
@@ -303,8 +328,12 @@ async function pressOnce(runAt, commit) {
303
328
  // what every stats write carries this run; the recipient watch and unswept are filled in once known.
304
329
  // the recipient row is red whenever an auto-mode machine is not where the fees point, key or no key.
305
330
  // lastRunAt is whatever the file holds: markRun sets it when a run commits to work, never a napping tick.
331
+ // supply locked at graduation sits in the pons locker and never comes out: out of circulation, like a burn
332
+ const lockedTokens = cfg.locker ? await tryRead('locker', () => pub.readContract({ address: token, abi: erc20Abi, functionName: 'balanceOf', args: [cfg.locker] }), 0n) : 0n;
333
+ const locked = lockedTokens > 0n ? { tokens: formatUnits(lockedTokens, 18), pctOfMint: ((Number(lockedTokens) / Number(TOTAL_SUPPLY)) * 100).toFixed(2) } : null;
306
334
  const common = () => ({
307
335
  presses: countKind('press'), snacks: countKind('snack'), checkedAt: new Date().toISOString(), peg: pegOut,
336
+ locked, treasuryOutflow: readLedger().meta?.treasuryOutflows?.at(-1) ?? null,
308
337
  cadenceMin: cfg.cadenceMin, days: dailyRollup(), holders, lastRunAt: prevStats().lastRunAt,
309
338
  phase: s.phaseWord, graduationPct: s.graduationPct,
310
339
  feeRecipient: { address: s.recipient, ok: cfg.claimMode !== 'auto' || same(s.recipient, cfg.pressWallet) }, pendingRecipient: null,
@@ -327,6 +356,26 @@ async function pressOnce(runAt, commit) {
327
356
  }
328
357
  const me = account().address;
329
358
  const w = wallet();
359
+ // one card a day when the machine wallet runs low on gas; the number rides in stats for the status page
360
+ let gasEth = '0', gasLow = false;
361
+ try {
362
+ const gas = await pub.getBalance({ address: me });
363
+ gasEth = Number(formatUnits(gas, 18)).toFixed(4);
364
+ gasLow = gas < parseUnits(String(cfg.lowGasEth), 18);
365
+ if (gasLow) {
366
+ const l = readLedger();
367
+ const last = l.meta?.lowGasAlertAt ? new Date(l.meta.lowGasAlertAt).getTime() : 0;
368
+ console.log(`[press] gas low: ${gasEth} eth in the machine wallet (floor ${cfg.lowGasEth})`);
369
+ if (Date.now() - last > 86_400_000 && !cfg.dry) {
370
+ l.meta = { ...(l.meta ?? {}), lowGasAlertAt: new Date().toISOString() };
371
+ writeLedger(l);
372
+ await postCard('', [`${cfg.emoji} gas is low`, `the machine wallet holds ${gasEth} eth. a few more ticks and it cannot burn.`, `send a little eth to ${me.slice(0, 6)}…${me.slice(-4)}.`]);
373
+ }
374
+ }
375
+ }
376
+ catch (e) {
377
+ console.log(`[press] gas check skipped: ${e.shortMessage || e.message}`);
378
+ }
330
379
  // 0b. The recipient watch, then the claim block.
331
380
  const watch = await watchRecipient(s, me);
332
381
  const mine = same(s.recipient, me);
@@ -425,7 +474,7 @@ async function pressOnce(runAt, commit) {
425
474
  }
426
475
  }
427
476
  }
428
- const watched = () => ({ ...common(), feeRecipient: watch.feeRecipient, pendingRecipient: watch.pendingRecipient, unswept: unsweptOut, escrowOwed: escrowOut, claimFloor: cfg.claimFloor });
477
+ const watched = () => ({ ...common(), feeRecipient: watch.feeRecipient, pendingRecipient: watch.pendingRecipient, unswept: unsweptOut, escrowOwed: escrowOut, claimFloor: cfg.claimFloor, gasEth, gasLow });
429
478
  // 2. The float. Everything the wallet holds is pressed; held-back ops
430
479
  // slices from earlier presses roll in naturally. A native pair keeps its gas money.
431
480
  let floatBal = await pairBal(me);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clockwork-press",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "clockwork-press",
9
- "version": "0.2.0",
9
+ "version": "0.2.3",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "dependencies": {
12
12
  "viem": "2.56.3"
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "clockwork-press",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "your fees, on a clock, with receipts. the fee machine for tokens on robinhood chain.",
6
+ "description": "your fees, on a clock, with receipts. ClockWorks, the fee machine for tokens on robinhood chain.",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
9
9
  "type": "git",
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: clockwork
3
- description: Set up and run a Clockwork machine for a pons v2 token on Robinhood Chain. Use when a founder asks to automate their creator fees, buy back and burn, build a treasury, or "run Clockwork" for their token. Walks the human through the machine wallet, the config, the repository, the secrets, the dry run, pointing fees at the machine, and going live. Never touches a private key.
3
+ description: Set up and run a ClockWorks machine for a pons v2 token on Robinhood Chain. Use when a founder asks to automate their creator fees, buy back and burn, build a treasury, or "run ClockWorks" for their token. Walks the human through the machine wallet, the config, the repository, the secrets, the dry run, pointing fees at the machine, and going live. Never touches a private key.
4
4
  ---
5
5
 
6
- # Clockwork, with an assistant next to you
6
+ # ClockWorks, with an assistant next to you
7
7
 
8
- Clockwork runs a token's creator fees on a clock: claims from the pons fee escrow, splits by percentages the
8
+ ClockWorks runs a token's creator fees on a clock: claims from the pons fee escrow, splits by percentages the
9
9
  founder sets once, buys the token back and burns it in slices, sends the treasury share to a wallet that only
10
10
  grows, and prints every hash. It runs in the founder's own GitHub repository on a wallet the founder created.
11
11
  Docs: https://gmerald.xyz/clockwork/docs/
@@ -23,15 +23,16 @@ Docs: https://gmerald.xyz/clockwork/docs/
23
23
  1. The token address (a pons v2 launch on Robinhood Chain, chain id 4663).
24
24
  2. A fresh machine wallet address, created on their device, with about 0.02 ETH on Robinhood Chain.
25
25
  3. A treasury wallet address (a cold wallet is best).
26
- 4. The split they want. Default: 45 burn / 45 treasury / 0 ops / 10 Clockwork. The Clockwork share is at
26
+ 4. The split they want. Default: 45 burn / 45 treasury / 0 ops / 10 ClockWorks. The ClockWorks share is at
27
27
  least 10 and is what pays for the software.
28
28
  5. The pace: gentle (a claim over a day), steady (over six hours, the default), or once (one slice).
29
- 6. Telegram: their own bot token set as a secret (they create the bot in @BotFather), or off for now.
30
- 7. How often to claim: `claim.floor` is how much of the pairing asset must be waiting in the escrow before the machine claims (default 5), and `claim.atLeastEveryHours` claims whatever waits at least that often (default 24). A claim costs gas and posts a card; most machines keep the defaults.
29
+ 6. Telegram: their own bot token set as a secret (they create the bot in @BotFather) and the group's chat id from `clockwork tgcheck`, or `telegram.mode` set to `off` for now. The config defaults to `own`; with no token set, the machine logs and posts nothing.
30
+ 7. The brand: `brand.name`, the words for treasury and burn (`brand.treasuryWord`, `brand.burnWord`, `brand.claimWord`), `brand.avatar` (an image URL the status page and the wallet card use), and `brand.art` (one image or short mp4 URL per Telegram card).
31
+ 8. How often to claim: `claim.floor` is how much of the pairing asset must be waiting in the escrow before the machine claims (default 5), and `claim.atLeastEveryHours` claims whatever waits at least that often (default 24). A claim costs gas and posts a card; most machines keep the defaults.
31
32
 
32
33
  ## The steps
33
34
  1. **Write the config.** In a terminal with Node 20 or newer, in an empty folder:
34
- `npx --yes clockwork-press@0.2.0 init <token address>`
35
+ `npx --yes clockwork-press@0.2.2 init <token address>`
35
36
  It reads the launch from the pons factory and writes `clockwork.json`. Fill `wallets.machine`,
36
37
  `wallets.treasury`, the `split`, and `slices.rule` (`{ "kind": "pace", "pace": "steady" }`). If they want
37
38
  Telegram, set `telegram.mode` to `own` and `telegram.chatId` to their group id. Do not put a key or a token
@@ -47,12 +48,14 @@ Docs: https://gmerald.xyz/clockwork/docs/
47
48
  the float, and `dry: would swap …`. The machine refuses to run if the key belongs to a different wallet
48
49
  than `wallets.machine`; that is the guard working, not a bug.
49
50
  5. **Point the fees at the machine.** First claim what is already owed to the current recipient
50
- (`npx --yes clockwork-press@0.2.0 claimcheck` from the folder with `clockwork.json` prints it), because a
51
+ (`npx --yes clockwork-press@0.2.2 claimcheck` from the folder with `clockwork.json` prints it), because a
51
52
  recipient change does not move credited balances. Then the current recipient signs
52
53
  `transferCreatorFeeRecipient(token, machineWallet)` on the pons factory
53
54
  `0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e`. Prepare the calldata for them (function selector
54
55
  `0x2931861b`, then the token address and the machine address each left-padded to 32 bytes) and tell them
55
- to send it as a raw transaction from that wallet with value 0. The pons UI does not show this function;
56
+ to send it as a raw transaction from that wallet with value 0, or point them at
57
+ https://gmerald.xyz/clockwork/point/?token=<token>&machine=<machine>, which reads the chain, shows what is
58
+ owed, and prepares the same transaction for their own wallet. The pons UI does not show this function;
56
59
  the contract has it and it takes effect immediately.
57
60
  6. **Go live.** Set `claim.mode` to `auto` in `clockwork.json` if it is not already, commit, and let the
58
61
  workflow's schedule run it on the quarter hour. Optional: a cron-job.org job that POSTs to
@@ -73,9 +76,9 @@ Docs: https://gmerald.xyz/clockwork/docs/
73
76
 
74
77
  ## Commands
75
78
  `init <token>` write the config from the chain · `dry` a tick without signing · `press` the tick ·
76
- `claimcheck` what the escrow holds and what is unswept · `doctor` RPC, wallet, gas, recipient, Telegram ·
79
+ `claimcheck` what the escrow holds and what is unswept · `dividends` (0.2.3) reads every payout the listed memestock distributors made to holders into a public book, no key, any folder, `DIVIDENDS_DIR` names where · `doctor` RPC, wallet, gas, recipient, Telegram ·
77
80
  `handback <address>` claim what is credited, then move the fee recipient · `holders` the holder count.
78
81
 
79
- ## What Clockwork costs
80
- Ten percent of every claim, sent on chain by the machine, slice by slice, to the Clockwork wallet pinned in
82
+ ## What ClockWorks costs
83
+ Ten percent of every claim, sent on chain by the machine, slice by slice, to the ClockWorks wallet pinned in
81
84
  the package (`0x6EA62Bd07FE08C7491543d495B42F6dA7ad298D0`). No setup fee. The chain is the invoice.