An Introduction to Solana Pay and how to Integrate it into Your Next.js App
Tags: solana, web3, solanapay, nextjs
Published: 2023-03-28 11:52:39

In this guide we'll walk through the following:

1. What is Solana Pay?
2. When to use Solana Pay?
3. How to use Solana Pay in a next.js app and create an app where people can scan a QR code to buy SPL tokens in exchange for SOL

By the end of this guide, we will build something like this:

{% embed https://youtube.com/shorts/a3Zr_LI1kEg?feature=share %}

Let's get started!

## What is Solana Pay?

![What the hell is Solana Pay](https://y.yarn.co/86e8371d-2a5b-4538-8d04-61ef45470811_text.gif)

Solana Pay is a standard protocol and set of reference implementations that enable developers to incorporate decentralized payments into their apps and services.

Using Solana Pay you can create [Transfer Request](https://docs.solanapay.com/spec#specification-transfer-request) and [Transaction Request](https://docs.solanapay.com/spec#specification-transaction-request) via a standard URL (which can also be encoded to be used in a QR). These can be used for creating a payment request and doing something once the payment is completed.

## When to use Solana Pay?

Solana Pay has a lot of use cases from complex payments on the web to using it in IRL stores to collect payments instantly. Some use cases for it:

- Selling NFTs
- Selling SPL tokens
- Creating an E-commerce website
- Accepting SOL/any SPL tokens in your IRL store
- Many more, you can go crazy with it!

## Using Solana Pay in our Next.js app

### Creating a Next.js App

We'll first create a new next.js app, if you already have an app you can skip this section and go to the installing dependencies section. Run this command in your terminal:

```bash
npx create-next-app solana-pay-demo
```

I am using the following config but feel free to use whatever you like!

![Create a new next app using create-next-app for solana pay demo](https://cdn.hashnode.com/res/hashnode/image/upload/v1679923239945/2a93ed8a-d762-4420-92ac-04ccf0ba519a.png)

Once the app is created, open it in your favourite editor and let's get started!

![Let's get started building our solana dapp](https://media.tenor.com/I4d1QyAghmUAAAAM/hooray-letsgo.gif)

### Installing the dependencies

We are going to need these packages for integrating solana pay:

```bash
npm i @solana/pay @solana/spl-token @solana/web3.js bignumber.js bs58 # npm

yarn add @solana/pay @solana/spl-token @solana/web3.js bignumber.js bs58 # yarn
```

For rendering the QR code:

```bash
npm i react-qr-code # npm

yarn add react-qr-code # yarn
```

Finally, we are going to create our token for selling but if you already have an SPL token no need to install these:

```bash
npm i @metaplex-foundation/js @metaplex-foundation/mpl-token-metadata # npm

yarn add @metaplex-foundation/js @metaplex-foundation/mpl-token-metadata # yarn
```

## Creating the SPL token

Create a new file called `scripts/create-token.mjs` and add the following:

```typescript
import {
  bundlrStorage,
  keypairIdentity,
  Metaplex,
} from "@metaplex-foundation/js";
import { createCreateMetadataAccountV2Instruction } from "@metaplex-foundation/mpl-token-metadata";
import {
  createAssociatedTokenAccountInstruction,
  createInitializeMintInstruction,
  createMintToInstruction,
  getAssociatedTokenAddress,
  getMinimumBalanceForRentExemptMint,
  MINT_SIZE,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
  Connection,
  Keypair,
  SystemProgram,
  TransactionMessage,
  VersionedTransaction,
} from "@solana/web3.js";
import base58 from "bs58";
import "dotenv/config";

const endpoint = "https://api.devnet.solana.com";
const solanaConnection = new Connection(endpoint);

const MINT_CONFIG = {
  numDecimals: 6,
  numberTokens: 10000,
};

const MY_TOKEN_METADATA = {
  name: "Solana Pay Demo",
  symbol: "SPD",
  description: "A demo token for Solana Pay",
  image: "https://cryptologos.cc/logos/solana-sol-logo.png",
};

const ON_CHAIN_METADATA = {
  name: MY_TOKEN_METADATA.name,
  symbol: MY_TOKEN_METADATA.symbol,
  uri: "",
  sellerFeeBasisPoints: 0,
  creators: null,
  collection: null,
  uses: null,
};

const uploadMetadata = async (wallet, tokenMetadata) => {
  const metaplex = Metaplex.make(solanaConnection)
    .use(keypairIdentity(wallet))
    .use(
      bundlrStorage({
        address: "https://devnet.bundlr.network",
        providerUrl: endpoint,
        timeout: 60000,
      })
    );

  const { uri } = await metaplex.nfts().uploadMetadata(tokenMetadata);
  console.log(`Arweave URL: `, uri);
  return uri;
};

const createNewMintTransaction = async (
  connection,
  payer,
  mintKeypair,
  destinationWallet,
  mintAuthority,
  freezeAuthority
) => {
  const metaplex = Metaplex.make(solanaConnection)
    .use(keypairIdentity(payer))
    .use(
      bundlrStorage({
        address: "https://devnet.bundlr.network",
        providerUrl: endpoint,
        timeout: 60000,
      })
    );

  const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);
  const metadataPDA = metaplex
    .nfts()
    .pdas()
    .metadata({ mint: mintKeypair.publicKey });
  const tokenATA = await getAssociatedTokenAddress(
    mintKeypair.publicKey,
    destinationWallet
  );

  const txInstructions = [];

  txInstructions.push(
    SystemProgram.createAccount({
      fromPubkey: payer.publicKey,
      newAccountPubkey: mintKeypair.publicKey,
      space: MINT_SIZE,
      lamports: requiredBalance,
      programId: TOKEN_PROGRAM_ID,
    }),
    createInitializeMintInstruction(
      mintKeypair.publicKey,
      MINT_CONFIG.numDecimals,
      mintAuthority,
      freezeAuthority,
      TOKEN_PROGRAM_ID
    ),
    createAssociatedTokenAccountInstruction(
      payer.publicKey,
      tokenATA,
      payer.publicKey,
      mintKeypair.publicKey
    ),
    createMintToInstruction(
      mintKeypair.publicKey,
      tokenATA,
      mintAuthority,
      MINT_CONFIG.numberTokens * Math.pow(10, MINT_CONFIG.numDecimals)
    ),
    createCreateMetadataAccountV2Instruction(
      {
        metadata: metadataPDA,
        mint: mintKeypair.publicKey,
        mintAuthority: mintAuthority,
        payer: payer.publicKey,
        updateAuthority: mintAuthority,
      },
      {
        createMetadataAccountArgsV2: {
          data: ON_CHAIN_METADATA,
          isMutable: true,
        },
      }
    )
  );
  const latestBlockhash = await connection.getLatestBlockhash();
  const messageV0 = new TransactionMessage({
    payerKey: payer.publicKey,
    recentBlockhash: latestBlockhash.blockhash,
    instructions: txInstructions,
  }).compileToV0Message();

  const transaction = new VersionedTransaction(messageV0);
  transaction.sign([payer, mintKeypair]);
  return transaction;
};

const main = async () => {
  const userWallet = Keypair.fromSecretKey(
    base58.decode(process.env.WALLET_PRIVATE_KEY)
  );
  let metadataUri = await uploadMetadata(userWallet, MY_TOKEN_METADATA);
  ON_CHAIN_METADATA.uri = metadataUri;

  let mintKeypair = Keypair.generate();

  const newMintTransaction = await createNewMintTransaction(
    solanaConnection,
    userWallet,
    mintKeypair,
    userWallet.publicKey,
    userWallet.publicKey,
    userWallet.publicKey
  );

  const transactionId = await solanaConnection.sendTransaction(
    newMintTransaction
  );
  console.log(
    `Succesfully minted ${MINT_CONFIG.numberTokens} ${
      ON_CHAIN_METADATA.symbol
    } to ${userWallet.publicKey.toString()}.`
  );
  console.log(
    `View Transaction: https://explorer.solana.com/tx/${transactionId}?cluster=devnet`
  );
};

main();
```

This script is taking the metadata entered, and uploading it to the bundlr network, then creates a token on solana, finally, it mints the number of tokens we entered at the top.

Before running the script you need to do the following:

- Install dotenv:

```bash
npm i dotenv # npm

yarn add dotenv # yarn
```

- Add your private key in `.env` (Make sure to add .env in .gitignore or you can loose your funds) with the name `WALLET_PRIVATE_KEY`
- Update `MY_TOKEN_METADATA` with metadata of your own token and `MINT_CONFIG` with the number of tokens you wanna mint

Now, you can run the script!

```bash
node scripts/create-token.mjs
```

![Run the create token script to create an SPL token](https://cdn.hashnode.com/res/hashnode/image/upload/v1679990159215/d9f8a05b-da14-4d28-a27e-91719542e189.png)

Click on the transaction URL and copy the token address, since we are going to need it later.

![Copy the token address](https://cdn.hashnode.com/res/hashnode/image/upload/v1679988165036/a438a259-fea8-4e4c-b05b-aaed75a07b0a.png)

### Creating the UI

The UI of our application will be really simple. We will have one input for selecting the number of tokens you want to buy, a button for generating the QR code and a QR code.

So, let's add the input and button:

```typescript
<main className={styles.main}>
  <div>
    <input
      type="number"
      value={quantity}
      onChange={(e) => setQuantity(Number(e.target.value))}
    />
    <button onClick={createPayment}>Generate QR</button>
  </div>
</main>
```

As you can see we need a state for storing the quantity:

```typescript
const [quantity, setQuantity] = useState(0);
```

We also need to create a `createPayment` like this:

```typescript
const reference = useMemo(() => Keypair.generate().publicKey, []);

const createPayment = async () => {
  if (!quantity) {
    return;
  }

  const apiUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/makeTransaction?amount=${quantity}&reference=${reference}`;

  const urlParams = {
    link: new URL(apiUrl),
    label: "Solana Pay Demo",
    message: "Thanks for buying our tokens!",
  };
  const solanaUrl = encodeURL(urlParams);
  setQrCode(solanaUrl.href);
};
```

Here, we are first creating a reference.

<details data-node-type="hn-details-summary"><summary>You might be wondering what exactly is a reference here. Click me to know more about it</summary><div data-type="detailsContent">When a customer scans a QR code to make a payment, it can be tricky to update the website to show that the payment went through. Solana pay has a unique way of handling this. We generate a special public key called a "reference" and attach it to the payment. Then we use that public key to find the payment and update the website.</div></details>

Then, we are checking if there is a quantity, if not we will do nothing. Later we are creating an API URL (we will create this later) and pass it to encode it and finally set it in a state.

So, create a new file called `.env.local` and add the following for now:

```typescript
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

This creates a new public environment variable that we can use in our app.

We also need to create a state for storing the URL:

```typescript
const [qrCode, setQrCode] = useState<string | null>(null);
```

Finally, let's use the `react-qr-code` package to render it:

```typescript
{
  qrCode && <QRCode value={qrCode} />;
}
```

You can [customise](https://github.com/rosskhanas/react-qr-code#api) it as you want but I am going to leave it just like this for now!

If you open up localhost you will be able to see a screen like this:

![Generate QR code localhost](https://cdn.hashnode.com/res/hashnode/image/upload/v1679924542671/5e6ae558-7bff-4a45-ba66-a55b1c457763.png)

If you scan this QR code it will erroQRout, but wait for a while!

![Patience You must learn - Solana Pay](https://media.tenor.com/3OZu_n2PL-QAAAAC/star-wars-yoda.gif)

## Building the API

We will now build the API that we passed in for generating our QR Code. So how Solana Pay works is, it will make 2 requests to the same router. One will be a `GET` request and another will be a `POST` request. The `GET` request is responsible for giving the icon and a label as metadata and the `POST` request is where the actual transaction stuff happens.

Here is a great illustration from the [Solana Pay docs](https://docs.solanapay.com/) that explains this:

![How solana pay works](https://docs.solanapay.com/assets/images/transaction-request-flow-dark-5bfeda8bfaf7cc66a89aed62b1b2db60.png)

So, let's start building the API! Create a new file `makeTransaction.ts` in `src/pages/api` and add the following:

```typescript
import { NextApiRequest, NextApiResponse } from "next";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "GET") {
    return get(res);
  } else if (req.method === "POST") {
    return await post(req, res);
  } else {
    return res.status(405).json({ error: "Method not allowed" });
  }
};

export default handler;
```

Here, we are simply checking what the request method is and based on that calling some functions:

- If it is a `GET` method we are calling the get method and passing in res
- If it is a `POST` method we are calling the post function and passing in req as well as res
- Finally, if it is neither of the two we are sending the 405 status with `Method not allowed` error.

Now, let's write the get function:

```typescript
const get = (res: NextApiResponse) => {
  const label = "Buy some tokens";
  const icon = "https://cryptologos.cc/logos/solana-sol-logo.png";

  return res.status(200).json({
    label,
    icon,
  });
};
```

The get function is pretty simple, we are just returning a label and an icon for the solana pay payment that the user will be able to see when the transaction pops up.

Now, let's head over to the post function which is where the actual magic happens:

```typescript
const post = async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const { reference, amount } = req.query as {
      reference: string;
      amount: string;
    };

    const { account } = req.body as {
      account: string;
    };

    if (parseInt(amount) === 0) {
      return res.status(400).json({ error: "Can't checkout with charge of 0" });
    }

    if (!reference) {
      return res.status(400).json({ error: "No reference provided" });
    }

    if (!account) {
      return res.status(400).json({ error: "No account provided" });
    }
  } catch (err) {
    console.error("error:", err);
    return res.status(500).json({ error: "error creating transaction" });
  }
};
```

We are adding a try-catch block for checking for errs and in the try block we will first get `reference` and `amount` from `req.query` and `account` from the body. Then we are doing some normal checks to make sure that we get the correct data when the API is called.

Then we need to create a connection with the solana devnet so add this:

```typescript
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
```

You can use any RPC you want but for now, I will use the official solana RPC.

Then, for the SPL token transfer we need the wallet private key, so create a new variable in `.env.local` called `WALLET_PRIVATE_KEY` and add in your private key. We will get it like this:

```typescript
const walletPrivateKey = process.env.WALLET_PRIVATE_KEY as string;

if (!walletPrivateKey) {
  res.status(500).json({ error: "Wallet private key not available" });
}

const walletKeyPair = Keypair.fromSecretKey(base58.decode(walletPrivateKey));
```

Import Keypair and bs58 like this:

```typescript
import { Keypair } from "@solana/web3.js";
import base58 from "bs58";
```

Now, we will add in the required addresses:

```typescript
const buyerPublicKey = new PublicKey(account);
const walletPublicKey = new PublicKey(
  "FW79xRL1yks1Y9bD8NSB888YGRmyEq4SCMYhFodHLWh9"
);
const tokenAddress = new PublicKey(
  "FtQBZ2jsDLvXLdeo12LkMndjvLm6kAUWmdntiaxsWQqu"
);
```

The `tokenAddress` is the address of the token that we deployed earlier and the `walletPublicKey` is the address of the wallet where you want the funds to go and the wallet which has the tokens that need to be sent to the buyer.

Finally we need the tokenAccountAddresses of the seller and buyer:

```typescript
const buyerTokenAddress = await getOrCreateAssociatedTokenAccount(
  connection,
  walletKeyPair,
  tokenAddress,
  buyerPublicKey
).then((account) => account.address);

const sellerTokenAddress = await getAssociatedTokenAddress(
  tokenAddress,
  walletPublicKey
);
```

You can import `getAssociatedTokenAddress` and `getOrCreateAssociatedTokenAccount` from `@solana/spl-token` :

```typescript
import {
  getAssociatedTokenAddress,
  getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
```

Now, let's add the transaction and `transferInstruction` of the SOL from the buyer to seller like this:

```typescript
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(
  "finalized"
);

const transaction = new Transaction({
  blockhash,
  feePayer: buyerPublicKey,
  lastValidBlockHeight,
});

const transferInstruction = SystemProgram.transfer({
  fromPubkey: buyerPublicKey,
  toPubkey: walletPublicKey,
  lamports: parseInt(amount) * LAMPORTS_PER_SOL,
});

transferInstruction.keys.push({
  pubkey: new PublicKey(reference),
  isSigner: false,
  isWritable: false,
});
```

And the token transfer instruction from the seller to the buyer:

```typescript
const tokenInstruction = createTransferCheckedInstruction(
  shopTokenAddress,
  tokenAddress,
  buyerTokenAddress,
  walletPublicKey,
  parseInt(amount) * 10 ** 6,
  6
);

tokenInstruction.keys.push({
  pubkey: walletPublicKey,
  isSigner: true,
  isWritable: false,
});
```

Finally, we need to add the instructions to the transaction, partially sign the transaction, and serialise it:

```typescript
transaction.add(transferInstruction, tokenInstruction);
transaction.partialSign(walletKeyPair);

const serializedTransaction = transaction.serialize({
  requireAllSignatures: false,
});
```

Now, we can return it like this:

```typescript
const base64 = serializedTransaction.toString("base64");

return res.status(200).json({
  transaction: base64,
  message: `Buying ${amount} ${amount === "1" ? "token" : "tokens"}`,
});
```

The final API code looks something like this:

```typescript
import {
  createTransferCheckedInstruction,
  getAssociatedTokenAddress,
  getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import base58 from "bs58";
import { NextApiRequest, NextApiResponse } from "next";

export type MakeTransactionOutputData = {
  transaction: string;
  message: string;
};

const post = async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const { reference, amount } = req.query as {
      reference: string;
      amount: string;
    };

    const { account } = req.body as {
      account: string;
    };

    if (parseInt(amount) === 0) {
      return res.status(400).json({ error: "Can't checkout with charge of 0" });
    }

    if (!reference) {
      return res.status(400).json({ error: "No reference provided" });
    }

    if (!account) {
      return res.status(400).json({ error: "No account provided" });
    }

    const connection = new Connection(
      "https://api.devnet.solana.com",
      "confirmed"
    );

    const walletPrivateKey = process.env.WALLET_PRIVATE_KEY as string;

    if (!walletPrivateKey) {
      res.status(500).json({ error: "Wallet private key not available" });
    }

    const walletKeyPair = Keypair.fromSecretKey(
      base58.decode(walletPrivateKey)
    );

    const buyerPublicKey = new PublicKey(account);
    const walletPublicKey = new PublicKey(
      "FW79xRL1yks1Y9bD8NSB888YGRmyEq4SCMYhFodHLWh9"
    );
    const tokenAddress = new PublicKey(
      "FtQBZ2jsDLvXLdeo12LkMndjvLm6kAUWmdntiaxsWQqu"
    );

    const buyerTokenAddress = await getOrCreateAssociatedTokenAccount(
      connection,
      walletKeyPair,
      tokenAddress,
      buyerPublicKey
    ).then((account) => account.address);

    const shopTokenAddress = await getAssociatedTokenAddress(
      tokenAddress,
      walletPublicKey
    );

    const { blockhash, lastValidBlockHeight } =
      await connection.getLatestBlockhash("finalized");

    const transaction = new Transaction({
      blockhash,
      feePayer: buyerPublicKey,
      lastValidBlockHeight,
    });

    const transferInstruction = SystemProgram.transfer({
      fromPubkey: buyerPublicKey,
      toPubkey: walletPublicKey,
      lamports: parseInt(amount) * LAMPORTS_PER_SOL,
    });

    transferInstruction.keys.push({
      pubkey: new PublicKey(reference),
      isSigner: false,
      isWritable: false,
    });

    const tokenInstruction = createTransferCheckedInstruction(
      shopTokenAddress,
      tokenAddress,
      buyerTokenAddress,
      walletPublicKey,
      parseInt(amount) * 10 ** 6,
      6
    );

    tokenInstruction.keys.push({
      pubkey: walletPublicKey,
      isSigner: true,
      isWritable: false,
    });

    transaction.add(transferInstruction, tokenInstruction);
    transaction.partialSign(walletKeyPair);

    const serializedTransaction = transaction.serialize({
      requireAllSignatures: false,
    });

    const base64 = serializedTransaction.toString("base64");

    return res.status(200).json({
      transaction: base64,
      message: `Buying ${amount} ${amount === "1" ? "token" : "tokens"}`,
    });
  } catch (err) {
    console.error("error:", err);
    return res.status(500).json({ error: "error creating transaction" });
  }
};

const get = (res: NextApiResponse) => {
  const label = "Buy some tokens";
  const icon = "https://cryptologos.cc/logos/solana-sol-logo.png";

  return res.status(200).json({
    label,
    icon,
  });
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "GET") {
    return get(res);
  } else if (req.method === "POST") {
    return await post(req, res);
  } else {
    return res.status(405).json({ error: "Method not allowed" });
  }
};

export default handler;
```

Solana Pay doesn't allow testing on localhost, so we will use [ngrok](https://ngrok.com/) to test it.

Go to [ngrok](https://ngrok.com/), [create an account/login](https://dashboard.ngrok.com/), and [download it on your machine](https://ngrok.com/download).

Once ngrok is downloaded run this command:

```typescript
ngrok http 3000
```

This will start a tunnel and serve your web app

![Start a new tunnel using ngrok](https://cdn.hashnode.com/res/hashnode/image/upload/v1679990249016/9cc6709e-f425-4549-8ac9-b27d973e9f79.png)

Copy the forwarding ngrok URL and replace localhost in your env variable with it.

If you now try generating a new QR code and scanning it via your phone, a transaction will pop up and you will be able to buy some SPL tokens in exchange for SOL 🥳

![Solana Pay working](https://cdn.hashnode.com/res/hashnode/image/upload/v1679989671487/46301d49-b8b4-4ab2-af91-f07434159cbe.png)

## Conclusion

That's it for this guide. Hope you learned what is solana pay and how to use it in your Next.js app! Massive shoutout to [0xMukesh](https://twitter.com/0xMukesh) for helping me with this guide and answering my stupid questions 🫡.

### Useful links

[**GitHub Repo**](https://github.com/avneesh0612/solana-pay)

[**Solana Pay**](https://solanapay.com/)

[**Connect with me**](https://links.avneesh.tech/)