Okay, so you wanna get into Solana Blinks? Awesome choice. Blinks are basically magic links that let people do on chain stuff-like donating SOL or swapping tokens-right from Twitter, Discord, or wherever. No dApps, no extensions. Just click, wallet pops up, sign, done. In my experience, it's changed how I share crypto actions with friends. Super shareable.
Why does this matter? Imagine posting a link on X that says "Donate 0.1 SOL to my tip jar" and boom, people can do it without leaving the app. That's Blinks. And today? We're building one step by step. A simple SOL transfer blink. You'll have it running in like 30 minutes if you're quick.
Short version: Solana Actions are APIs you build. Blinks are the clients (like on X or Phantom) that turn those APIs into clickable buttons. You make a URL with an action param pointing to your API. Client fetches metadata (GET), shows a pretty UI, user clicks, signs via POST. Transactions cost like ~0.000005 SOL in fees on mainnet, peanuts.
The thing is, it's all URL based. Post solana action:https://your site.com/donate?amount=0.1 anywhere. Boom. And it works on devnet for testing, no real money lost.
| Method | Example | When to use |
|---|---|---|
| Explicit | solana action:https://yoursite.com/action | For max compatibility, but only Blink clients see it |
| Known URLs | Register at Dialect, maps normal URLs to actions | Best for social sharing, gets previews |
| Query param | https://yoursite.com?action=https://api.yoursite.com/action | Fallback for non Blink spots |
Look, I usually start with Next.js 'cause the official guide does it that way. Grab a new project:
npx create next app@latest my blink --typescript --tailwind --eslint --app. Say yes to everything.cd my blinknpm install @solana/web3.js @solana/actionsnpm run dev. Hits localhost:3000.Now? Create your action file. Make a folder src/app/api/actions/transfer sol/ and inside it, route.ts. That's your API endpoint. All the magic lives here.
This one's crucial. GET returns JSON that Blinks use to render buttons, labels, icons. Miss it? No UI shows. And don't forget OPTIONS for CORS, or it won't render anywhere.
Copy this into your route.ts. Swap YOURSOLANAWALLETADDRESS with a devnet wallet you control. Use Phantom to make one if needed.
import { ActionGetResponse, ActionPostRequest, ActionPostResponse, ACTIONSCORSHEADERS, BLOCKCHAINIDS,
} from "@solana/actions"; import { Connection, PublicKey, LAMPORTSPERSOL, SystemProgram, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js"; const blockchain = BLOCKCHAIN_IDS.devnet;
const connection = new Connection("https://api.devnet.solana.com");
const donationWallet = "YOURSOLANAWALLETADDRESS"; const headers = { ..ACTIONSCORS_HEADERS, "x blockchain ids": blockchain, "x action version": "2.4",
}; export const OPTIONS = async () => { return new Response(null, { headers });
}; export const GET = async (req: Request) => { const response: ActionGetResponse = { type: "action", icon: ${new URL("/icon.jpg", req.url).toString()}, // Add your own icon label: "Donate SOL", title: "Quick SOL Tip", description: "Send some SOL my way on devnet. Testing Blinks!", links: { actions: [ { type: "transaction", label: "0.01 SOL", href: /api/actions/transfer sol?amount=0.01, }, { type: "transaction", label: "0.05 SOL", href: /api/actions/transfer sol?amount=0.05, }, { type: "transaction", label: "Custom Amount", href: /api/actions/transfer sol?amount={amount}, parameters: [{ name: "amount", label: "SOL Amount", type: "number", }], }, ], }, }; return new Response(JSON.stringify(response), { status: 200, headers });
};
Test it. Hit http://localhost:3000/api/actions/transfer sol in browser. You should see JSON. Pretty much instant feedback.
This fires when user clicks. Grabs amount from URL, payer from request body, builds tx, serializes to base64. Boom.
export const POST = async (req: Request) => { try { const url = new URL(req.url); const amount = Number(url.searchParams.get("amount")); const request: ActionPostRequest = await req.json(); const payer = new PublicKey(request.account); const receiver = new PublicKey(donationWallet); const transaction = await prepareTransaction(connection, payer, receiver, amount); const response: ActionPostResponse = { type: "transaction", transaction: Buffer.from(transaction.serialize()).toString("base64"), }; return Response.json(response, { status: 200, headers }); } catch (error) { console.error("Error:", error); return new Response(JSON.stringify({ error: "Oops, try again" }), { status: 500, headers }); }
}; const prepareTransaction = async (connection: Connection, payer: PublicKey, receiver: PublicKey, amount: number) => { const instruction = SystemProgram.transfer({ fromPubkey: payer, toPubkey: receiver, lamports: amount * LAMPORTSPERSOL, }); const { blockhash } = await connection.getLatestBlockhash(); const message = new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: [instruction], }).compileToV0Message(); return new VersionedTransaction(message);
};
Add that below your GET. Full file now? Restart dev server. Test POST with curl or Postman if you're nerdy.
Grab your full URL: http://localhost:3000/api/actions/transfer sol. To make it a Blink link: https://solana action:http://localhost:3000/api/actions/transfer sol. Ngrok it for public testing: ngrok http 3000, use the ngrok URL.
Common gotcha? Wrong wallet address. Or forgetting OPTIONS - page just blanks. Happened to me first time. Double check headers.
Local's cute, but shareable needs hosting. Vercel is free and easy for Next.js.
BLOCKCHAIN_IDS.mainnet, connection to "https://api.mainnet beta.solana.com" or your RPC (QuickNode free tier rocks).Alternative? Fleek Functions if you hate Next.js. Super serverless. Install CLI, make index.js with similar logic (check their guide for exact code), deploy. Gets you a .fleek.co URL fast.
Honestly, this is. Unregistered Blinks don't unfurl on X. Go to dial.to/register or Dialect form. Paste your action URL. They verify, approve, now your links preview with buttons everywhere. Takes a day or two sometimes.
Pro tip: Add an icon (512x512 PNG). Upload to public folder as /icon.png. Makes it pop.
Your basic donate works. But what about swaps? Or forms?
For inputs, like custom amount - you already have it in the example. Blinks show a number field. User types 0.42, hits your POST with ?amount=0.42.
Multi step? Chain links. In GET response, make actions point to other endpoints. Like step1: approve, step2: swap. Each POST returns next action JSON. Wildly powerful.
In my experience, token transfers next. Grab @solana/spl token, make transferChecked instruction. Amount in UI lamports? Nah, show SOL decimals.
Swap SystemProgram for:
import { getAssociatedTokenAddress, createTransferInstruction } from '@solana/spl token';
// In prepareTransaction:
const sourceATA = await getAssociatedTokenAddress(mint, payer);
const destATA = await getAssociatedTokenAddress(mint, receiver);
const instruction = createTransferInstruction(sourceATA, destATA, payer, amount * 1e9); // USDC decimals
Button gray? Check console for CORS errors. Add OPTIONS.
Tx fails signature? Use VersionedTransaction, not legacy. Devnet blockhash expires fast - getLatestBlockhash('finalized').
No UI on X? Register first. Or use solana action: prefix.
Fees too high? Set computeUnitLimit lower in tx message. But for transfers, default's fine at 0.000005 SOL.
Tip jars. NFT mints. Vote with tokens (burn or send to pool). Raffles: pay entry, get ticket ATA.
Posted a blink for my project's airdrop claim. 500 claims in a day via Twitter. No website needed. That's the power.
What's next for you? Tweak the amounts, add validation (min 0.001 SOL?), deploy to mainnet. Share your first blink with me - hit up Discord or whatever. You've got this.