Docs: the proof, the parameters, and the code.
This machine asks for zero trust. Every transaction it signs is public, every rule it follows is on this page, and the exact source code it runs is embedded below at build time, straight from the files themselves. If something here disagrees with the chain, believe the chain.
How to verify a cycle
Everything the machine does is signed by one wallet. Follow it on Solscan and you can reconstruct every cycle without touching this site.
- The claim. Each cycle starts with a
collectCreatorFeeinstruction against the pump.fun program. On Solscan you'll see SOL flow from the fee vault into the machine wallet. The ledger row links this signature. - The buy. Minutes later the wallet routes that SOL through Jupiter into $PUMP. Check the swap's output mint matches the official PUMP address below, and that the input is the full spendable balance, not a slice of it.
- The airdrop. Batched transactions each carry up to five PUMP transfers to holder wallets. Sum the transfers across the batch signatures and you get the airdrop total the broadcast announced.
- The split. Pick any recipient, divide their $SHEKELS balance by the snapshot supply of all eligible holders, and their PUMP share should match, to the rounding dust, which stays in the wallet for the next cycle.
Live parameters
Read directly from the running worker, not from a config file that might be stale. These are the only knobs that exist; there is no admin panel behind them.
The code that runs
Not a summary: these are the six modules of the worker, embedded into this page at build time from the same files the machine executes. About 500 lines total; small enough to read in one sitting, which is the point.
The cyclesrc/worker/cycle.ts
The orchestrator. Runs the four stations in order, records every result, and broadcasts each receipt.
import { config, fmtSol, fmtToken } from "../lib/config";
import { startCycle, updateCycle } from "../lib/db";
import { claimCreatorFees } from "../lib/claim";
import { swapSolToPump } from "../lib/swap";
import { snapshotHolders } from "../lib/holders";
import { airdropPump, computeShares } from "../lib/airdrop";
import { broadcast } from "../lib/broadcast";
import { creator, solBalance } from "../lib/solana";
const PUMP_DECIMALS = 6;
/**
* One full flywheel cycle:
* 1. claim creator fees (SOL)
* 2. swap claimable SOL into PUMP
* 3. snapshot coin holders
* 4. airdrop PUMP pro rata
* Every step is recorded and broadcast with its tx signature.
*/
export async function runCycle(): Promise<void> {
const cycleId = startCycle(config.dryRun);
const t = new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
console.log(`\n=== cycle #${cycleId} @ ${t} (dry_run=${config.dryRun}) ===`);
try {
// 1. Claim
const claim = await claimCreatorFees();
updateCycle(cycleId, {
claim_sig: claim.sig ?? "",
claimed_lamports: claim.claimedLamports.toString(),
});
if (claim.sig) {
await broadcast(
cycleId,
"claim",
`Creator fees claimed: ${fmtSol(claim.claimedLamports)} SOL at ${t}`,
claim.sig
);
}
// Decide how much SOL is actually spendable this cycle. We use the
// wallet's whole balance minus the gas reserve, not just this claim,
// so any SOL left over from failed or skipped cycles gets swept in.
const balance = await solBalance(creator().publicKey);
const spendable = balance > config.gasReserveLamports ? balance - config.gasReserveLamports : 0n;
if (spendable < config.minClaimLamports) {
updateCycle(cycleId, { status: "skipped", finished_at: Date.now() });
await broadcast(
cycleId,
"skip",
`Cycle #${cycleId}: fees below threshold (${fmtSol(spendable)} SOL spendable). Rolling into next cycle.`
);
return;
}
// 2. Swap SOL -> PUMP
const swap = await swapSolToPump(spendable);
updateCycle(cycleId, { swap_sig: swap.sig ?? "", pump_raw: swap.pumpRaw.toString() });
if (swap.sig) {
await broadcast(
cycleId,
"swap",
`Bought ${fmtToken(swap.pumpRaw, PUMP_DECIMALS)} PUMP with ${fmtSol(spendable)} SOL`,
swap.sig
);
}
// 3. Snapshot holders
const holders = await snapshotHolders();
updateCycle(cycleId, { holders_count: holders.length });
console.log(`[holders] ${holders.length} eligible holders`);
if (holders.length === 0 || swap.pumpRaw === 0n) {
updateCycle(cycleId, { status: "skipped", finished_at: Date.now() });
await broadcast(cycleId, "skip", `Cycle #${cycleId}: nothing to distribute.`);
return;
}
// 4. Airdrop
const shares = computeShares(holders, swap.pumpRaw);
const drop = await airdropPump(shares);
updateCycle(cycleId, {
recipients_count: drop.recipients,
airdrop_sigs: JSON.stringify(drop.sigs),
status: "done",
finished_at: Date.now(),
});
await broadcast(
cycleId,
"airdrop",
`Airdropped ${fmtToken(drop.distributedRaw, PUMP_DECIMALS)} PUMP to ${drop.recipients} holders across ${drop.sigs.length || (config.dryRun ? 0 : 1)} transactions`,
drop.sigs[0]
);
} catch (err: any) {
console.error(`[cycle #${cycleId}] error:`, err);
updateCycle(cycleId, {
status: "error",
error: String(err?.message ?? err),
finished_at: Date.now(),
});
await broadcast(cycleId, "error", `Cycle #${cycleId} hit an error and will retry next interval.`);
}
}
Station 1: claimsrc/lib/claim.ts
Builds the collectCreatorFee transaction through PumpPortal's local API and signs it here. The claimed amount is read back from the confirmed transaction's balance delta.
import { VersionedTransaction } from "@solana/web3.js";
import { config } from "./config";
import { connection, creator, signAndSend, solBalance } from "./solana";
export interface ClaimResult {
sig: string | null;
claimedLamports: bigint;
}
/**
* Claim accumulated pump.fun creator fees via the PumpPortal Local
* Transaction API. The API builds an unsigned collectCreatorFee
* transaction; we sign locally and submit it ourselves, so the key
* never leaves this machine.
*/
export async function claimCreatorFees(): Promise<ClaimResult> {
const wallet = creator();
const before = await solBalance(wallet.publicKey);
if (config.dryRun) {
console.log("[claim] DRY_RUN: skipping collectCreatorFee submission");
return { sig: null, claimedLamports: 0n };
}
const resp = await fetch("https://pumpportal.fun/api/trade-local", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
publicKey: wallet.publicKey.toBase58(),
action: "collectCreatorFee",
priorityFee: 0.000001,
pool: "pump",
}),
});
if (resp.status !== 200) {
const text = await resp.text();
throw new Error(`PumpPortal collectCreatorFee failed (${resp.status}): ${text}`);
}
const data = new Uint8Array(await resp.arrayBuffer());
const tx = VersionedTransaction.deserialize(data);
const sig = await signAndSend(tx, "collectCreatorFee");
// Read the actual claimed amount from the confirmed transaction's
// balance delta rather than trusting a wallet re-read (other activity
// could land between reads).
let claimed = 0n;
const parsed = await connection().getTransaction(sig, {
maxSupportedTransactionVersion: 0,
commitment: "confirmed",
});
if (parsed?.meta) {
const keys = parsed.transaction.message.getAccountKeys().staticAccountKeys;
const idx = keys.findIndex((k) => k.equals(wallet.publicKey));
if (idx >= 0) {
const delta =
BigInt(parsed.meta.postBalances[idx]) - BigInt(parsed.meta.preBalances[idx]);
claimed = delta > 0n ? delta : 0n;
}
}
if (claimed === 0n) {
// Fallback: wallet balance delta.
const after = await solBalance(wallet.publicKey);
claimed = after > before ? after - before : 0n;
}
return { sig, claimedLamports: claimed };
}
Station 2: convertsrc/lib/swap.ts
Quotes and executes the SOL to PUMP swap through Jupiter. What actually arrived is measured from the wallet's PUMP balance change, not the quote.
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
import { config, SOL_MINT } from "./config";
import { creator, signAndSend, tokenBalanceRaw } from "./solana";
const JUP = "https://lite-api.jup.ag/swap/v1";
export interface SwapResult {
sig: string | null;
pumpRaw: bigint; // PUMP actually received (raw units)
}
/** Swap `lamports` of SOL into PUMP via Jupiter. */
export async function swapSolToPump(lamports: bigint): Promise<SwapResult> {
const wallet = creator();
const pumpMint = new PublicKey(config.pumpMint);
const quoteUrl =
`${JUP}/quote?inputMint=${SOL_MINT}&outputMint=${config.pumpMint}` +
`&amount=${lamports.toString()}&slippageBps=${config.slippageBps}&restrictIntermediateTokens=true`;
const quoteResp = await fetch(quoteUrl);
if (!quoteResp.ok) {
throw new Error(`Jupiter quote failed (${quoteResp.status}): ${await quoteResp.text()}`);
}
const quote = await quoteResp.json();
if (config.dryRun) {
console.log(`[swap] DRY_RUN: would swap ${lamports} lamports for ~${quote.outAmount} PUMP raw`);
return { sig: null, pumpRaw: BigInt(quote.outAmount ?? 0) };
}
const before = await tokenBalanceRaw(wallet.publicKey, pumpMint);
const swapResp = await fetch(`${JUP}/swap`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
quoteResponse: quote,
userPublicKey: wallet.publicKey.toBase58(),
wrapAndUnwrapSol: true,
dynamicComputeUnitLimit: true,
dynamicSlippage: true,
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: { maxLamports: 2_000_000, priorityLevel: "high" },
},
}),
});
if (!swapResp.ok) {
throw new Error(`Jupiter swap build failed (${swapResp.status}): ${await swapResp.text()}`);
}
const { swapTransaction } = await swapResp.json();
const tx = VersionedTransaction.deserialize(Buffer.from(swapTransaction, "base64"));
const sig = await signAndSend(tx, "jupiter swap");
const after = await tokenBalanceRaw(wallet.publicKey, pumpMint);
const received = after > before ? after - before : BigInt(quote.outAmount ?? 0);
return { sig, pumpRaw: received };
}
Station 3: snapshotsrc/lib/holders.ts
Pages through every holder via Helius. Excludes the creator wallet, the bonding curve, off-curve pool vaults, and anything on the exclusion list.
import { PublicKey } from "@solana/web3.js";
import { config } from "./config";
import { creator } from "./solana";
export interface Holder {
owner: string;
amountRaw: bigint;
}
const PUMP_FUN_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
const PUMP_AMM_PROGRAM = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA";
/**
* Snapshot all holders of the coin using Helius's getTokenAccounts DAS
* method (paginated, works for tokens with thousands of holders).
* Excludes the creator wallet, known pump.fun program vaults (bonding
* curve + AMM), and any configured EXCLUDED_OWNERS.
*/
export async function snapshotHolders(): Promise<Holder[]> {
const byOwner = new Map<string, bigint>();
let cursor: string | undefined;
for (let page = 0; page < 200; page++) {
const resp = await fetch(config.rpcUrl(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "flywheel",
method: "getTokenAccounts",
params: { mint: config.coinMint(), limit: 1000, ...(cursor ? { cursor } : {}) },
}),
});
if (!resp.ok) throw new Error(`Helius getTokenAccounts failed: ${resp.status}`);
const json = await resp.json();
if (json.error) throw new Error(`Helius getTokenAccounts error: ${JSON.stringify(json.error)}`);
const accounts: Array<{ owner: string; amount: number | string }> =
json.result?.token_accounts ?? [];
for (const acc of accounts) {
const amt = BigInt(acc.amount ?? 0);
if (amt <= 0n) continue;
byOwner.set(acc.owner, (byOwner.get(acc.owner) ?? 0n) + amt);
}
cursor = json.result?.cursor;
if (!cursor || accounts.length === 0) break;
}
const excluded = new Set<string>([
creator().publicKey.toBase58(),
...config.excludedOwners,
]);
const holders: Holder[] = [];
for (const [owner, amountRaw] of byOwner) {
if (excluded.has(owner)) continue;
if (amountRaw < config.minHoldingRaw) continue;
if (isProgramVault(owner)) continue;
holders.push({ owner, amountRaw });
}
holders.sort((a, b) => (b.amountRaw > a.amountRaw ? 1 : -1));
return holders;
}
/**
* Owners that are PDAs of the pump.fun bonding curve or pump AMM
* programs hold pooled liquidity, not personal bags. PDA detection by
* off-curve check is unreliable alone (multisigs are off-curve too), so
* we only auto-exclude owners derived from the known programs.
*/
function isProgramVault(owner: string): boolean {
try {
const pk = new PublicKey(owner);
if (PublicKey.isOnCurve(pk.toBytes())) return false;
// Off-curve owner: check if it's a PDA of a pump.fun program by
// deriving the canonical bonding-curve PDA for our mint.
const mint = new PublicKey(config.coinMint());
const [bondingCurve] = PublicKey.findProgramAddressSync(
[Buffer.from("bonding-curve"), mint.toBuffer()],
new PublicKey(PUMP_FUN_PROGRAM)
);
if (pk.equals(bondingCurve)) return true;
// Any other off-curve owner is most likely a pool/vault (pump AMM
// pools, DEX vaults). Personal wallets are on-curve. Exclude.
return true;
} catch {
return false;
}
}
export { PUMP_FUN_PROGRAM, PUMP_AMM_PROGRAM };
Station 4: airdropsrc/lib/airdrop.ts
Splits the PUMP pro rata with integer math (rounding dust stays in the wallet and rolls forward) and sends batched transfers with idempotent account creation.
import {
ComputeBudgetProgram,
PublicKey,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import {
createAssociatedTokenAccountIdempotentInstruction,
createTransferInstruction,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { config } from "./config";
import { connection, creator, signAndSend } from "./solana";
import type { Holder } from "./holders";
export interface AirdropShare {
owner: string;
shareRaw: bigint;
}
export interface AirdropResult {
sigs: string[];
recipients: number;
distributedRaw: bigint;
}
/** Pro-rata split of `totalRaw` PUMP across holders by coin balance. */
export function computeShares(holders: Holder[], totalRaw: bigint): AirdropShare[] {
const capped = holders.slice(0, config.maxRecipients);
const supply = capped.reduce((s, h) => s + h.amountRaw, 0n);
if (supply === 0n || totalRaw === 0n) return [];
const shares: AirdropShare[] = [];
for (const h of capped) {
const share = (totalRaw * h.amountRaw) / supply;
if (share >= config.minAirdropRaw) shares.push({ owner: h.owner, shareRaw: share });
}
return shares;
}
const RECIPIENTS_PER_TX = 5; // ATA creation is compute+size heavy; stay conservative
/** Send PUMP to each recipient in batched transactions. */
export async function airdropPump(shares: AirdropShare[]): Promise<AirdropResult> {
const wallet = creator();
const pumpMint = new PublicKey(config.pumpMint);
const sourceAta = getAssociatedTokenAddressSync(pumpMint, wallet.publicKey);
if (config.dryRun) {
const total = shares.reduce((s, x) => s + x.shareRaw, 0n);
console.log(`[airdrop] DRY_RUN: would send ${total} raw PUMP to ${shares.length} holders`);
return { sigs: [], recipients: shares.length, distributedRaw: total };
}
const sigs: string[] = [];
let distributed = 0n;
let sent = 0;
for (let i = 0; i < shares.length; i += RECIPIENTS_PER_TX) {
const batch = shares.slice(i, i + RECIPIENTS_PER_TX);
const ixs = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 80_000 * batch.length + 20_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
];
for (const s of batch) {
const ownerPk = new PublicKey(s.owner);
const destAta = getAssociatedTokenAddressSync(pumpMint, ownerPk, true);
ixs.push(
createAssociatedTokenAccountIdempotentInstruction(
wallet.publicKey,
destAta,
ownerPk,
pumpMint
),
createTransferInstruction(sourceAta, destAta, wallet.publicKey, s.shareRaw)
);
}
const { blockhash } = await connection().getLatestBlockhash("confirmed");
const msg = new TransactionMessage({
payerKey: wallet.publicKey,
recentBlockhash: blockhash,
instructions: ixs,
}).compileToV0Message();
const tx = new VersionedTransaction(msg);
try {
const sig = await signAndSend(tx, `airdrop batch ${i / RECIPIENTS_PER_TX + 1}`);
sigs.push(sig);
distributed += batch.reduce((s, x) => s + x.shareRaw, 0n);
sent += batch.length;
} catch (err) {
// One failed batch shouldn't sink the whole cycle. Log and continue;
// the unsent share stays in the wallet and rides into the next cycle.
console.error(`[airdrop] batch failed, continuing:`, err);
}
}
return { sigs, recipients: sent, distributedRaw: distributed };
}
Broadcastssrc/lib/broadcast.ts
Every event goes to the public ledger on this site, and to Telegram when configured. Same message, same signature, both places.
import { config } from "./config";
import { addBroadcast } from "./db";
import { solscanTx } from "./solana";
/**
* Broadcast a cycle event: always to the site ledger (sqlite), and to
* Telegram when configured. Messages carry the tx link so anyone can
* verify on-chain.
*/
export async function broadcast(
cycleId: number | null,
kind: "claim" | "swap" | "airdrop" | "skip" | "error",
message: string,
sig?: string
) {
addBroadcast(cycleId, kind, message, sig);
const line = sig ? `${message}\n${solscanTx(sig)}` : message;
console.log(`[broadcast:${kind}] ${line}`);
if (config.telegramBotToken && config.telegramChatId) {
try {
await fetch(`https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: config.telegramChatId,
text: line,
disable_web_page_preview: true,
}),
});
} catch (err) {
console.error("[broadcast] telegram send failed:", err);
}
}
}