clockwork-press 0.0.1 → 0.2.0

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.
@@ -0,0 +1,121 @@
1
+ // The buy bot. Scans the pool's Swap events since the last run and posts each buy above the floor
2
+ // to the burrow: size in gme and dollars, new holder or not, market cap at that moment, the buyer,
3
+ // the hash. Runs at the end of every press run (the reliable clock); state is the last scanned
4
+ // block in site/buys-state.json, committed with the terminal's numbers.
5
+ import { readFileSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { encodeAbiParameters, formatUnits, keccak256, parseAbiItem } from 'viem';
8
+ import { cfg } from '../env.js';
9
+ import { pub } from '../chain.js';
10
+ import { postMedia, ART } from '../telegram.js';
11
+ import { checkPeg } from '../peg.js';
12
+ import { V4, poolKey } from '../v4.js';
13
+ const statePath = () => join(cfg.siteDir, 'buys-state.json');
14
+ const short = (a) => a.slice(0, 6) + '…' + a.slice(-4);
15
+ const TRANSFER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
16
+ const SEAT = 250_000;
17
+ // tokens that land here are in transit, not bought: the pool, the routers, permit2, the hook
18
+ const NOT_BUYERS = new Set(['0x8366a39cc670b4001a1121b8f6a443a643e40951', '0x8876789976decbfcbbbe364623c63652db8c0904', '0x000000000022d473030f116ddee9f6b43ac78ba3', '0xe5e702641ea86f4ae6cc3cdaed2b886f976be044', '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f', '0x39b38686a19836ac10162c490e4558e120cbbe5f', '0x8f10b468b06c6fd214b65f87778827f7d113f996']);
19
+ const erc20 = [parseAbiItem('function balanceOf(address) view returns (uint256)'), parseAbiItem('function totalSupply() view returns (uint256)')];
20
+ const money = (n) => n >= 1e6 ? `$${(n / 1e6).toFixed(2)}m` : n >= 1e3 ? `$${(n / 1e3).toFixed(0)}k` : `$${n.toFixed(0)}`;
21
+ // the wallet the bought tokens ended up in: the biggest token transfer in the tx that is not to infra
22
+ async function buyerOf(hash, token, fallback) {
23
+ try {
24
+ const rc = await pub.getTransactionReceipt({ hash });
25
+ let best = 0n, who = '';
26
+ for (const l of rc.logs) {
27
+ if (l.address.toLowerCase() !== token.toLowerCase() || l.topics[0] !== TRANSFER || l.topics.length < 3)
28
+ continue;
29
+ const to = '0x' + l.topics[2].slice(26);
30
+ const v = BigInt(l.data);
31
+ if (!NOT_BUYERS.has(to.toLowerCase()) && v > best) {
32
+ best = v;
33
+ who = to;
34
+ }
35
+ }
36
+ return who || fallback;
37
+ }
38
+ catch {
39
+ return fallback;
40
+ }
41
+ }
42
+ export async function runBuys() {
43
+ if (!cfg.token) {
44
+ console.log('[buys] TOKEN_ADDRESS not set');
45
+ return;
46
+ }
47
+ // telegram.cards.buys is the switch for this job; off means nothing scanned, nothing posted
48
+ if (!cfg.cards.buys) {
49
+ console.log('[buys] buy cards are off in clockwork.json (telegram.cards.buys); nothing posted');
50
+ return;
51
+ }
52
+ const token = cfg.token;
53
+ const k = poolKey(token);
54
+ const poolId = keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'address' }, { type: 'uint24' }, { type: 'int24' }, { type: 'address' }], [k.currency0, k.currency1, k.fee, k.tickSpacing, k.hooks]));
55
+ const latest = await pub.getBlockNumber();
56
+ let from;
57
+ try {
58
+ from = BigInt(JSON.parse(readFileSync(statePath(), 'utf8')).lastBlock) + 1n;
59
+ }
60
+ catch {
61
+ from = latest - 200n;
62
+ }
63
+ if (from > latest) {
64
+ console.log('[buys] nothing new');
65
+ return;
66
+ }
67
+ // blocks are ~0.1s apart. if the state is stale (first run, or the clock slept), only look back ~15 minutes:
68
+ // nobody wants an hour of buys dumped into the chat at once.
69
+ const MAX_BACK = 10000n;
70
+ if (latest - from > MAX_BACK) {
71
+ console.log(`[buys] state is ${latest - from} blocks behind; skipping ahead to the last ${MAX_BACK}`);
72
+ from = latest - MAX_BACK;
73
+ }
74
+ const logs = await pub.getLogs({
75
+ address: V4.poolManager,
76
+ event: parseAbiItem('event Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee)'),
77
+ args: { id: poolId }, fromBlock: from, toBlock: latest,
78
+ });
79
+ const peg = await checkPeg();
80
+ const gmeUsd = peg.tokenUsd ?? 0;
81
+ const supply = Number(formatUnits(await pub.readContract({ address: token, abi: erc20, functionName: 'totalSupply' }), 18));
82
+ const gmeIs0 = k.zeroForOne; // GME is currency0 when GME sorts first
83
+ let posted = 0;
84
+ for (const log of logs) {
85
+ const a0 = log.args.amount0, a1 = log.args.amount1;
86
+ const gmeDelta = gmeIs0 ? a0 : a1, tokDelta = gmeIs0 ? a1 : a0;
87
+ // from the swapper's view: negative = paid in, positive = received. a buy pays gme and receives $gmerald.
88
+ if (!(gmeDelta < 0n && tokDelta > 0n))
89
+ continue;
90
+ const gme = Number(formatUnits(-gmeDelta, 18)), got = Number(formatUnits(tokDelta, 18));
91
+ const usd = gme * gmeUsd;
92
+ if (usd < Number(cfg.minBuyUsd))
93
+ continue;
94
+ const tx = await pub.getTransaction({ hash: log.transactionHash });
95
+ const buyer = await buyerOf(log.transactionHash, token, tx.from);
96
+ if (buyer.toLowerCase() === String(cfg.pressWallet).toLowerCase())
97
+ continue; // the machine's own snacks are posted as snacks
98
+ let before = 0;
99
+ try {
100
+ before = Number(formatUnits(await pub.readContract({ address: token, abi: erc20, functionName: 'balanceOf', args: [buyer], blockNumber: (log.blockNumber ?? latest) - 1n }), 18));
101
+ }
102
+ catch { }
103
+ const after = before + got, fresh = before < 1;
104
+ const mcap = got > 0 && usd > 0 ? (usd / got) * supply : 0;
105
+ const hamsters = '\u{1F439}'.repeat(Math.max(1, Math.min(40, Math.floor(usd / Number(cfg.buyStepUsd)))));
106
+ const who = fresh ? 'new holder · welcome to the burrow' : `holder since before · now ${after.toLocaleString('en-US', { maximumFractionDigits: 0 })} $gmerald`;
107
+ const seat = after >= SEAT ? (fresh || before < SEAT ? ' · takes a seat on the board' : ' · a seat on the board') : '';
108
+ const text = [
109
+ hamsters,
110
+ `buy: ${gme.toFixed(3)} gme${gmeUsd ? ` ($${usd.toFixed(0)})` : ''} → ${got.toLocaleString('en-US', { maximumFractionDigits: 0 })} $gmerald`,
111
+ who + seat,
112
+ mcap > 0 ? `market cap at the time: ${money(mcap)}` : null,
113
+ `buyer ${short(buyer)} · ${cfg.explorer}/tx/${log.transactionHash}`,
114
+ `chart: https://dexscreener.com/robinhood/${poolId}`,
115
+ ].filter(Boolean).join('\n');
116
+ await postMedia(fresh ? ART.newHolder : ART.buy, text);
117
+ posted++;
118
+ }
119
+ writeFileSync(statePath(), JSON.stringify({ lastBlock: latest.toString(), scannedAt: new Date().toISOString() }));
120
+ console.log(`[buys] ${logs.length} swaps scanned, ${posted} buys posted, up to block ${latest}`);
121
+ }
@@ -0,0 +1,53 @@
1
+ // clockwork claimcheck: where the launch is, who the fees point at, and what is owed, credited, unswept
2
+ // and releasable right now, without claiming any of it.
3
+ import { formatUnits } from 'viem';
4
+ import { cfg } from '../env.js';
5
+ import { readLaunch, unswept, owed, vestStatus, trySweep } from '../pons.js';
6
+ const fmt = (v, dp = 4) => Number(formatUnits(v, cfg.pairDecimals)).toFixed(dp);
7
+ const short = (a) => `${a.slice(0, 6)}…${a.slice(-4)}`;
8
+ const when = (t) => new Date(t * 1000).toISOString().slice(0, 16).replace('T', ' ') + ' utc';
9
+ const say = (l) => console.log(`[claimcheck] ${l}`);
10
+ export async function runClaimCheck() {
11
+ if (!cfg.escrow) {
12
+ say('no fee escrow in clockwork.json (launch.kind is not pons-v2)');
13
+ return;
14
+ }
15
+ const s = await readLaunch();
16
+ if (!s.exists) {
17
+ say(`the pons factory has no record of ${cfg.token}; nothing to check`);
18
+ return;
19
+ }
20
+ const where = s.phase === 0 ? `on the curve, ${s.graduationPct?.toFixed(1) ?? '?'}% to graduation`
21
+ : s.phase === 2 ? 'graduated, trading in the pool' : s.phase === 1 ? 'swept, waiting for its pool' : 'rescued';
22
+ say(`${cfg.brandName}: ${where} (phase ${s.phase}, ${s.phaseWord})`);
23
+ const machine = cfg.pressWallet;
24
+ const isMachine = s.recipient.toLowerCase() === machine.toLowerCase();
25
+ say(`fee recipient: ${s.recipient}${isMachine ? ' — the machine wallet' : ` — not the machine wallet (${short(machine)}); fees credit the recipient, the machine only presses what it is sent`}`);
26
+ if (s.pending)
27
+ say(`pending takeover: fees move to ${s.pending.to} at ${when(s.pending.effectiveAt)} (offer expires ${when(s.pending.expiresAt)})`);
28
+ else
29
+ say('pending takeover: none');
30
+ const [mine, theirs, un, vest, sweep] = await Promise.all([
31
+ // send: false — this job reads and simulates; it never signs, whatever key or claim mode is set
32
+ owed(machine), isMachine ? null : owed(s.recipient), unswept(s), vestStatus(s), trySweep(s, { send: false }),
33
+ ]);
34
+ say(`escrow credited to the machine wallet: ${fmt(mine.pair)} ${cfg.pairSymbol}${mine.token > 0n ? ` + ${Number(formatUnits(mine.token, 18)).toFixed(0)} ${cfg.tokenSymbol} from released vests` : ''}`);
35
+ if (theirs)
36
+ say(`escrow credited to the recipient: ${fmt(theirs.pair)} ${cfg.pairSymbol}`);
37
+ const venue = s.phase === 0 ? 'the curve' : 'the hook';
38
+ if (s.phase === 0 || s.phase === 2) {
39
+ say(`unswept on ${venue}: ${fmt(un.total)} ${cfg.pairSymbol} (fee ${fmt(un.quote)} + creator tax ${fmt(un.tax)}); the creator's cut lands in the escrow at the next sweep`);
40
+ if (un.tokenSide > 0n)
41
+ say(`plus ${Number(formatUnits(un.tokenSide, 18)).toLocaleString('en-US', { maximumFractionDigits: 0 })} ${cfg.tokenSymbol} in launch-token fees that only the pons operator can convert`);
42
+ say(`self-sweep from the machine wallet: ${sweep.reason}`);
43
+ }
44
+ else
45
+ say(`unswept: nothing accrues in phase ${s.phaseWord}`);
46
+ if (s.buybackEnabled)
47
+ say(`buyback vest: ${Number(formatUnits(vest.releasable, 18)).toFixed(0)} ${cfg.tokenSymbol} releasable now, ${Number(formatUnits(vest.escrowToken, 18)).toFixed(0)} already in the escrow`);
48
+ else
49
+ say('buyback vest: buybacks are off for this launch');
50
+ const now = mine.pair;
51
+ const later = isMachine ? un.total : 0n;
52
+ say(`the machine could collect now: ${fmt(now)} ${cfg.pairSymbol} (claim mode: ${cfg.claimMode})${later > 0n ? `; up to ${fmt(later)} ${cfg.pairSymbol} more after a sweep` : ''}`);
53
+ }
@@ -0,0 +1,74 @@
1
+ // Preflight that works BEFORE the token exists: connectivity, identity,
2
+ // config completeness, integrations. Run it after every secret change.
3
+ import { formatUnits } from 'viem';
4
+ import { cfg, launched } from '../env.js';
5
+ import { pub, account } from '../chain.js';
6
+ import { erc20Abi, factoryAbi } from '../abis.js';
7
+ import { checkPeg } from '../peg.js';
8
+ import { readLaunch, unswept } from '../pons.js';
9
+ const ok = (label, detail = '') => console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`);
10
+ const warn = (label, detail = '') => console.log(` ⚠ ${label}${detail ? ` — ${detail}` : ''}`);
11
+ export async function runDoctor() {
12
+ console.log('press doctor\n');
13
+ const chainId = await pub.getChainId();
14
+ chainId === 4663 ? ok('rpc', `robinhood chain (${chainId})`) : warn('rpc', `unexpected chain id ${chainId}`);
15
+ const [sym, approved] = await Promise.all([
16
+ pub.readContract({ address: cfg.gme, abi: erc20Abi, functionName: 'symbol' }),
17
+ pub.readContract({ address: cfg.factory, abi: factoryAbi, functionName: 'approvedPairTokens', args: [cfg.gme] }),
18
+ ]);
19
+ sym === 'GME' ? ok('gme stock token', cfg.gme) : warn('gme token symbol', `read "${sym}" — verify the address`);
20
+ approved ? ok('gme approved as pons pair') : warn('gme NOT approved as pons pair anymore — re-plan before launch');
21
+ if (cfg.key) {
22
+ const me = account().address;
23
+ const [eth, gme] = await Promise.all([
24
+ pub.getBalance({ address: me }),
25
+ pub.readContract({ address: cfg.gme, abi: erc20Abi, functionName: 'balanceOf', args: [me] }),
26
+ ]);
27
+ ok('press wallet', me);
28
+ Number(formatUnits(eth, 18)) > 0.002
29
+ ? ok('gas', `${Number(formatUnits(eth, 18)).toFixed(4)} ETH`)
30
+ : warn('gas low', `${formatUnits(eth, 18)} ETH — top up for ~6 tx/press`);
31
+ ok('float', `${Number(formatUnits(gme, 18)).toFixed(4)} GME`);
32
+ }
33
+ else
34
+ warn('MACHINE_WALLET_KEY unset', 'reads only; press will fail');
35
+ launched()
36
+ ? ok('launch config', `token ${cfg.token}, stash ${cfg.stash}`)
37
+ : warn('not launched', 'TOKEN_ADDRESS / STASH_ADDRESS unset — press exits cleanly');
38
+ cfg.escrow ? ok('escrow', cfg.escrow) : warn('ESCROW_ADDRESS unset', 'claim step skipped');
39
+ cfg.ops ? ok('ops wallet', cfg.ops) : warn('no ops wallet in clockwork.json', 'ops slice held');
40
+ // pons: where the launch is, who the fees point at, and what is sitting unswept on the curve or the hook.
41
+ if (cfg.escrow) {
42
+ try {
43
+ const s = await readLaunch();
44
+ if (!s.exists)
45
+ warn('pons launch', `the factory has no record of ${cfg.token}`);
46
+ else {
47
+ ok('phase', s.phase === 0 ? `on the curve, ${s.graduationPct?.toFixed(1)}% to graduation` : `${s.phaseWord} (${s.phase})`);
48
+ s.recipient.toLowerCase() === String(cfg.pressWallet).toLowerCase()
49
+ ? ok('fee recipient', 'the machine wallet')
50
+ : warn('fee recipient is not the machine', `${s.recipient} — fees credit that address; the machine only presses what it is sent`);
51
+ if (s.pending)
52
+ warn('pending takeover', `fees move to ${s.pending.to} at ${new Date(s.pending.effectiveAt * 1000).toISOString()}`);
53
+ const un = await unswept(s);
54
+ const total = Number(formatUnits(un.total, cfg.pairDecimals));
55
+ (s.phase === 0 || s.phase === 2) && total > 0
56
+ ? warn('unswept', `${total.toFixed(4)} ${cfg.pairSymbol} on the ${s.phase === 0 ? 'curve' : 'hook'} waits for a sweep${un.tokenSide > 0n ? `, plus ${Number(formatUnits(un.tokenSide, 18)).toFixed(0)} ${cfg.tokenSymbol} the operator must convert` : ''}`)
57
+ : ok('unswept', 'nothing waiting for a sweep');
58
+ }
59
+ }
60
+ catch (e) {
61
+ warn('pons reads failed', e.shortMessage || e.message);
62
+ }
63
+ }
64
+ const peg = await checkPeg();
65
+ peg.status === 'unchecked' ? warn('peg check', peg.note) : ok('peg check', peg.note);
66
+ if (cfg.tgToken) {
67
+ const res = await fetch(`https://api.telegram.org/bot${cfg.tgToken}/getMe`);
68
+ res.ok ? ok('telegram bot') : warn('telegram', `getMe ${res.status}`);
69
+ cfg.tgChat ? ok('telegram chat', cfg.tgChat) : warn('TELEGRAM_CHAT_ID unset');
70
+ }
71
+ else
72
+ warn('telegram unset', 'presses will only log to console');
73
+ console.log('\ndone.');
74
+ }
@@ -0,0 +1,50 @@
1
+ // clockwork handback <newRecipient>: give the fees back. claims what the escrow has already credited to
2
+ // the machine wallet (a transfer does not move it), then points the creator fee recipient at the new
3
+ // address. in dry mode, or without a key, it simulates both and says what it would do.
4
+ import { formatUnits } from 'viem';
5
+ import { cfg } from '../env.js';
6
+ import { readLaunch, owed, handback, reason } from '../pons.js';
7
+ const say = (l) => console.log(`[handback] ${l}`);
8
+ export async function runHandback() {
9
+ const to = process.argv[3] ?? '';
10
+ if (!/^0x[0-9a-fA-F]{40}$/.test(to))
11
+ throw new Error('usage: clockwork handback <new recipient address>');
12
+ if (/^0x0{40}$/.test(to))
13
+ throw new Error('refusing to hand the fees to the zero address');
14
+ if (!cfg.escrow) {
15
+ say('no fee escrow in clockwork.json (launch.kind is not pons-v2); nothing to hand back');
16
+ return;
17
+ }
18
+ const s = await readLaunch();
19
+ if (!s.exists)
20
+ throw new Error(`the pons factory has no record of ${cfg.token}`);
21
+ const machine = cfg.pressWallet;
22
+ if (s.recipient.toLowerCase() !== machine.toLowerCase()) {
23
+ say(`the fee recipient is ${s.recipient}, not the machine wallet ${machine}; only the current recipient can move it. nothing to do.`);
24
+ return;
25
+ }
26
+ if (s.recipient.toLowerCase() === to.toLowerCase()) {
27
+ say(`${to} is already the recipient; nothing to do.`);
28
+ return;
29
+ }
30
+ const o = await owed(machine);
31
+ const fmt = (v) => Number(formatUnits(v, cfg.pairDecimals)).toFixed(4);
32
+ say(`credited to the machine wallet: ${fmt(o.pair)} ${cfg.pairSymbol}${o.token > 0n ? ` and ${Number(formatUnits(o.token, 18)).toFixed(0)} ${cfg.tokenSymbol}` : ''}; ${o.pair > 0n || o.token > 0n ? 'claiming first' : 'nothing to claim first'}`);
33
+ const mode = cfg.dry ? 'dry run' : cfg.key ? 'live' : 'no key';
34
+ say(`${mode}: ${cfg.dry || !cfg.key ? 'simulating' : 'sending'} the recipient move ${machine} -> ${to}`);
35
+ let r;
36
+ try {
37
+ r = await handback(to);
38
+ }
39
+ catch (e) {
40
+ throw new Error(`handback stopped: ${reason(e)}`);
41
+ }
42
+ if (r.claimed)
43
+ say(`claimed: ${cfg.explorer}/tx/${r.claimed}`);
44
+ if (r.moved)
45
+ say(`recipient moved: ${cfg.explorer}/tx/${r.moved}`);
46
+ if (!r.claimed && !r.moved)
47
+ say(`simulation passed; nothing was sent (${mode}). run without CLOCKWORK_DRY_RUN and with MACHINE_WALLET_KEY to do it.`);
48
+ else
49
+ say(`done: ${to} receives the fees from here; balances claimed before the move stay in the machine wallet.`);
50
+ }
@@ -0,0 +1,71 @@
1
+ // clockwork init <token> [--out clockwork.json] [--ops <address>]: reads the launch from the pons factory and
2
+ // writes a config with the chain facts filled in and the client's addresses left to fill. Does not touch env.ts,
3
+ // because the config file does not exist yet.
4
+ import { writeFileSync, existsSync } from 'node:fs';
5
+ import { createPublicClient, http, parseAbi } from 'viem';
6
+ import { CLOCKWORK_ADDRESS, ZERO as ZERO_ADDRESS, isHouse } from '../config.js';
7
+ const FACTORY = '0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e', ESCROW = '0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e', HOOK = '0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044', LOCKER = '0x267444D099b10fB5Ed7c3Cc7B7c767AdcA574952', VAULT = '0x42df2a798f82289E177311362e8f5ccC45c1219c';
8
+ const ZERO = '0x0000000000000000000000000000000000000000';
9
+ const FEEDS = { '0x1b0e319c6a659f002271b69db8a7df2f911c153e': '0x27C71df6A64fB476468EdF256CF72c038baB5B67' }; // pairing asset -> chainlink usd feed, when one exists
10
+ const PHASES = ['on the curve', 'swept, pool pending', 'trading in the pool', 'rescued'];
11
+ const factoryAbi = parseAbi([
12
+ 'struct LaunchedToken { address token; address curve; address deployer; address creatorFeeRecipient; address pairToken; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; uint16 creatorTaxBps; bool buybackEnabled; uint8 phase; uint256 sweptQuote; uint256 sweptTokens; uint256 sweptAt; bool exists; }',
13
+ 'function getLaunchedToken(address token) view returns (LaunchedToken)',
14
+ // the factory records decimals per quote asset; a six-decimal stable is not an eighteen-decimal stock
15
+ 'function pairTokenEconomics(address pairToken) view returns (uint256 phantomQuote, uint256 graduationThreshold, uint8 decimals)',
16
+ ]);
17
+ const erc20 = parseAbi(['function symbol() view returns (string)', 'function name() view returns (string)', 'function decimals() view returns (uint8)']);
18
+ const isAddr = (a) => /^0x[0-9a-fA-F]{40}$/.test(String(a ?? ''));
19
+ const flag = (name) => { const i = process.argv.indexOf(name); return i > -1 ? process.argv[i + 1] : undefined; };
20
+ export async function runInit() {
21
+ const token = process.argv[3];
22
+ if (!isAddr(token))
23
+ throw new Error('usage: clockwork init <token address> [--out clockwork.json] [--ops <address>]');
24
+ const out = flag('--out') || 'clockwork.json';
25
+ if (existsSync(out))
26
+ throw new Error(`${out} already exists; move it first`);
27
+ const ops = flag('--ops');
28
+ if (ops !== undefined && !isAddr(ops))
29
+ throw new Error('--ops needs an address');
30
+ const pub = createPublicClient({ transport: http(process.env.RPC_URL || 'https://rpc.mainnet.chain.robinhood.com', { retryCount: 4, retryDelay: 1000 }) });
31
+ let lt = null;
32
+ try {
33
+ lt = await pub.readContract({ address: FACTORY, abi: factoryAbi, functionName: 'getLaunchedToken', args: [token] });
34
+ }
35
+ catch { }
36
+ if (!lt?.exists)
37
+ throw new Error('this token was not launched by the pons v2 factory; pass --pair <address> support is next');
38
+ // a zero pairToken is a native launch: quoted in eth, no erc20 to ask, no usd feed to pin
39
+ const native = /^0x0{40}$/i.test(String(lt.pairToken));
40
+ const [tSym, tName] = await Promise.all([pub.readContract({ address: token, abi: erc20, functionName: 'symbol' }), pub.readContract({ address: token, abi: erc20, functionName: 'name' })]);
41
+ let pair = { address: ZERO, symbol: 'ETH', decimals: 18, native: true };
42
+ if (!native) {
43
+ const p = lt.pairToken;
44
+ const [pSym, econ] = await Promise.all([pub.readContract({ address: p, abi: erc20, functionName: 'symbol' }), pub.readContract({ address: FACTORY, abi: factoryAbi, functionName: 'pairTokenEconomics', args: [p] })]);
45
+ pair = { address: p, symbol: pSym, decimals: Number(econ[2]), ...(FEEDS[p.toLowerCase()] ? { usdFeed: FEEDS[p.toLowerCase()] } : {}) };
46
+ }
47
+ // the house does not pay itself; everyone else pays the service share, which needs the service wallet pinned in this build
48
+ const house = isHouse(token);
49
+ const unpinned = !house && CLOCKWORK_ADDRESS === ZERO_ADDRESS;
50
+ const cfg = {
51
+ version: 1, chain: { id: 4663 },
52
+ token: { address: token, symbol: tSym, name: tName, supply: 1000000000 },
53
+ pair,
54
+ launch: { kind: 'pons-v2', factory: FACTORY, feeEscrow: ESCROW, hook: HOOK, locker: LOCKER, vault: VAULT, launchBlock: 0 },
55
+ wallets: { machine: '0x_FILL_ME_the_fresh_machine_wallet', treasury: '0x_FILL_ME_your_treasury', ...(ops ? { ops } : {}) },
56
+ split: house ? { burnBps: 5000, treasuryBps: 5000, opsBps: 0, serviceBps: 0 } : { burnBps: 4500, treasuryBps: 4500, opsBps: 0, serviceBps: 1000 },
57
+ schedule: { cadenceMin: 15, weekends: true, napUntil: null },
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' },
60
+ telegram: { mode: 'shared', cards: { claim: true, slice: true, percent: true, buys: false }, buyFloorUsd: 50 },
61
+ site: { enabled: true, modules: ['treasury', 'burns', 'holders', 'seats', 'wallet', 'rank', 'ledger'], seatThreshold: 250000 },
62
+ brand: { name: tName, treasuryWord: 'treasury', burnWord: 'burn', accent: '#3DFF8E' },
63
+ };
64
+ writeFileSync(out, JSON.stringify(cfg, null, 1) + '\n');
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'}.`);
66
+ // where fees go today. the machine claims only once the founder points this at the machine wallet (claim the old balance first).
67
+ console.log(`creator fees go to ${lt.creatorFeeRecipient} right now${String(lt.creatorFeeRecipient).toLowerCase() === String(lt.deployer).toLowerCase() ? ' (the deployer)' : ''}.`);
68
+ console.log(`fill in the machine and treasury wallets${ops ? '' : ' (add wallets.ops if you want an ops share)'}, then run: clockwork dry`);
69
+ if (unpinned)
70
+ console.log('note: this build of clockwork-press has no service wallet pinned, so a file with a service share will not load until the package is updated. the house token runs with serviceBps 0.');
71
+ }
@@ -0,0 +1,82 @@
1
+ // Launch morning, one command: every pre-launch read from GO.md phase 3, plus
2
+ // the exact launchAndBuy parameters printed ready to sign. This job only
3
+ // reads the chain and prints; the founder's wallet does the signing.
4
+ import { formatUnits, toHex } from 'viem';
5
+ import { parseAbi } from 'viem';
6
+ import { cfg } from '../env.js';
7
+ import { pub } from '../chain.js';
8
+ import { readLaunch } from '../pons.js';
9
+ const launchAbi = parseAbi([
10
+ 'struct LaunchConfig { uint256 supply; uint256 curveFeeBps; uint256 phantomQuote; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; bool enabled; }',
11
+ 'function canLaunch(address) view returns (bool)',
12
+ 'function launchFee() view returns (uint256)',
13
+ 'function maxCreatorTaxBps() view returns (uint16)',
14
+ 'function launchConfigCount() view returns (uint256)',
15
+ 'function getLaunchConfig(uint256 id) view returns (LaunchConfig)',
16
+ 'function approvedPairTokens(address) view returns (bool)',
17
+ 'function pairTokenEconomics(address) view returns (uint256 phantomQuote, uint256 graduationThreshold, uint8 decimals)',
18
+ 'function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)',
19
+ ]);
20
+ const ok = (l, d = '') => console.log(` ✓ ${l}${d ? ` — ${d}` : ''}`);
21
+ const bad = (l, d = '') => console.log(` ✗ ${l}${d ? ` — ${d}` : ''}`);
22
+ export async function runLaunchCheck() {
23
+ const founder = (process.env.FOUNDER_ADDRESS ?? '');
24
+ const read = (functionName, args = []) => pub.readContract({ address: cfg.factory, abi: launchAbi, functionName, args });
25
+ console.log('launch check — gmerald, paired with gme, config 0\n');
26
+ let go = true;
27
+ // already launched? the factory record says so; the rest of this check is for the morning before.
28
+ const live = await readLaunch();
29
+ if (live.exists)
30
+ ok('already launched', `phase ${live.phase} (${live.phaseWord})${live.graduationPct != null ? `, ${live.graduationPct.toFixed(1)}% to graduation` : ''}, fees to ${live.recipient}`);
31
+ if (founder) {
32
+ const can = await read('canLaunch', [founder]);
33
+ can ? ok('canLaunch(founder)') : ((go = false), bad('canLaunch(founder)', 'gate closed for this address'));
34
+ }
35
+ else {
36
+ const can = await read('canLaunch', ['0x000000000000000000000000000000000000dEaD']);
37
+ can
38
+ ? ok('public launch gate open', 'set FOUNDER_ADDRESS to check your wallet specifically')
39
+ : ((go = false), bad('public gate closed', 'whitelist only right now — set FOUNDER_ADDRESS and re-run'));
40
+ }
41
+ const config = await read('getLaunchConfig', [0n]);
42
+ config.enabled ? ok('config 0 enabled', `curve fee ${config.curveFeeBps} bps`) : ((go = false), bad('config 0 DISABLED'));
43
+ const approved = await read('approvedPairTokens', [cfg.gme]);
44
+ approved ? ok('gme approved as pair') : ((go = false), bad('gme pair approval REVOKED'));
45
+ const econ = await read('pairTokenEconomics', [cfg.gme]);
46
+ econ[1] > 0n
47
+ ? ok('gme pair economics', `phantom ${formatUnits(econ[0], 18)} gme, graduation ${formatUnits(econ[1], 18)} gme`)
48
+ : ((go = false), bad('gme pair economics empty'));
49
+ const maxTax = await read('maxCreatorTaxBps');
50
+ Number(maxTax) >= 200 ? ok('2% creator tax under cap', `cap ${maxTax} bps`) : ((go = false), bad('cap below 200 bps'));
51
+ const [fee, pin] = await Promise.all([
52
+ read('launchFee'),
53
+ read('previewLaunchEconomics', [0n, cfg.gme]),
54
+ ]);
55
+ ok('launch fee', `${formatUnits(fee, 18)} ETH (sent as value)`);
56
+ ok('economics pin (read NOW, use NOW)', pin);
57
+ const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));
58
+ console.log(`\n${go ? 'ALL CLEAR.' : 'BLOCKED — fix the ✗ lines first.'} launchAndBuy args, ready to sign:\n`);
59
+ console.log(JSON.stringify({
60
+ params: {
61
+ name: 'Gmerald',
62
+ symbol: 'GMERALD',
63
+ logo: 'ipfs://<pin the head mark first>',
64
+ description: 'He buys GameStop and burns himself. Every 4 hours. Forever.',
65
+ socials: { twitter: 'https://x.com/gmeraldexe', telegram: 'https://t.me/GMERALDportal', discord: '', website: 'https://gmerald.xyz', farcaster: '' },
66
+ creatorFeeRecipient: founder || '<FOUNDER WALLET — never the press wallet>',
67
+ creatorTaxBps: 200,
68
+ buybackEnabled: false,
69
+ expectedEconomics: pin,
70
+ salt,
71
+ },
72
+ launchConfigId: 0,
73
+ pairToken: cfg.gme,
74
+ quoteIn: '<opening buy, in gme wei — the pre-announced size>',
75
+ minTokensOut: '<from the quoted rate, slippage-adjusted>',
76
+ recipient: founder || '<FOUNDER WALLET>',
77
+ snipeTaxExemptions: [],
78
+ }, null, 2));
79
+ console.log('\nremember: ERC-20 pair, so approve the launchAndBuy router for quoteIn first,');
80
+ console.log('and send ONLY the launch fee as value. the pin above goes stale if pons edits');
81
+ console.log('the config — re-run this check immediately before signing.');
82
+ }
@@ -0,0 +1,50 @@
1
+ // Record a MANUAL press so the site's ledger and status bar reflect it.
2
+ // Usage (from press/):
3
+ // npx tsx src/index.ts log --burned 1234567 --stashed 12.5 --burn-tx 0x.. --stash-tx 0x.. [--gme-spent 12.5] [--ops 2.7] [--note "pressed by hand"]
4
+ // Then commit and push site/press-ledger.json + site/press-stats.json.
5
+ import { formatUnits } from 'viem';
6
+ import { cfg } from '../env.js';
7
+ import { pub } from '../chain.js';
8
+ import { erc20Abi } from '../abis.js';
9
+ import { appendPress, writeStats, burnGmeTotal, totals } from '../ledger.js';
10
+ import { checkPeg } from '../peg.js';
11
+ const arg = (k, d = '') => { const i = process.argv.indexOf(`--${k}`); return i > -1 ? (process.argv[i + 1] ?? d) : d; };
12
+ export async function runLog() {
13
+ const burned = arg('burned', '0'), stashed = arg('stashed', '0'), spent = arg('gme-spent', stashed), ops = arg('ops', '0');
14
+ const peg = await checkPeg();
15
+ const entry = appendPress({
16
+ kind: arg('kind', 'press'),
17
+ note: arg('note', 'pressed by hand'),
18
+ burnedGmerald: Number(burned).toFixed(0),
19
+ burnGmeSpent: Number(spent).toFixed(4),
20
+ stashedGme: Number(stashed).toFixed(4),
21
+ opsMovedGme: Number(ops).toFixed(4),
22
+ pegStatus: peg.status,
23
+ burnTx: arg('burn-tx') || undefined,
24
+ stashTx: arg('stash-tx') || undefined,
25
+ gmeUsd: peg.fairUsd && peg.fairUsd > 0 ? peg.fairUsd : undefined,
26
+ });
27
+ let burnedPct = '0.00', stashGme = 0;
28
+ if (cfg.token) {
29
+ const supply = await pub.readContract({ address: cfg.token, abi: erc20Abi, functionName: 'totalSupply' });
30
+ burnedPct = ((Number(10n ** 27n - supply) / 1e27) * 100).toFixed(2);
31
+ }
32
+ if (cfg.stash) {
33
+ const bal = await pub.readContract({ address: cfg.gme, abi: erc20Abi, functionName: 'balanceOf', args: [cfg.stash] });
34
+ stashGme = Number(formatUnits(bal, 18));
35
+ }
36
+ else {
37
+ stashGme = totals().stashedGme;
38
+ }
39
+ const queued = await pub.readContract({ address: cfg.gme, abi: erc20Abi, functionName: 'balanceOf', args: [cfg.pressWallet] });
40
+ const slice = Number(cfg.pressSliceGme) || 20, slicesLeft = Math.ceil(Number(formatUnits(queued, 18)) / slice);
41
+ const status = Number(formatUnits(queued, 18)) >= Number(cfg.minPressGme) ? `snacking. ${slicesLeft} slice${slicesLeft === 1 ? '' : 's'} of ${slice} gme to go` : 'napping between claims';
42
+ writeStats({
43
+ presses: totals().press, snacks: totals().snack, burnedPct, stashGme: stashGme.toFixed(2),
44
+ gmeSunk: (stashGme + burnGmeTotal() + Number(cfg.gradSeedGme)).toFixed(2),
45
+ status, queuedGme: formatUnits(queued, 18), updatedAt: entry.ts, checkedAt: entry.ts,
46
+ cadenceMin: cfg.cadenceMin,
47
+ peg: { status: peg.status, premiumBps: peg.premiumBps, tokenUsd: peg.tokenUsd, fairUsd: peg.fairUsd, note: peg.note },
48
+ });
49
+ console.log(`[log] press #${entry.n} recorded. commit site/press-ledger.json + site/press-stats.json and push.`);
50
+ }
@@ -0,0 +1,45 @@
1
+ // Launch-day confidence: find a recent live pons launch and run our quote
2
+ // arithmetic against its real curve. If this prints a sane quote, the burn
3
+ // leg's math matches the chain.
4
+ import { parseAbiItem, formatUnits } from 'viem';
5
+ import { cfg } from '../env.js';
6
+ import { pub } from '../chain.js';
7
+ import { factoryAbi, curveAbi } from '../abis.js';
8
+ import { quoteBuy } from '../quote.js';
9
+ export async function runQuoteCheck() {
10
+ const latest = await pub.getBlockNumber();
11
+ const span = 200000n;
12
+ console.log(`scanning TokenLaunched over the last ${span} blocks...`);
13
+ const logs = await pub.getLogs({
14
+ address: cfg.factory,
15
+ event: parseAbiItem('event TokenLaunched(address indexed token, address indexed curve, address indexed deployer, address pairToken, uint256 launchConfigId, uint256 graduationThreshold)'),
16
+ fromBlock: latest > span ? latest - span : 0n,
17
+ toBlock: latest,
18
+ });
19
+ console.log(`found ${logs.length} launches`);
20
+ for (const log of logs.reverse()) {
21
+ const token = log.args.token;
22
+ const rec = await pub.readContract({
23
+ address: cfg.factory, abi: factoryAbi, functionName: 'getLaunchedToken', args: [token],
24
+ });
25
+ if (rec.phase !== 0)
26
+ continue;
27
+ const ready = await pub.readContract({
28
+ address: rec.curve, abi: curveAbi, functionName: 'readyToGraduate',
29
+ });
30
+ if (ready)
31
+ continue;
32
+ const oneUnit = 10n ** 18n;
33
+ const q = await quoteBuy(rec.curve, oneUnit, '0x000000000000000000000000000000000000dEaD');
34
+ console.log(`\nlive curve: ${rec.curve} (token ${token}, pair ${rec.pairToken})`);
35
+ console.log(` quote for 1.0 pair-unit in:`);
36
+ console.log(` tokens out : ${formatUnits(q.tokensOut, 18)}`);
37
+ console.log(` spent : ${formatUnits(q.spent, 18)} (refund ${formatUnits(q.refund, 18)})`);
38
+ console.log(` snipe bps : ${q.snipeBps}`);
39
+ if (q.tokensOut <= 0n)
40
+ throw new Error('quote produced zero out — investigate before launch');
41
+ console.log('\nquote math verified against a live curve.');
42
+ return;
43
+ }
44
+ console.log('no phase-0 curve found in range — widen the span or check factory activity.');
45
+ }
@@ -0,0 +1,32 @@
1
+ // Prints what the telegram bot can see: itself, and every chat it has been added to.
2
+ // Run it once after `gh secret set TELEGRAM_BOT_TOKEN` to learn the burrow's chat id
3
+ // without adding any third-party "get id" bot to the group.
4
+ import { cfg } from '../env.js';
5
+ export async function runTgCheck() {
6
+ if (!cfg.tgToken) {
7
+ console.log('[tgcheck] TELEGRAM_BOT_TOKEN not set');
8
+ return;
9
+ }
10
+ const api = (m) => fetch(`https://api.telegram.org/bot${cfg.tgToken}/${m}`).then(r => r.json());
11
+ const me = await api('getMe');
12
+ if (!me.ok) {
13
+ console.log(`[tgcheck] token rejected: ${JSON.stringify(me)}`);
14
+ return;
15
+ }
16
+ console.log(`[tgcheck] bot: @${me.result.username} (${me.result.first_name})`);
17
+ const upd = await api('getUpdates?limit=100&allowed_updates=["message","my_chat_member","channel_post"]');
18
+ const chats = new Map();
19
+ for (const u of upd.result ?? []) {
20
+ const c = u.message?.chat ?? u.my_chat_member?.chat ?? u.channel_post?.chat;
21
+ if (c)
22
+ chats.set(String(c.id), `${c.type} · ${c.title ?? c.username ?? ''}`);
23
+ }
24
+ if (chats.size === 0)
25
+ console.log('[tgcheck] no chats seen yet. add the bot to the group as an admin, post one message there, run again.');
26
+ for (const [id, d] of chats)
27
+ console.log(`[tgcheck] chat ${id} ${d}`);
28
+ if (cfg.tgChat) {
29
+ const r = await api(`getChat?chat_id=${cfg.tgChat}`);
30
+ console.log(`[tgcheck] TELEGRAM_CHAT_ID=${cfg.tgChat}: ${r.ok ? `${r.result.type} · ${r.result.title} (ok)` : JSON.stringify(r)}`);
31
+ }
32
+ }