Solana Payments with Jupiter

solanajupiterweb3typescriptpayment-system

Complete implementation of a payment system using Jupiter for token swaps and Solana for transactions, allowing users to pay with any token while receiving USDC

Building a Solana Payment System with Jupiter Integration

This comprehensive guide demonstrates how to build a payment system on Solana that accepts any SPL token as payment while settling in USDC. The system leverages Jupiter's aggregation protocol for seamless token swaps and implements a secure, server-side transaction architecture.

System Architecture Overview

The payment system is designed with modularity, security, and performance in mind, separating concerns across distinct layers:

    1. Token Discovery Engine: Dynamically fetches user-owned tokens and their real-time USD prices
    1. Payment Processing Pipeline: Handles server-side transaction creation and client-side signing
    1. Jupiter Integration Layer: Facilitates token-to-USDC swaps via Jupiter's API
    1. Security Framework: Implements robust validation and error handling

This architecture keeps sensitive operations server-side, maintains user control over transaction signing, and ensures reliable settlement in USDC.

Token Discovery, Dynamic Pricing, and Secure Payment Flow

Core Token Fetching Logic

The fetchTokens function powers the payment system by discovering user-owned tokens, fetching real-time prices, and calculating the amount needed for a USD-based payment (e.g., $40/hour).

import { PublicKey } from '@solana/web3.js';
import { getTokenAccounts, getMints, getPrices, getMetadata, getPrice } from '@/lib/solana';
import { mintFromSymbol, mintDecimals } from '@/lib/constants';
import BigNumber from 'bignumber.js';
import config from '@/lib/config';

interface TokenInfo {
  mint: string;
  address: string;
  amount: string;
  value: string;
  decimals: number;
  metadata: { name: string; symbol: string; image: string };
  unitAmount: string;
}

const threshold = new BigNumber(40); // $40 minimum value

async function fetchTokens(userKey: string): Promise<TokenInfo[]> {
  const tokenAccounts = await getTokenAccounts(userKey);
  const mintAddresses = tokenAccounts.map((x) => x.data.mint.toBase58());
  const [mints, prices] = await Promise.all([
    getMints(mintAddresses),
    getPrices(mintAddresses),
  ]);

  const tokens = await Promise.all(
    tokenAccounts.map(async (accountData) => {
      try {
        const mint = accountData.data.mint.toBase58();
        const mintData = mints[mint];
        const amount = accountData.data.amount.dividedBy(
          new BigNumber(10).pow(mintData.decimals),
        );

        const price = prices[mint]?.usdPrice;
        if (!price) return null;

        const totalValue = amount.multipliedBy(new BigNumber(price));
        if (totalValue.isLessThan(threshold)) return null;

        const metadata = await getMetadata(mint);
        const unitAmount = threshold.dividedBy(price).toFixed(mintData.decimals);

        return {
          mint,
          address: accountData.pubkey,
          amount: amount.toFixed(2),
          value: totalValue.toFixed(2),
          decimals: mintData.decimals,
          metadata,
          unitAmount,
        } as TokenInfo;
      } catch (e: any) {
        return null;
      }
    }),
  );

  const filteredTokens = tokens.filter(
    (token) => token !== null,
  ) as TokenInfo[];

  const solMint = mintFromSymbol["SOL"];
  const solDecimals = mintDecimals["SOL"];
  const solBalance = await config.SOL_RPC.getBalance(new PublicKey(userKey));
  const solAmount = new BigNumber(solBalance).dividedBy(
    new BigNumber(10).pow(solDecimals),
  );
  const price = await getPrice(solMint);
  if (price !== null) {
    const priceBN = new BigNumber(price);
    const solValue = solAmount.multipliedBy(priceBN);
    if (solValue.isGreaterThan(threshold)) {
      filteredTokens.push({
        mint: solMint,
        address: userKey,
        amount: solAmount.toFixed(2),
        value: solValue.toFixed(2),
        decimals: solDecimals,
        metadata: {
          name: "Solana",
          symbol: "SOL",
          image: "/solanaLogo.svg",
        },
        unitAmount: threshold.dividedBy(priceBN).toFixed(solDecimals),
      });
    }
  }

  return filteredTokens;
}

This function queries the Solana blockchain to retrieve a user's token accounts, extracts mint addresses, and fetches mint data and USD prices concurrently using Promise.all for efficiency. Each token's balance is converted to a human-readable format using BigNumber to handle decimal precision accurately. Tokens with a total USD value below the $40 threshold are filtered out to ensure only viable payment options are presented. The unitAmount field calculates the exact amount of each token needed for a $40 payment, rounded to the token's decimal precision. Native SOL is handled separately, fetching its balance and price, and included if its value exceeds the threshold. The function returns a TokenInfo array with metadata (name, symbol, image) for UI rendering, ensuring a user-friendly payment selection experience. Error handling is robust, gracefully skipping invalid tokens to maintain system reliability.

Caching Strategy

The getMints function optimizes performance by caching mint data, reducing redundant blockchain queries.

import { PublicKey } from '@solana/web3.js';
import { db } from '@/lib/firebase';
import { fetchMints } from '@/lib/solana';

interface DecodedMint {
  decimals: number;
  supply: string;
  [key: string]: any;
}

async function getMints(mints: string[]): Promise<Record<string, DecodedMint>> {
  const missingMints: PublicKey[] = [];

  const cachedData = await Promise.all(
    mints.map(async (mint, index) => {
      const cacheMint = await db.collection("mint").doc(mint).get();

      if (cacheMint.exists) {
        return cacheMint.data() as DecodedMint;
      } else {
        missingMints.push(new PublicKey(mint));
        return null;
      }
    }),
  );

  const filteredCachedData = cachedData.filter(
    (x) => x !== null,
  ) as DecodedMint[];
  const fetchedData = await fetchMints(missingMints);
  const combinedData: Record<string, DecodedMint> = {};

  filteredCachedData.forEach((data, idx) => {
    combinedData[mints[idx]] = data;
  });

  return { ...combinedData, ...fetchedData };
}

The getMints function checks a Firestore database for cached mint data before making blockchain RPC calls, significantly reducing latency and costs. It iterates through the provided mint addresses, retrieving cached data where available and marking missing mints for fetching. The fetchMints function is called only for uncached mints, and the results are combined with cached data into a single Record. This caching strategy minimizes Solana RPC calls, which are resource-intensive, and ensures scalability by reusing previously fetched data. The function maintains type safety with the DecodedMint interface and handles errors implicitly through Firestore's robust querying, making it suitable for high-traffic applications.

Metadata Management

The getMetadata function retrieves and caches token metadata to enhance the user interface with token names, symbols, and images.

import { db } from '@/lib/firebase';
import { getTokenInfo } from '@/lib/solana';

interface Metadata {
  name: string;
  symbol: string;
  image: string;
}

async function getMetadata(mint: string): Promise<Metadata> {
  const cachedMetadata = await db.collection("metadata").doc(mint).get();
  if (cachedMetadata.exists) {
    const metadata = cachedMetadata.data() as any;
    return {
      name: metadata.name,
      symbol: metadata.symbol,
      image: metadata.image,
    };
  }

  const metadata = await getTokenInfo(mint);
  await db.collection("metadata").doc(mint).set(metadata);
  return {
    name: metadata.name,
    symbol: metadata.symbol,
    image: metadata.image,
  };
}

This function first checks Firestore for cached token metadata, returning it immediately if available to reduce latency. If not cached, it fetches metadata using getTokenInfo (e.g., from Solana's Metaplex protocol or another source) and stores it in Firestore for future use. The returned Metadata object includes the token's name, symbol, and image URL, which are used in the UI to display recognizable token information. This caching approach minimizes external API calls, improves performance, and ensures consistent UI rendering. The function is designed for reusability and integrates seamlessly with the token discovery pipeline.

User Interface Components

The TokenPicker component provides an intuitive interface for selecting payment tokens, handling dynamic pricing and user interactions.

Token Picker Component

"use client";

import { useWallet } from "@solana/wallet-adapter-react";
import { TokenInfo } from "@/actions/types";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { ClipLoader } from "react-spinners";
import { LogOutIcon } from "lucide-react";

export type TokenPickerProps = {
  tokens: TokenInfo[];
  selectedToken: TokenInfo | undefined;
  setSelectedToken: (token: TokenInfo) => void;
  handlePayment: () => void;
  quantity: number;
  loading: boolean;
};

export function TokenPicker({
  tokens,
  selectedToken,
  setSelectedToken,
  handlePayment,
  quantity,
  loading,
}: TokenPickerProps) {
  const { disconnect } = useWallet();

  if (loading) {
    return (
      <div className="flex justify-center items-center h-full w-full gap-5">
        <ClipLoader size={50} color={"#123abc"} loading={loading} />
        Fetching tokens
      </div>
    );
  }

  if (!selectedToken) {
    return <span className="px-2">No tokens</span>;
  }

  return (
    <div className="flex flex-col w-full">
      {selectedToken && (
        <>
          <div className="flex items-center space-x-2 w-full">
            <Select
              onValueChange={(value) =>
                setSelectedToken(
                  tokens.find((token) => token.metadata.symbol === value)!,
                )
              }
            >
              <SelectTrigger className="flex-1 flex items-center justify-between rounded-md border border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
                <span>{selectedToken.metadata.symbol}</span>
                <img
                  src={selectedToken.metadata.image}
                  alt={selectedToken.metadata.symbol}
                  className="w-7 h-7"
                />
              </SelectTrigger>
              <SelectContent>
                <div className="px-4 py-2 font-medium">
                  Pay per hour | 50 USD
                </div>
                {tokens.map((token) => (
                  <SelectItem
                    key={token.metadata.symbol}
                    value={token.metadata.symbol}
                  >
                    <div className="grid grid-cols-[auto_auto_1fr] gap-4 items-center">
                      <span>{token.metadata.symbol}</span>
                      <img
                        src={token.metadata.image}
                        alt={token.metadata.symbol}
                        width={28}
                        height={28}
                      />
                      <span className="text-right">{Number(token.value)}</span>
                    </div>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <Button
              type="button"
              onClick={handlePayment}
              className="flex-1 bg-[#7c3aed] text-white rounded-md hover:bg-[#6d28d9] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#7c3aed]"
            >
              Pay {(
                Number(selectedToken.unitAmount || 0) * (quantity === 0 ? 1 : quantity)
              ).toFixed(2)} {selectedToken.metadata.symbol}
            </Button>
            <Button
              type="button"
              onClick={disconnect}
              className="p-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
            >
              <LogOutIcon className="h-5 w-5" />
            </Button>
          </div>
        </>
      )}
    </div>
  );
}

The TokenPicker component, built with shadcn/ui's Select and Button components, provides a user-friendly interface for selecting a payment token. It displays a loading state with a spinner during token fetching, a fallback message if no tokens are available, or a dropdown of available tokens with their symbols, images, and USD values. The selected token's details are shown in the trigger, and the payment button dynamically displays the required token amount based on the selected quantity. The disconnect button, using the useWallet hook, allows users to disconnect their wallet. The component is responsive, accessible with proper focus states, and integrates with the authentication system via @solana/wallet-adapter-react. It handles edge cases like missing tokens and provides clear visual feedback, enhancing the user experience.

Transaction Processing Architecture

The transaction processing system balances security and user control by handling transaction creation server-side and signing client-side.

Server-Side Transaction Creation

The createTransaction server action constructs Solana transactions, integrating Jupiter for token swaps when needed.

// app/actions/createTransaction.ts
"use server";

import config from "@/lib/config";
import {
  TEN,
  USDC_AMOUNT,
  USDC_DECIMALS,
  USDC_MINT,
  USDC_MINT_KEY,
} from "@/lib/constants";
import { getJupInstructions } from "@/lib/jup";
import { createPayInstruction, getTransaction } from "@/lib/solana";
import { getAccount, getAssociatedTokenAddress } from "@solana/spl-token";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import BigNumber from "bignumber.js";

export async function createTransaction(
  pubkey: string,
  quantity: string,
  currency: string,
  decimals: number,
) {
  try {
    const signer = new PublicKey(pubkey);
    const senderATA = await getAssociatedTokenAddress(USDC_MINT_KEY, signer);
    const senderAccount = await getAccount(config.SOL_RPC, senderATA);
    if (!senderAccount.isInitialized) throw new Error("sender not initialized");
    if (senderAccount.isFrozen) throw new Error("sender frozen");

    const quantityBN = new BigNumber(quantity);
    const amount = String(
      USDC_AMOUNT.multipliedBy(quantityBN)
        .times(TEN.pow(USDC_DECIMALS))
        .integerValue(BigNumber.ROUND_FLOOR),
    );

    const instructions: TransactionInstruction[] = [];
    const lookupTableAddresses: string[] = [];
    if (currency !== USDC_MINT) {
      const { addressLookupTableAddresses, jupInstructions } =
        await getJupInstructions(pubkey, currency, quantityBN);

      instructions.push(...jupInstructions);
      lookupTableAddresses.push(...addressLookupTableAddresses);
    } else {
      const tokens = BigInt(amount);
      if (tokens > senderAccount.amount) throw new Error("insufficient funds");
    }

    const payInstruction = await createPayInstruction(
      amount,
      signer,
      senderATA,
    );
    instructions.push(payInstruction);

    return await getTransaction(instructions, signer, lookupTableAddresses);
  } catch (error: any) {
    console.error("Transaction creation failed:", error.message);
    throw error;
  }
}

This server action validates the user's associated token account (ATA) for USDC, ensuring it's initialized and not frozen. For non-USDC payments, it fetches swap instructions from Jupiter via getJupInstructions, which are added to the transaction. For USDC payments, it verifies sufficient funds. The createPayInstruction function adds a transfer instruction to send USDC to the recipient, and getTransaction assembles the final transaction with address lookup tables for optimization. The use of BigNumber ensures precise calculations, and comprehensive error handling prevents invalid transactions. Keeping RPC calls server-side protects sensitive endpoints and reduces client-side complexity.

Jupiter Integration for Token Swaps

The getJupInstructions function integrates with Jupiter's API to enable token-to-USDC swaps.

// lib/jup.ts
import ky from 'ky';
import { TransactionInstruction } from '@solana/web3.js';
import { PAYMENT_REFERENCE } from '@/lib/constants';
import { JupInstruction, JupSwapInstructionsResponse } from '@/types/jup';
import BigNumber from 'bignumber.js';

interface JupInstructions {
  jupInstructions: TransactionInstruction[];
  addressLookupTableAddresses: string[];
}

async function getJupQuote(inputMint: string, quantity: BigNumber): Promise<any> {
  // Simulated quote fetching logic
  return await ky.get(https://quote-api.jup.ag/v6/quote?inputMint=${inputMint}&outputMint=${USDC_MINT}&amount=${quantity.toString()}).json();
}

export async function getJupInstructions(
  signer: string,
  inputMint: string,
  quantity: BigNumber,
): Promise<JupInstructions> {
  const quoteResponse = await getJupQuote(inputMint, quantity);
  const {
    setupInstructions,
    swapInstruction,
    cleanupInstruction,
    otherInstructions,
    addressLookupTableAddresses,
  } = await ky
    .post("https:">//quote-api.jup.ag/v6/swap-instructions", {
      json: {
        quoteResponse,
        trackingAccount: PAYMENT_REFERENCE.toBase58(),
        userPublicKey: signer,
        wrapAndUnwrapSol: true,
        useSharedAccounts: false,
        dynamicComputeUnitLimit: false,
        skipUserAccountsRpcCalls: true,
        asLegacyTransaction: false,
        useTokenLedger: false,
      },
      retry: {
        limit: 5,
        statusCodes: [408, 413, 429, 500, 502, 503, 504, 422],
        methods: ["post"],
        delay: (attemptCount) => 0.3  2  (attemptCount - 1)  1000,
      },
    })
    .json<JupSwapInstructionsResponse>();

  const jupInstructions: TransactionInstruction[] = [];
  if (setupInstructions && setupInstructions.length > 0)
    jupInstructions.push(
      ...setupInstructions.map((ix: JupInstruction) =>
        deserializeInstruction(ix),
      ),
    );
  jupInstructions.push(deserializeInstruction(swapInstruction));
  if (cleanupInstruction)
    jupInstructions.push(deserializeInstruction(cleanupInstruction));
  if (otherInstructions && otherInstructions.length > 0)
    jupInstructions.push(
      ...otherInstructions.map((ix: JupInstruction) =>
        deserializeInstruction(ix),
      ),
    );

  return { jupInstructions, addressLookupTableAddresses };
}

function deserializeInstruction(ix: JupInstruction): TransactionInstruction {
  return new TransactionInstruction({
    keys: ix.accounts.map((key) => ({
      pubkey: new PublicKey(key.pubkey),
      isSigner: key.isSigner,
      isWritable: key.isWritable,
    })),
    programId: new PublicKey(ix.programId),
    data: Buffer.from(ix.data, 'base64'),
  });
}

This function fetches a swap quote from Jupiter's v6 API and requests swap instructions, specifying the input token, quantity, and output as USDC. It includes setup, swap, and cleanup instructions, as well as address lookup tables to optimize transaction size. The ky library handles HTTP requests with exponential backoff retries for reliability. The deserializeInstruction helper converts Jupiter's instruction format into Solana's TransactionInstruction format. The function supports native SOL wrapping/unwrapping and ensures the payment reference is included for tracking. This integration enables seamless token swaps across multiple DEXs, optimizing for the best price and reliability.

Client-Side Transaction Handling

The handleSolanaPayment function manages client-side transaction signing and submission.

import { useWallet } from '@solana/wallet-adapter-react';
import { VersionedTransaction } from '@solana/web3.js';
import { toast } from '@/components/ui/toast';
import { z } from 'zod';
import { createTransaction } from '@/app/actions/createTransaction';
import { MeetingSchema } from '@/types/meeting';
import { TokenInfo } from '@/actions/types';
import { Buffer } from 'buffer';

async function handleSolanaPayment(data: z.infer<typeof MeetingSchema>) {
  const { publicKey, signTransaction } = useWallet();
  const selectedToken: TokenInfo | undefined = / retrieved from state /;

  try {
    if (!publicKey || !signTransaction || !selectedToken) {
      toast({ title: "Wallet not connected" });
      return;
    }

    const quantity = data.hours.length.toString();
    const { mint, decimals } = selectedToken;
    const transaction = await createTransaction(
      publicKey.toBase58(),
      quantity,
      mint,
      decimals,
    );
    if (!transaction) return;

    const deserializedTransaction = VersionedTransaction.deserialize(
      Buffer.from(transaction, "base64"),
    );
    const signedTransaction = await signTransaction(deserializedTransaction);
    const serializedTransaction = Buffer.from(
      signedTransaction.serialize(),
    ).toString("base64");
    const formData = JSON.stringify({
      ...data,
      dob: data.dob.toISOString(),
    });
    const signature = await sendTransaction(serializedTransaction, formData);

    toast({
      title: "Payment done. Meeting booked.",
      description: (
        <a
          href={https://solana.fm/tx/${signature}}
          className="text-blue-500"
          target="_blank"
          rel="noopener noreferrer"
        >
          Check Transaction
        </a>
      ),
    });
  } catch (error: any) {
    toast({ title: error.message || "Payment processing failed." });
  }
}

This function, running client-side, validates that the wallet is connected and a token is selected before proceeding. It calls the createTransaction server action to generate a transaction, which is deserialized into a VersionedTransaction. The user signs the transaction using their wallet via signTransaction, and the signed transaction is serialized and sent to the sendTransaction action for submission. The function integrates with a MeetingSchema (validated with Zod) to include meeting data, such as booking details. Upon successful submission, a toast notification provides a link to view the transaction on Solana.fm. Error handling ensures user-friendly feedback for issues like a disconnected wallet or failed transactions, enhancing the payment experience.

Transaction Confirmation and Validation

The sendTransaction server action confirms and validates transactions, ensuring integrity and updating system state.

// app/actions/confirmTransaction.ts
"use server";

import { db } from "@/lib/firebase";
import config from "@/lib/config";
import {
  HOUR_PRICE,
  mintDecimals,
  PAYMENT_REFERENCE,
  RIKI_PUBKEY,
  TEN,
  USDC_MINT,
} from "@/lib/constants";
import {
  AccountLayout,
  transferCheckedInstructionData,
} from "@solana/spl-token";
import { VersionedTransaction, PublicKey } from "@solana/web3.js";
import BigNumber from "bignumber.js";
import { generateMeet } from "@/lib/googleMeet";

export async function sendTransaction(transaction: string, data: string) {
  try {
    const deserializedTransaction = VersionedTransaction.deserialize(
      Buffer.from(transaction, "base64"),
    );
    const signature = await config.SOL_RPC.sendRawTransaction(
      deserializedTransaction.serialize(),
      {
        skipPreflight: true,
        maxRetries: 0,
      },
    );

    let confirmedTx: any = null;
    const latestBlockHash = await config.SOL_RPC.getLatestBlockhash();
    const confirmTransactionPromise = config.SOL_RPC.confirmTransaction(
      {
        signature,
        blockhash: deserializedTransaction.message.recentBlockhash,
        lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
      },
      "confirmed",
    );

    while (!confirmedTx) {
      confirmedTx = await Promise.race([
        confirmTransactionPromise,
        new Promise((resolve) =>
          setTimeout(() => {
            resolve(null);
          }, 2000),
        ),
      ]);

      if (!confirmedTx) {
        await config.SOL_RPC.sendRawTransaction(
          deserializedTransaction.serialize(),
          {
            skipPreflight: true,
            maxRetries: 0,
          },
        );
      }
    }

    if (!confirmedTx) throw new Error("Transaction confirmation failed");

    const response = await fetchTransaction(signature);
    const { message } = response.transaction;
    const versionedTransaction = new VersionedTransaction(message);
    const instructions = versionedTransaction.message.compiledInstructions;

    const payInstruction = instructions.pop();
    if (!payInstruction) throw new Error("missing transfer instruction");

    const { amount: rawAmount } = transferCheckedInstructionData.decode(
      payInstruction.data,
    );
    const [source, mint, destination, owner, paymentReference] =
      payInstruction.accountKeyIndexes.map(
        (index: number) =>
          versionedTransaction.message.staticAccountKeys[index],
      );

    const sellerATA = await config.SOL_RPC.getAccountInfo(
      destination,
      "confirmed",
    );
    if (!sellerATA) throw new Error("error fetching ata info");
    const decodedSellerATA = AccountLayout.decode(sellerATA.data);

    const price = BigNumber(HOUR_PRICE)
      .times(TEN.pow(mintDecimals["USDC"]))
      .integerValue(BigNumber.ROUND_FLOOR);
    const signer = owner.toBase58();
    const seller = decodedSellerATA.owner.toBase58();
    const currency = decodedSellerATA.mint.toBase58();
    const amount = rawAmount.toString(16);
    const quotient = new BigNumber(amount, 16).dividedBy(price);

    if (!quotient.isInteger()) throw new Error("amount not transferred");
    if (PAYMENT_REFERENCE.toString() !== paymentReference.toString())
      throw new Error("wrong app reference");
    if (seller !== RIKI_PUBKEY.toBase58()) throw new Error("wrong seller");
    if (currency !== USDC_MINT) throw new Error("wrong currency");

    await db.collection(cryptoPayment).doc(signature).set({
      signature,
      signer,
      currency,
      amount,
      timestamp: new Date().toISOString(),
    });

    await generateMeet(data);

    return signature;
  } catch (error: any) {
    console.error("An error occurred:", error);
    throw error;
  }
}

async function fetchTransaction(signature: string) {
  const retryDelay = 400;
  const response = await config.SOL_RPC.getTransaction(signature, {
    commitment: "confirmed",
    maxSupportedTransactionVersion: 0,
  });
  if (response) {
    return response;
  } else {
    await new Promise((resolve) => setTimeout(resolve, retryDelay));
    return fetchTransaction(signature);
  }
}

This server action deserializes the signed transaction, submits it to the Solana blockchain, and waits for confirmation with a retry mechanism to handle network delays. It validates the transaction by checking the transfer instruction's amount, recipient (seller), currency (USDC), and payment reference, ensuring the payment is correct and directed to the intended recipient (RIKI_PUBKEY). The BigNumber library ensures precise amount calculations, and the transaction is logged in Firestore for record-keeping. Upon successful validation, it triggers the generateMeet function to book a meeting, integrating with external services like Google Meet. The fetchTransaction helper retries fetching transaction details to handle network latency, ensuring reliability. Comprehensive error handling and validation prevent fraudulent or incorrect transactions, making the system robust for production use.