scratch, for solana■ builder live
Notch
Solana scripts, snapped together.
Stack blocks like Scratch, or say what you want in one sentence. Notch hands you a script you can read, run from your own machine, and keep. Swaps, transfers, price alerts, wallet watchers.
// the problem
A script shouldn't need a developer.
Everything on Solana can be automated. Buy on a schedule, sell at a level, watch a wallet, sweep a balance. The people who do it either write code or know someone who does.
Everyone else is left with three bad options, and all three mean running something you can't read with a wallet that holds real money.
Notch is the fourth option. You build the logic with blocks that read like sentences, and you get the code anyway.
// see it work
One sentence in. Blocks and a script out.
These are the real blocks and the real generator, not a recording. Pick a script, read the blocks on the left, and compare with the file on the right.
// 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);});// two ways in
Drag it, or say it.
Both end in the same place: a stack of blocks you can check and change before anything is written.
Pick a trigger, then snap actions under it. Every block is a sentence with a few blanks. Mistakes are flagged on the block, before any code exists.
Type what you want. The AI lays out the blocks for you and tells you what is left to fill in. It can only arrange blocks, so you check its work the same way you check your own.
// the box of blocks
19 blocks. One stack.
Small on purpose. Each block does one thing you can explain to a friend.
- (!)when the script starts [live]
- (!)every __ __ [live]
- (!)when __ price goes __ __ USD [live]
- (!)when SOL balance of __ changes [live]
- (!)when __ makes a transaction [live]
- [>]log __ [live]
- [>]send __ SOL to __ [live]
- [>]send __ __ to __ [live]
- [>]swap __ __ for __ with __ % max slippage [live]
- [>]post __ to __ [live]
- [>]wait __ seconds [live]
- [>]stop the script [live]
- [=]set __ to SOL balance of __ [live]
- [=]set __ to __ balance of __ [live]
- [=]set __ to USD price of __ [live]
- [=]set __ to __ [live]
- [=]change __ by __ [live]
- <?>if __ __ __ [live]
- <?>repeat __ times [live]
What each one does and what it writes: block reference
// what keeps you safe
Built so you never have to trust us.
01
Your key never comes here
Notch writes a script. The script runs on your machine and reads the wallet key from an environment variable there. The site has no field to paste a secret key into, on purpose.
02
Dry run until you say so
A script that signs only simulates its transactions and prints what would happen. It sends for real once you start it with DRY_RUN=false.
03
Code you can read
The output is one TypeScript file on the standard Solana libraries, with only the helpers your blocks use. Nothing minified, nothing fetched at run time, no Notch server in the loop.
04
Blocks can't smuggle code
Whatever you or the AI type into a block ends up as quoted text in the script. The prompt box arranges blocks; it never writes code of its own.
// $NOTCH
The token buys the thinking.
Blocks are free and stay free. The prompt box runs an AI model, and that costs money on every request. So a prompt has a price, $0.25, and the only way to pay it is $NOTCH.
There are no accounts, no cards and no packs to buy. You hold $NOTCH in your wallet, and each prompt sends the right amount for that moment's price. If the token is worth more, a prompt takes fewer tokens. If it is worth less, it takes more.
Every script someone asks for is $NOTCH that had to be bought and spent.
One prompt
$0.25 = ... $NOTCH
$NOTCH isn't live, so there is no price to show yet. The dollar price is fixed; the number of tokens will follow the market.
// questions
Before you ask.
[+]What is a Solana script?
A small program that watches something on Solana and acts on it: buy a little every hour, warn you when a price moves, forward funds when a balance changes. Normally you need a developer to write one.
[+]Do I need to know how to code?
No to build one. To run one you paste two commands in a terminal, and the docs walk through them. If you do code, the file is yours to edit: it's plain TypeScript.
[+]Is the block editor free?
Yes. Blocks, the generated script and downloads cost nothing, with no account. Only the prompt box costs something: $0.25 per prompt.
[+]Why are prompts paid in $NOTCH?
Every prompt costs real compute, so it has a price: $0.25. You pay it in $NOTCH, straight from your wallet, at the market rate. That gives the token one clear job: it is the only way to use the AI. The dollar price stays put, so a prompt costs the same whether the token goes up or down.
[+]How does paying work?
Connect a wallet that holds $NOTCH, type your request and press the button. Your wallet shows one transfer for the exact amount; you approve it, and the blocks appear. If no script comes out, the payment is kept and your next try is free.
[+]Can a script lose my money?
A script does exactly what its blocks say, with the wallet you give it. A swap block swaps. Read the blocks, run it dry first, and use a fresh wallet that only holds what the script needs.
[+]Where does the script run?
On your computer or your own server. Notch doesn't host or run scripts, and can't see them once you download them.