Transaction Requests
Server-generated transactions for Solana Pay checkout flows.
Recipe
// Merchant API returns application/vnd.solana.transaction+json or spec-compliant body
import { createSolanaRpc } from "@solana/kit";When to reach for this:
- Checkout needs swap, mint, or custom program - not plain SOL transfer.
- POS QR opens wallet with full transaction preview.
- Pairing with reference pubkeys for reconciliation.
Working Example
import { NextRequest, NextResponse } from "next/server";
import {
createSolanaRpc,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
address,
} from "@solana/kit";
export async function GET(req: NextRequest) {
const account = req.nextUrl.searchParams.get("account");
if (!account) return NextResponse.json({ error: "missing account" }, { status: 400 });
const rpc = createSolanaRpc(process.env.RPC_URL!);
const { value: bh } = await rpc.getLatestBlockhash().send();
// Build transaction with customer as fee payer (account param)
let msg = createTransactionMessage({ version: 0 });
// append merchant instructions referencing account as signer/payer
// serialize to base64 wire transaction for wallet
return NextResponse.json({
transaction: "BASE64_WIRE_TX",
message: "Complete your purchase",
});
}What this demonstrates:
- Customer pubkey arrives as query param from wallet.
- Server constructs message; customer signs as fee payer.
- Return spec-shaped JSON for wallet consumption.
Deep Dive
How It Works
- Wallet scans QR or link → fetches merchant API → displays tx → user signs.
- Reference accounts tie on-chain tx to order id.
- HTTPS required for production trust.
Gotchas
- Trusting client-paid flag without RPC verify - fraud. Fix: server confirmation job.
- Unvalidated account param - building txs for attacker-chosen keys is OK but log abuse.
- Stale blockhash in cached response - fail confirm. Fix: generate per request.
- Missing CORS - web wallet cannot fetch API. Fix: Actions CORS headers per spec.
- Oversized tx - wallet reject. Fix: ALTs and lean instructions.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Simple transfer URL | Plain SOL pay | Need swap/mint |
| Custodial checkout | Fiat on-ramps | Self-custody merchant |
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
- Solana Pay Basics - QR intro
- Building an Action - Actions overlap
- API Routes for Transactions - Next.js
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.