Actions (the spec)
Standardized HTTP endpoints that produce Solana transactions for wallets and Blinks.
Recipe
GET /action-info → metadata (title, icon, description)
POST /action-execute → transaction for connected account
When to reach for this:
- Interoperable buttons across wallets, Twitter, and Discord.
- Discoverable on-chain interactions without custom deep links per wallet.
- Marketing campaigns needing shareable links.
Working Example
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
type: "action",
icon: "https://example.com/icon.png",
title: "Vote on Proposal 1",
description: "Cast your governance vote",
label: "Vote",
});
}
export async function POST(req: Request) {
const { account } = await req.json();
if (!account) return NextResponse.json({ error: "missing account" }, { status: 400 });
// build and return { transaction: "<base64>" }
return NextResponse.json({ transaction: "BASE64_TX" });
}What this demonstrates:
- GET advertises human metadata for unfurlers.
- POST receives user pubkey and returns serialized transaction.
- Follow Solana Actions CORS and security guidelines.
Deep Dive
How It Works
- Blink clients fetch GET for preview card.
- User click triggers POST with wallet pubkey.
- Wallet signs returned transaction.
- Optional callback URLs report completion to dApp.
Response Fields
| Field | Purpose |
|---|---|
| title | Card headline |
| icon | HTTPS image URL |
| label | Button text |
| transaction | Base64 wire tx |
Gotchas
- HTTP not HTTPS - wallets refuse. Fix: TLS everywhere.
- Wrong CORS - browser wallet blocked. Fix: spec headers on OPTIONS/POST.
- Malicious icon URL - phishing. Fix: host on your domain.
- No account validation - format check only; still build safe instructions.
- GET mutates state - cache poisoning. Fix: GET is read-only metadata only.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Deep link only | Single wallet integration | Cross-platform Blinks |
| In-app browser dApp | Full UX control | Viral share campaigns |
FAQs
App Router?
Providers in client layout.tsx; pages mix server + client components.
Secrets?
Server env only for keypairs; NEXT_PUBLIC_ for RPC URLs.
kit 7.0.0?
All examples use @solana/kit, not web3.js v1.
Wallet?
Signing only in client components.
Caching?
Revalidate on-chain reads carefully - chain is source of truth.
Actions?
HTTPS endpoints returning Solana Actions JSON.
Mobile?
Separate React Native app with MWA.
Testing?
Surfpool for integration; mock RPC in unit tests.
Mainnet?
Separate env files and program IDs.
Errors?
Decode simulation logs for user-facing messages.
Related
- Building an Action - implementation
- Blinks - embed links
- Security & Verification - safety
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.