A payment provider for Medusa.js 2.0 that accepts Solana cryptocurrency payments, with built-in protection against price volatility through payment session expiration, and real-time currency conversion via external API.
- Accept Solana (SOL) cryptocurrency payments in your Medusa store.
- Convert cart totals from fiat to SOL using fixed rates or a CoinGecko integration.
- Payment session expiration to lock in prices for a configurable duration.
- Handles partial payments and price updates gracefully.
- Monitors the blockchain for payment confirmation via a scheduled job.
- Support for payment authorization and capture workflows, including automated fund transfer to a cold storage wallet.
- Medusa.js 2.0 project (find our recommended template here: MedusaJS 2.0 Template)
- Node.js 16 or higher
- A Solana wallet address for cold storage and a mnemonic phrase for deriving payment addresses.
npm install medusa-payment-solanaAdd the module to your medusa-config.js. The available options are:
| Option | Type | Description | Required |
|---|---|---|---|
passPhrase |
string |
Your 12 or 24-word BIP39 mnemonic phrase. Used to generate unique, one-time payment addresses. | Yes |
coldStorageWallet |
string |
The public key of the wallet where funds will be transferred after a payment is captured. | Yes |
rpcUrl |
string |
The URL of the Solana RPC endpoint. Defaults to Solana's public testnet. Use a mainnet RPC for production. | No |
sessionExpirationSeconds |
number |
The time in seconds a payment session is valid before the price is renewed. Defaults to 300 (5 minutes). |
No |
currencyConverter |
object |
Configuration for currency conversion. See Currency Conversion. | No |
Example medusa-config.js:
module.exports = defineConfig({
// ...
modules: [
// ...
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
resolve: "medusa-payment-solana",
id: "solana",
options: {
passPhrase: process.env.SOLANA_MNEMONIC,
coldStorageWallet: process.env.SOLANA_COLD_STORAGE_WALLET,
rpcUrl: "https://api.testnet.solana.com", // Use mainnet for production
sessionExpirationSeconds: 600, // 10 minutes
currencyConverter: {
provider: "coingecko",
apiKey: process.env.COINGECKO_API_KEY,
}
}
}
]
}
}
]
});Make sure to set the SOLANA_MNEMONIC environment variable in your .env file:
SOLANA_MNEMONIC="word word word word word word word word word word word word"
If you don't know what a mnemonic prhase is, or how to generate one you can use script node scripts/generatePassPhrase.js to get one. (DONT SHARE WITH ANYONE)
To automatically monitor and process payments, you must set up a scheduled job. Create a file backend/src/jobs/check-payments.ts with the following content:
import { checkPaymentsJob } from 'medusa-payment-solana';
// Export the job function
export default checkPaymentsJob;
// Configure the job schedule (e.g., every 5 minutes)
export const config = {
name: "check-solana-payments",
schedule: "*/5 * * * *",
};This job ensures orders are processed even if the shopper leaves the website after payment.
The module supports two currency conversion options, configured via the currencyConverter object.
-
default- Uses fixed exchange rates:- 1 USD = 0.0075 SOL
- 1 EUR = 0.008 SOL
-
coingecko- Uses real-time rates from the CoinGecko API.
To use the CoinGecko provider, update your Medusa config and set the COINGECKO_API_KEY in your backend .env file.
// In medusa-config.js, within the provider options:
currencyConverter: {
provider: "coingecko",
apiKey: process.env.COINGECKO_API_KEY
}- Go to Settings > Regions.
- Select the region where you want to enable Solana payments.
- In the "Payment Providers" section, check the box next to "solana".
- Save the changes.
The module is designed to protect merchants from cryptocurrency price volatility.
- Initiation: When a customer selects Solana, a payment session is created with a calculated SOL amount and an expiration date (e.g., 5 minutes in the future).
- Monitoring: A scheduled job periodically checks for payments.
- Verification:
- If the session is active and the full amount is received, the payment is authorized.
- If the session has expired, the system checks if the payment was made before the expiration time. If so, it's authorized.
- If the payment is late or partial, the system recalculates the price. If the received amount covers the new price, it's authorized. Otherwise, the session is updated with the new price and a new expiration time, allowing the customer to pay the remaining balance.
- Capture & Transfer: Once authorized, the payment is captured, the order is placed, and the funds are automatically transferred to your cold storage wallet.
This ensures fairness: the customer has a reasonable window to pay the quoted price, and the merchant is protected if the payment is late and the market has moved.
Here is a framework-agnostic guide to integrating the Solana payment module into your storefront.
Your checkout page should periodically poll your Medusa backend to get the latest payment session information. The correct way to do this with the storefront SDK is to retrieve the cart by its ID and then find the Solana payment session within its payment_collection.
// Example: Based on the Medusa Storefront SDK
// This function polls for the latest cart information.
async function checkPaymentStatus(cartId, currentPaymentSessionId) {
// Use the SDK to retrieve the cart
const { cart: updatedCart } = await sdk.store.cart.retrieve(cartId);
// Find the Solana payment session in the cart
const solanaSession = updatedCart?.payment_collection?.payment_sessions?.find(
(session) => session.id === currentPaymentSessionId
);
return solanaSession;
}The data object within the Solana payment session will look like this:
{
"id": "sol_175149...",
"amount": 20,
"currency_code": "eur",
"status": "pending",
"created_at": "2025-07-02T22:06:36.654Z",
"updated_at": "2025-07-02T22:12:02.578Z",
"sol_amount": 0.153527289,
"received_sol_amount": 0.1,
"expiration_date": "2025-07-02T22:17:02.578Z",
"solana_one_time_address": "GCfKDFEv49rGvfVHSCT2fLLsvg27UYarbaZLFyaHWUBC"
}Use the payment session data to build your UI.
// --- Pseudo-code for UI logic ---
// Extract data from the session
const {
sol_amount,
received_sol_amount,
solana_one_time_address,
expiration_date
} = solanaPaymentSession.data;
// 1. Calculate the remaining amount
const remainingAmount = Math.max(0, sol_amount - received_sol_amount);
// 2. Create the payment URL for the QR code
// The URL should only request the *remaining* amount.
const paymentUrl = `solana:${solana_one_time_address}?amount=${remainingAmount.toFixed(9)}`;
// 3. Render the UI
// - Display total price (sol_amount), amount paid (received_sol_amount), and remaining amount.
// - Show a countdown timer based on expiration_date.
// - Generate a QR code from paymentUrl.
// - If the session is expired, show a message explaining that the system is verifying the payment.When your polling detects that the payment session status is authorized or captured, the order has been successfully processed by the backend.
At this point, you should complete the cart, which turns it into a finalized order. The example storefront uses a helper function for this, which is a good practice.
// Example:
if (solanaPaymentSession.status === 'authorized' || solanaPaymentSession.status === 'captured') {
// The `placeOrder` function is a helper that likely calls the
// Medusa SDK to complete the cart and then handles redirecting
// the user to the order confirmation page.
await placeOrder();
}MIT