Transaction Requests

Build interactive payment flows with server-side transaction creation

Transaction Requests

Transaction requests enable interactive payment flows by allowing wallets to communicate with your server to compose any Solana transaction. This unlocks advanced use cases like NFT minting, dynamic pricing, multi-step DeFi transactions, and custom business logic.

Overview

Transaction requests follow this URL format:

solana:<server-endpoint-url>

The wallet makes two HTTP requests to your endpoint:

  1. GET request - Retrieve payment information (label, icon)
  2. POST request - Get the transaction to sign

Basic Setup

1. Install Dependencies

pnpm add @solana/pay @solana/web3.js @solana/spl-token bignumber.js

2. Create API Endpoint

Set up an API endpoint that handles both GET and POST requests:

// pages/api/pay.js (Next.js) or similar endpoint
import { 
  Connection, 
  PublicKey, 
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL 
} from '@solana/web3.js';
import BigNumber from 'bignumber.js';

const connection = new Connection(process.env.SOLANA_RPC_URL);
const MERCHANT_WALLET = new PublicKey(process.env.MERCHANT_WALLET);

export default async function handler(req, res) {
  if (req.method === 'GET') {
    return handleGet(req, res);
  }
  
  if (req.method === 'POST') {
    return handlePost(req, res);
  }
  
  return res.status(405).json({ error: 'Method not allowed' });
}

// GET request handler
async function handleGet(req, res) {
  const label = 'My Store';
  const icon = 'https://mystore.com/icon.png';
  
  res.status(200).json({
    label,
    icon,
  });
}

// POST request handler
async function handlePost(req, res) {
  try {
    const { account } = req.body;
    
    if (!account) {
      return res.status(400).json({ error: 'Missing account parameter' });
    }
    
    const buyerPublicKey = new PublicKey(account);
    
    // Create transaction
    const transaction = await createPaymentTransaction(buyerPublicKey);
    
    // Serialize transaction
    const serializedTransaction = transaction
      .serialize({ requireAllSignatures: false })
      .toString('base64');
    
    res.status(200).json({
      transaction: serializedTransaction,
      message: 'Thanks for your purchase!',
    });
    
  } catch (error) {
    console.error('Transaction creation error:', error);
    res.status(500).json({ error: 'Failed to create transaction' });
  }
}

async function createPaymentTransaction(buyer) {
  // Calculate payment amount (you should do this server-side)
  const amount = new BigNumber(0.1); // 0.1 SOL
  const lamports = amount.multipliedBy(LAMPORTS_PER_SOL).integerValue();
  
  // Get recent blockhash
  const { blockhash } = await connection.getLatestBlockhash();
  
  // Create transfer instruction
  const transferInstruction = SystemProgram.transfer({
    fromPubkey: buyer,
    toPubkey: MERCHANT_WALLET,
    lamports: lamports.toNumber(),
  });
  
  // Create transaction
  const transaction = new Transaction({
    feePayer: buyer,
    recentBlockhash: blockhash,
  }).add(transferInstruction);
  
  return transaction;
}

3. Create Payment URL

Generate the transaction request URL:

import { createTransactionRequestURL } from '@solana/pay';

// Your API endpoint
const apiUrl = 'https://yoursite.com/api/pay';

// Create transaction request URL
const url = createTransactionRequestURL({
  link: new URL(apiUrl),
});

console.log(url.toString());
// Output: solana:https://yoursite.com/api/pay

Advanced Examples

SPL Token Payment

Create a transaction for USDC payment:

import { 
  createTransferCheckedInstruction, 
  getAssociatedTokenAddress,
  getMint 
} from '@solana/spl-token';

async function createTokenPaymentTransaction(buyer, amount) {
  const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
  
  // Get token accounts
  const buyerTokenAccount = await getAssociatedTokenAddress(USDC_MINT, buyer);
  const merchantTokenAccount = await getAssociatedTokenAddress(USDC_MINT, MERCHANT_WALLET);
  
  // Get mint info for decimals
  const mintInfo = await getMint(connection, USDC_MINT);
  
  // Calculate token amount with decimals
  const tokenAmount = amount.multipliedBy(Math.pow(10, mintInfo.decimals)).integerValue();
  
  const { blockhash } = await connection.getLatestBlockhash();
  
  // Create transfer instruction
  const transferInstruction = createTransferCheckedInstruction(
    buyerTokenAccount,    // from
    USDC_MINT,           // mint
    merchantTokenAccount, // to
    buyer,               // owner
    tokenAmount.toNumber(), // amount
    mintInfo.decimals    // decimals
  );
  
  const transaction = new Transaction({
    feePayer: buyer,
    recentBlockhash: blockhash,
  }).add(transferInstruction);
  
  return transaction;
}

Dynamic Pricing with Discounts

Implement dynamic pricing based on customer data:

async function handlePost(req, res) {
  const { account } = req.body;
  const buyer = new PublicKey(account);
  
  // Check if customer holds discount NFT
  const hasDiscountNFT = await checkForDiscountNFT(buyer);
  
  // Calculate dynamic price
  let basePrice = new BigNumber(10); // $10 base price
  if (hasDiscountNFT) {
    basePrice = basePrice.multipliedBy(0.8); // 20% discount
  }
  
  const transaction = await createTokenPaymentTransaction(buyer, basePrice);
  const serializedTransaction = transaction
    .serialize({ requireAllSignatures: false })
    .toString('base64');
  
  const message = hasDiscountNFT 
    ? `20% discount applied! Total: $${basePrice.toFixed(2)}`
    : `Total: $${basePrice.toFixed(2)}`;
  
  res.status(200).json({
    transaction: serializedTransaction,
    message,
  });
}

async function checkForDiscountNFT(wallet) {
  // Check wallet for specific NFT collection
  const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet, {
    programId: TOKEN_PROGRAM_ID,
  });
  
  // Logic to check for discount NFT
  return tokenAccounts.value.some(account => {
    // Check if account holds discount NFT
    return account.account.data.parsed.info.mint === DISCOUNT_NFT_MINT;
  });
}

NFT Minting at Point of Sale

Mint an NFT as part of the payment transaction:

import { 
  createInitializeMintInstruction,
  createAssociatedTokenAccountInstruction,
  createMintToInstruction,
  MINT_SIZE,
  TOKEN_PROGRAM_ID,
  ASSOCIATED_TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import { Keypair } from '@solana/web3.js';

async function createNFTMintTransaction(buyer) {
  // Generate new NFT mint
  const nftMint = Keypair.generate();
  
  // Calculate rent for mint account
  const mintRent = await connection.getMinimumBalanceForRentExemption(MINT_SIZE);
  
  // Get buyer's associated token account for NFT
  const buyerTokenAccount = await getAssociatedTokenAddress(nftMint.publicKey, buyer);
  
  const { blockhash } = await connection.getLatestBlockhash();
  
  const transaction = new Transaction({
    feePayer: buyer,
    recentBlockhash: blockhash,
  });
  
  // 1. Payment to merchant
  transaction.add(
    SystemProgram.transfer({
      fromPubkey: buyer,
      toPubkey: MERCHANT_WALLET,
      lamports: LAMPORTS_PER_SOL * 0.1, // 0.1 SOL
    })
  );
  
  // 2. Create mint account
  transaction.add(
    SystemProgram.createAccount({
      fromPubkey: buyer,
      newAccountPubkey: nftMint.publicKey,
      space: MINT_SIZE,
      lamports: mintRent,
      programId: TOKEN_PROGRAM_ID,
    })
  );
  
  // 3. Initialize mint
  transaction.add(
    createInitializeMintInstruction(
      nftMint.publicKey,
      0, // decimals
      MERCHANT_WALLET, // mint authority
      MERCHANT_WALLET, // freeze authority
    )
  );
  
  // 4. Create buyer's token account
  transaction.add(
    createAssociatedTokenAccountInstruction(
      buyer, // payer
      buyerTokenAccount, // associated token account
      buyer, // owner
      nftMint.publicKey, // mint
    )
  );
  
  // 5. Mint NFT to buyer
  transaction.add(
    createMintToInstruction(
      nftMint.publicKey,
      buyerTokenAccount,
      MERCHANT_WALLET, // mint authority
      1, // amount (1 for NFT)
    )
  );
  
  // Partially sign with NFT mint keypair
  transaction.partialSign(nftMint);
  
  return transaction;
}

Order Management Integration

Integrate with your existing order system:

import { createHash } from 'crypto';

// Store order in database
async function createOrder(items, customerEmail) {
  const order = {
    id: generateOrderId(),
    items,
    customerEmail,
    total: calculateTotal(items),
    status: 'pending',
    createdAt: new Date(),
  };
  
  await db.orders.create(order);
  return order;
}

// Enhanced POST handler with order management
async function handlePost(req, res) {
  const { account } = req.body;
  const { orderId } = req.query;
  
  if (!orderId) {
    return res.status(400).json({ error: 'Missing order ID' });
  }
  
  // Get order details
  const order = await db.orders.findById(orderId);
  if (!order || order.status !== 'pending') {
    return res.status(400).json({ error: 'Invalid order' });
  }
  
  const buyer = new PublicKey(account);
  
  // Create payment transaction with order reference
  const transaction = await createOrderPaymentTransaction(buyer, order);
  
  // Update order status
  await db.orders.update(orderId, { 
    status: 'processing',
    walletAddress: account 
  });
  
  const serializedTransaction = transaction
    .serialize({ requireAllSignatures: false })
    .toString('base64');
  
  res.status(200).json({
    transaction: serializedTransaction,
    message: `Order ${order.id} - ${order.items.length} items`,
  });
}

async function createOrderPaymentTransaction(buyer, order) {
  // Create reference from order ID
  const reference = new PublicKey(
    createHash('sha256')
      .update(order.id)
      .digest()
      .slice(0, 32)
  );
  
  const { blockhash } = await connection.getLatestBlockhash();
  
  // Payment instruction
  const transferInstruction = SystemProgram.transfer({
    fromPubkey: buyer,
    toPubkey: MERCHANT_WALLET,
    lamports: order.total * LAMPORTS_PER_SOL,
  });
  
  // Add reference as read-only key
  transferInstruction.keys.push({
    pubkey: reference,
    isWritable: false,
    isSigner: false,
  });
  
  return new Transaction({
    feePayer: buyer,
    recentBlockhash: blockhash,
  }).add(transferInstruction);
}

Payment Validation

Validate completed transactions:

import { findTransactionSignature } from '@solana/pay';

async function validatePayment(reference) {
  try {
    const signature = await findTransactionSignature(
      connection,
      reference,
      { finality: 'confirmed' }
    );
    
    if (signature) {
      const transaction = await connection.getTransaction(signature, {
        commitment: 'confirmed'
      });
      
      if (transaction && transaction.meta?.err === null) {
        return {
          success: true,
          signature,
          transaction,
        };
      }
    }
  } catch (error) {
    console.error('Payment validation error:', error);
  }
  
  return { success: false };
}

// Use in webhook or polling
app.post('/webhook/payment-status', async (req, res) => {
  const { orderId } = req.body;
  const order = await db.orders.findById(orderId);
  
  if (!order) {
    return res.status(404).json({ error: 'Order not found' });
  }
  
  const reference = generateReferenceFromOrder(order);
  const validation = await validatePayment(reference);
  
  if (validation.success) {
    // Update order status
    await db.orders.update(orderId, {
      status: 'completed',
      transactionSignature: validation.signature,
    });
    
    // Send confirmation email, fulfill order, etc.
    await fulfillOrder(order);
  }
  
  res.json(validation);
});

Error Handling

Implement robust error handling:

async function handlePost(req, res) {
  try {
    const { account } = req.body;
    
    if (!account) {
      return res.status(400).json({ 
        error: 'Missing account parameter',
        code: 'MISSING_ACCOUNT'
      });
    }
    
    let buyer;
    try {
      buyer = new PublicKey(account);
    } catch (error) {
      return res.status(400).json({ 
        error: 'Invalid account address',
        code: 'INVALID_ACCOUNT'
      });
    }
    
    // Check if account exists
    const accountInfo = await connection.getAccountInfo(buyer);
    if (!accountInfo) {
      return res.status(400).json({ 
        error: 'Account not found',
        code: 'ACCOUNT_NOT_FOUND'
      });
    }
    
    // Check minimum balance
    const minBalance = 0.01 * LAMPORTS_PER_SOL; // 0.01 SOL
    if (accountInfo.lamports < minBalance) {
      return res.status(400).json({ 
        error: 'Insufficient balance',
        code: 'INSUFFICIENT_BALANCE',
        required: minBalance,
        current: accountInfo.lamports
      });
    }
    
    const transaction = await createPaymentTransaction(buyer);
    
    const serializedTransaction = transaction
      .serialize({ requireAllSignatures: false })
      .toString('base64');
    
    res.status(200).json({
      transaction: serializedTransaction,
      message: 'Payment ready',
    });
    
  } catch (error) {
    console.error('Transaction request error:', error);
    
    res.status(500).json({ 
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    });
  }
}

Best Practices

Security

  • Validate all input parameters
  • Calculate amounts server-side only
  • Use HTTPS for all endpoints
  • Implement rate limiting
  • Store sensitive data securely

Performance

  • Cache frequently accessed data
  • Use connection pooling
  • Implement request timeouts
  • Consider using webhooks for payment confirmation

User Experience

  • Provide clear error messages
  • Show transaction progress
  • Handle network issues gracefully
  • Support transaction retries