Script builder
Prompts are paid in $NOTCH, $0.25 each, and $NOTCH isn't live yet. The prompt box opens with the token: how it works. The block editor below is free and works now.
4 blocks · ready
every
setto SOL balance of
if
swapforwith% max slippage
log
Drag blocks in, or click one to add it. Click an if or repeat block first to add inside it. A new trigger replaces the one on top.
// hourly-dca.ts// Built with Notch. Read it before you run it: it is your wallet and your script.//// npm install @solana/web3.js bs58// SECRET_KEY=... npx tsx hourly-dca.ts//// Transactions are only simulated until you add DRY_RUN=false.import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, type TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js";import bs58 from "bs58";const RPC_URL = process.env.RPC_URL ?? "https://api.mainnet-beta.solana.com";const DRY_RUN = process.env.DRY_RUN !== "false";const JUP_API = "https://lite-api.jup.ag";const SOL_MINT = "So11111111111111111111111111111111111111112";const connection = new Connection(RPC_URL, "confirmed");const wallet = loadWallet();function loadWallet(): Keypair { const raw = process.env.SECRET_KEY?.trim(); if (!raw) throw new Error("Set SECRET_KEY to the secret key of the wallet this script signs with (base58 or JSON array)."); return Keypair.fromSecretKey(raw.startsWith("[") ? Uint8Array.from(JSON.parse(raw) as number[]) : bs58.decode(raw));}// Values the blocks read and write.const vars: Record<string, number | string> = {};function num(name: string): number { const value = Number(vars[name]); if (Number.isNaN(value)) throw new Error(`Variable ${name} is not a number yet.`); return value;}function fmt(template: string): string { return template.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (whole, name: string) => (name in vars ? String(vars[name]) : whole));}async function solBalance(owner: PublicKey): Promise<number> { return (await connection.getBalance(owner)) / LAMPORTS_PER_SOL;}const mints = new Map<string, { decimals: number; program: PublicKey }>();async function mintInfo(mint: string): Promise<{ decimals: number; program: PublicKey }> { const known = mints.get(mint); if (known) return known; const account = await connection.getParsedAccountInfo(new PublicKey(mint)); const data = account.value?.data; if (!account.value || !data || !("parsed" in data)) throw new Error(`${mint} is not a token mint.`); const info = { decimals: Number(data.parsed.info.decimals), program: account.value.owner }; mints.set(mint, info); return info;}async function build(instructions: TransactionInstruction[]): Promise<VersionedTransaction> { const { blockhash } = await connection.getLatestBlockhash(); return new VersionedTransaction(new TransactionMessage({ payerKey: wallet.publicKey, recentBlockhash: blockhash, instructions }).compileToV0Message());}async function submit(label: string, tx: VersionedTransaction): Promise<void> { tx.sign([wallet]); if (DRY_RUN) { const sim = await connection.simulateTransaction(tx); console.log(`[dry run] ${label}: ${sim.value.err ? `would fail, ${JSON.stringify(sim.value.err)}` : "would succeed"}`); return; } const signature = await connection.sendTransaction(tx); const latest = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature, ...latest }, "confirmed"); console.log(`${label}: https://solscan.io/tx/${signature}`);}async function swap(amount: number, from: string, to: string, slippagePercent: number): Promise<void> { const decimals = from === SOL_MINT ? 9 : (await mintInfo(from)).decimals; const units = BigInt(Math.round(amount * 10 ** decimals)); const query = new URLSearchParams({ inputMint: from, outputMint: to, amount: units.toString(), slippageBps: String(Math.round(slippagePercent * 100)) }); const quoteRes = await fetch(`${JUP_API}/swap/v1/quote?${query}`); if (!quoteRes.ok) throw new Error(`Quote failed: ${quoteRes.status} ${await quoteRes.text()}`); const quote: unknown = await quoteRes.json(); const swapRes = await fetch(`${JUP_API}/swap/v1/swap`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.publicKey.toBase58(), dynamicComputeUnitLimit: true }), }); const body = (await swapRes.json()) as { swapTransaction?: string; error?: string }; if (!swapRes.ok || !body.swapTransaction) throw new Error(`Swap failed: ${body.error ?? swapRes.status}`); await submit(`swap ${amount} of ${from} for ${to}`, VersionedTransaction.deserialize(Buffer.from(body.swapTransaction, "base64")));}// The stack of blocks under the trigger.async function run(): Promise<void> { vars["balance"] = await solBalance(wallet.publicKey); if (num("balance") > 1) { await swap(0.05, "So11111111111111111111111111111111111111112", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 1); console.log(fmt("bought, had {balance} SOL")); }}// One run at a time: a trigger that fires while the stack is busy is skipped.let busy = false;async function tick(): Promise<void> { if (busy) return; busy = true; try { await run(); } catch (error) { console.error(error); } finally { busy = false; }}// Trigger: every 1 hoursasync function main(): Promise<void> { console.log(`Wallet ${wallet.publicKey.toBase58()}, ${DRY_RUN ? "dry run" : "LIVE"}`); await tick(); setInterval(() => void tick(), 3600000);}main().catch((error) => { console.error(error); process.exit(1);});! This script signs with a wallet. It reads the key from SECRET_KEY on your machine and only simulates until you set DRY_RUN=false. Use a fresh wallet holding only what the script needs. How to run it