Solana “Actions” & Blinks: Transacting from Any App (and X)
Solana Actions and Blinks unlock a new UX primitive: onchain actions that execute from almost anywhere, a website, a blog post, an email, a chat client, even an X (Twitter) post. Instead of driving users through multi-page dApp flows, you publish a simple, shareable link or embed that wallets and compatible clients can turn into a one-click transaction. This guide is a practical deep dive: what Actions/Blinks actually are, how they differ from traditional deep links, how to add them to your site and social posts, how to instrument analytics and protect against fraud, and what to test before you go live.
TL;DR: A Solana Action is an HTTP endpoint that, when called by a wallet or Action-aware client, returns a serialized transaction payload (or an instruction bundle) tailored to the caller. A Blink is a shareable link or embed that points to that Action and renders a “Do the thing” button swap → sign, mint → sign, tip → sign, inside compatible contexts (sites, chats, and X). You keep the business logic server-side, you personalize the transaction to the user/wallet at request time, and you let wallets handle signing/broadcast. The big wins: shorter funnels, higher conversion, and portable distribution. The hard parts: replay protection, calldata validation, price slippage checks, rate limiting, and analytics that respect privacy.
What Are Solana Actions & “Blinks” (Blockchain Links)?
Think of an Action as a lightweight transaction API for your product. Instead of asking users to navigate to a full dApp, connect a wallet, pick options, and click through modals, you expose a single endpoint. The wallet (or a Blink-enabled client) calls it with minimal parameters (e.g., “mint 1”, “swap 0.3 SOL to USDC”), your server constructs a transaction tailored to that wallet’s address and current network conditions (recent blockhash, slippage limits, dynamic fees), returns it, and the wallet signs and sends.
A Blink is the shareable wrapper: an onchain link that clients can render as an interactive card with a button. Post it on X, drop it into Discord, paste it into an article, compatible clients show a preview (“Mint Now”, “Swap SOL→USDC”, “Tip Creator”), and clicking routes the user straight to a wallet to review and sign. It’s the “anywhere commerce” moment for onchain actions.
Key characteristics
- Stateless link, stateful response: The URL can be stable; the server response uses the caller’s wallet and fresh blockhash.
- Wallet-first security: Wallets run preflight checks, simulate, and show human-readable summaries.
- Composability: Same Action powers website buttons, QR codes at events, and viral social posts.
- Analytics-friendly: You can add UTM params and server-side logs without leaking secrets on-chain.
| Capability | Classic dApp | Action + Blink |
|---|---|---|
| Funnel length | Multi-page flows | One link → one click → wallet |
| Distribution surface | Your site/app only | Websites, X posts, chats, emails, QR |
| Personalization | Client-side logic | Server builds tx per wallet |
| Fraud controls | Manual and mixed | Preflights, allowlists, server rate limits |
Architecture & Request Flow
At a high level, an Action looks like a standard HTTPS endpoint (GET or POST) behind your domain. A Blink is a URL that points to that endpoint, optionally decorated with metadata so clients render a “call-to-action” card. When a user clicks the Blink, a wallet or Action-aware client fetches the endpoint, your server builds a transaction targeted to the requester’s wallet, and the wallet presents, simulates, and signs.
Action Response Shape (Conceptual)
Different frameworks/SDKs exist, but the response generally includes:
- Display metadata (label, description, icon) for clients that render cards/buttons.
- Transaction payload: a base64-encoded signed-by-server-where-needed (or partially signed) Solana transaction with a fresh blockhash and the user’s wallet set as the fee payer or payer of specific accounts.
- Post-action links (success/fail), optional simulate first flag, and a valid-until timestamp to prevent replay.
// Example JSON (illustrative)
{
"type": "action",
"version": "1",
"title": "Swap SOL → USDC",
"icon": "https://yourcdn.com/icons/swap.svg",
"description": "Best price routed. Max slippage 0.5%.",
"transaction": {
"network": "mainnet-beta",
"encoded": "<base64-serialized-transaction>",
"validUntil": 1735689600 // unix timestamp
},
"links": {
"success": "https://yourapp.com/success?tx={signature}",
"fail": "https://yourapp.com/fail"
}
}
Your server fills the wallet’s public key, looks up quotes, sets slippage, fetches a recent blockhash, and returns an encoded transaction bound to a short validity window.
Hands-On Examples: Swap, Tip, and NFT Mint
Below we outline three common Actions with code you can adapt. We’ll assume a Node/TypeScript server (Next.js API route or Cloudflare Worker) and @solana/web3.js. In production you would also wire price APIs/routers, a cache, and your allowlists.
A) “Swap SOL→USDC” Action (Best-Effort Routing)
This Action takes an input amount in SOL, fetches a price route (via your chosen aggregator), builds a swap transaction, and returns it as a Blink-ready response. Keep heavy logic server-side: slippage bounds, fee payer rules, and per-wallet caps.
// pages/api/actions/swap-sol-usdc.ts (illustrative)
import { Connection, PublicKey, Transaction, SystemProgram } from "@solana/web3.js";
// import your router SDK (Jupiter/Helius/YourRouter)
const RPC = process.env.SOLANA_RPC!;
const connection = new Connection(RPC, "confirmed");
export default async function handler(req, res) {
try {
const { wallet, amount } = req.method === "POST" ? req.body : req.query;
if (!wallet || !amount) return res.status(400).json({ error: "wallet and amount required" });
const user = new PublicKey(wallet);
const lamports = BigInt(Math.floor(parseFloat(String(amount)) * 1e9)); // SOL → lamports
// 1) Preflights & caps
if (lamports < 1000000n) return res.status(400).json({ error: "min 0.001 SOL" });
// rate-limit by IP/wallet outside this snippet
// 2) Fetch route from your DEX router (pseudo)
const route = await getBestRouteSOLtoUSDC(lamports); // slippage 0.5%, etc.
// 3) Build transaction (pseudo; replace with router builder)
const { transaction } = await route.build({
userPublicKey: user,
slippageBps: 50,
});
transaction.feePayer = user;
transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
const encoded = transaction.serialize({ requireAllSignatures: false }).toString("base64");
const validUntil = Math.floor(Date.now() / 1000) + 60; // 60s validity
return res.status(200).json({
type: "action",
version: "1",
title: "Swap SOL → USDC",
icon: "https://yourcdn.com/icons/swap.svg",
description: "Best price routed. Max slippage 0.5%.",
transaction: { network: "mainnet-beta", encoded, validUntil },
links: {
success: "https://yourapp.com/swap/success?tx={signature}",
fail: "https://yourapp.com/swap/fail"
}
});
} catch (e) {
console.error(e);
return res.status(500).json({ error: "internal_error" });
}
}
Notes:
- Set a very short transaction validity (fresh blockhash). Old links should fail safely.
- Simulate on the wallet side and show the minimum expected USDC out. If the quote moved, instruct users to retry.
- Enforce per-wallet caps server-side to avoid abuse during subsidized campaigns.
B) “Tip the Creator” Action (SOL or USDC)
A dead-simple Action: send 0.05 SOL (or any amount) to a recipient. You can publish different Blinks for preset values and track each with UTM tags.
// pages/api/actions/tip.ts (illustrative)
import { Connection, PublicKey, SystemProgram, Transaction } from "@solana/web3.js";
const RPC = process.env.SOLANA_RPC!;
const connection = new Connection(RPC, "confirmed");
const CREATOR = new PublicKey(process.env.CREATOR_ADDRESS!);
export default async function handler(req, res) {
try {
const { wallet, amount } = req.method === "POST" ? req.body : req.query;
if (!wallet || !amount) return res.status(400).json({ error: "wallet and amount required" });
const user = new PublicKey(wallet);
const lamports = BigInt(Math.floor(parseFloat(String(amount)) * 1e9));
if (lamports < 1000000n) return res.status(400).json({ error: "min 0.001 SOL" });
const ix = SystemProgram.transfer({
fromPubkey: user,
toPubkey: CREATOR,
lamports: Number(lamports),
});
const tx = new Transaction().add(ix);
tx.feePayer = user;
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
const encoded = tx.serialize({ requireAllSignatures: false }).toString("base64");
const validUntil = Math.floor(Date.now() / 1000) + 60;
return res.status(200).json({
type: "action",
version: "1",
title: "Tip the Creator (SOL)",
icon: "https://yourcdn.com/icons/tip.svg",
description: "Direct onchain tip. Thank you!",
transaction: { network: "mainnet-beta", encoded, validUntil },
links: { success: "https://yourapp.com/tip/thanks?tx={signature}" }
});
} catch (e) {
console.error(e);
return res.status(500).json({ error: "internal_error" });
}
}
C) “Mint an NFT” Action (Candy Machine or Custom Program)
For mints, Actions shine: you can throttle per-wallet mints server-side, inject a merkle proof or signature for allowlists, and cancel when supply is exhausted, all without users ever leaving a social post or article.
// pages/api/actions/mint.ts (illustrative)
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
// import your mint program client (e.g., Candy Machine SDK)
const RPC = process.env.SOLANA_RPC!;
const connection = new Connection(RPC, "confirmed");
export default async function handler(req, res) {
try {
const { wallet } = req.method === "POST" ? req.body : req.query;
if (!wallet) return res.status(400).json({ error: "wallet required" });
const user = new PublicKey(wallet);
// 1) Check allowlist / supply / per-wallet mint caps
const ok = await checkEligibility(user);
if (!ok) return res.status(403).json({ error: "not eligible or cap reached" });
// 2) Build mint transaction with your program client
const tx = await buildMintTx({ payer: user }); // returns Transaction with instructions
tx.feePayer = user;
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
const encoded = tx.serialize({ requireAllSignatures: false }).toString("base64");
const validUntil = Math.floor(Date.now() / 1000) + 60;
return res.status(200).json({
type: "action",
version: "1",
title: "Mint Your NFT",
icon: "https://yourcdn.com/icons/mint.svg",
description: "Supply limited. One per wallet.",
transaction: { network: "mainnet-beta", encoded, validUntil },
links: {
success: "https://yourapp.com/mint/success?tx={signature}",
fail: "https://yourapp.com/mint/fail"
}
});
} catch (e) {
console.error(e);
return res.status(500).json({ error: "internal_error" });
}
}
How to Embed on Websites, Blogs, and X
You can surface Actions anywhere a link fits. For websites and blogs, create styled buttons that resolve to your Action URL and let integrated wallets intercept. For X, post the Blink URL with a clear CTA. Many wallets and Action-aware clients render a clickable card inside the post so users transact without leaving the feed.
Website Button (Vanilla)
<a
href="https://yourapp.com/api/actions/swap-sol-usdc?amount=0.3"
rel="nofollow"
style="display:inline-block; padding:12px 16px; background:#2fc9b2; color:#fff; border-radius:10px; text-decoration:none; font-weight:800;"
>Swap 0.3 SOL → USDC</a>
Enhance with device detection: if a mobile wallet is installed, open its deep link; otherwise, fallback to a universal wallet selector that can consume the Action endpoint.
X (Twitter) Post Copy
Swap SOL → USDC in one click ⚡ Action-enabled link below (wallets will simulate + confirm): https://yourapp.com/api/actions/swap-sol-usdc?amount=0.3&utm_source=x&utm_campaign=blink-swap #Solana #Blinks #OnchainUX
Add UTM parameters for analytics attribution, and pin the post during campaigns. Post multiple preset amounts to test click-through and completion rates.
QR Codes (Events & IRL)
Generate a QR pointing to your Action URL. At conferences, this is perfect for instant tip jars, POAP-style claims, or limited mints. Rotate the QR if you notice abuse.
Analytics & Growth Loops: Measure and Improve
Actions and Blinks reduce friction, but you still need data. The flow splits across your server, the wallet, and the chain. Your goal is a privacy-respecting pipeline that answers: which surfaces drive signed transactions, what’s the drop-off between click → simulation → signature, and how do fee spikes affect completion?
Event model (server-side and on-chain)
1) blink_impression (optional): generated by your site if you render a card 2) action_request: every hit to /api/actions/* (log UTM, IP hash, user-agent, wallet param if present) 3) action_response: returned a tx payload (log type, quote details, blockhash age) 4) wallet_simulation: cannot observe directly; infer via time-to-signature patterns 5) tx_submitted: wallet calls back /success?tx=... (record signature) 6) onchain_confirmation: watch signature; record slot, compute slippage, realized fees
- Attribution: Use UTM tags in your Blink URLs and propagate them into
success/failcallbacks. Avoid storing full IPs; hash with a rotating salt if you need repeat-click metrics. - Completion rates: Track response → signature conversion; bucket by fee levels and time of day. If completion collapses at high fees, surface UI nudges (“try again when fees drop”).
- AB tests: Compare “preset amounts” vs “custom amount” Blinks; test short vs long descriptions; try different icons and verbs (“Mint Now” vs “Claim”).
- Growth loop: After success, show a “Share this Blink” prompt with a prefilled X post—this compounds distribution.
Security Caveats & Hardening (Read Before You Ship)
Moving the transaction builder server-side is powerful and dangerous if you skip guardrails. Here are the major risks and how to mitigate them.
Risk: Replay & Stale Blockhashes
- Old transactions might still be signable if the wallet caches them.
- Mitigation: always fetch a fresh blockhash and include a validUntil (60–120s). Reject if too old.
Risk: Parameter Tampering
- Attackers tweak query params (e.g., amount=1000) or target accounts.
- Mitigation: server-side input schemas, hard caps per wallet, and strict allowlists for destination mints/programs.
Risk: Price/Slippage Shifts
- Market moves between quote and sign time.
- Mitigation: tight slippage bounds, on-wallet simulation, and server-side revalidation if validUntil exceeded.
Risk: Campaign Abuse
- Bots drain subsidized Actions (airdrops, promo mints).
- Mitigation: per-wallet caps, IP+device rate limiting (privacy-aware), proofs (captcha/passkeys), and velocity rules.
Risk: Phishing Clones
- Fake Blinks that point to malicious endpoints.
- Mitigation: always host on your verified domain, publish DKIM/DMARC, and encourage wallets to display the domain prominently.
Risk: Program Incompatibility
- Some wallets/clients might not support your custom instructions.
- Mitigation: stick to well-known program interfaces where possible; feature-detect and offer a fallback “open dApp” link.
Pre-flight hardening checklist
- Validate every input server-side; never trust client values for amounts, mints, token accounts.
- Fetch a fresh blockhash on every build; expire responses quickly.
- Simulate server-side for high-value Actions; bail early if accounts/mints are frozen or insufficient.
- Set conservative slippage; include a note in the Blink description so users know the guardrails.
- Log action_request/action_response/tx_submitted; alert on unusual velocity or failure spikes.
- Throttle by wallet/IP/device; consider allowlists for limited mints.
Production-Ready Checklist (Copy/Paste into Your Runbook)
- Domain & TLS: Serve Actions from your primary HTTPS domain; configure HSTS; pin your canonical URL in all Blinks.
- Schema Validation: Enforce JSON schema for inputs; reject unknown query params immediately.
- Blockhash Freshness: Target < 30s; expire responses; include
validUntilin payload. - Simulation: Simulate server-side for complex Actions; wallets will simulate again, double safety.
- Slippage Discipline: Hard cap slippage; fail fast if quotes are stale; instruct users to retry.
- Caps & Rate Limits: Per-wallet caps (on chain + server), IP throttles, and campaign-level limits.
- Observability: Log action_request/response; export metrics (p95 time-to-signature, fail codes); set alerts.
- Fallback UX: Provide “Open in dApp” link if the client cannot consume the Action.
- Content Security: Where possible, sign Action metadata or include a short fingerprint so wallets can detect tampering.
- Docs & Transparency: Publicly document your Action endpoints and safety policies; it builds trust.
Go Deeper with TokenToolHub
Frequently Asked Questions
Do Actions replace my dApp?
No. Actions are the fast lane for common flows (tip, claim, mint, swap). Keep your full dApp for complex journeys (portfolio views, settings, multi-step flows). In practice, Actions boost top-of-funnel conversion and discovery; the dApp handles power-user depth.
How do wallets know what to show?
Wallets and compatible clients fetch your Action endpoint and parse the returned JSON metadata and transaction payload. They render a descriptive summary, run simulation, and then prompt the user to sign. Good wallets surface domain names and safety warnings to prevent phishing.
Can I include dynamic pricing or allowlists?
Yes. Compute quotes server-side at request time and insert merkle proofs or signatures for allowlisted mints. Always revalidate when the wallet calls back with a signature; if the response is stale, instruct the user to retry.
What happens if fees spike mid-flow?
Wallet simulation should catch insufficient fees or changing compute budgets. Your Action can also set a short validity window and conservative slippage so transactions fail safely instead of executing at a bad price. Teach users that “Retry” is normal under volatile fees.
How do I prevent abuse during promotions?
Use per-wallet server caps, allowlists, device/IP throttles, and signed coupons/tokens that the wallet must present to mint/claim. Consider progressive disclosure (email capture → Action link) to slow bots, and monitor velocity with alerts.
Glossary
- Action: An HTTPS endpoint that returns a tailored Solana transaction payload with display metadata for wallets.
- Blink (Blockchain Link): A shareable URL clients render as a “do it now” card/button that calls an Action.
- Preflight/Simulation: A wallet step that simulates your transaction before signing to catch errors and high slippage.
- ATA: Associated Token Account; the standard SPL token holding account for a wallet.
- Slippage: Allowed price movement between quote and execution; expressed in basis points (bps).
- Blockhash: A recent block’s hash; transactions must include a fresh one to be valid (prevents replay).
