Accept USDC subscriptions in your Solana app

By the end of this page, your app has three things:

After that, every renewal is charged on schedule, a failed one is retried on the paid tiers, and your route keeps giving the right answer, without more code from you.

The full code for Express, Next.js and NestJS is in the quickstart. This page explains what each piece does, and what happens after the first payment.

The customer approves once

A subscription on Solana is a permission the customer gives once. They sign the plan's terms: this amount, this often. From then on, the plan can collect up to that amount each period, and never more. The rule is enforced by the Solana Subscriptions program, not by Mesub.

The customer stays in control. They can cancel at any time, from their side, and nothing is charged after the period they are in. Their money stays in their wallet until each payment.

What Mesub adds is everything after the signature: sending each payment when it is due, retrying the ones that fail, and telling your server who has access.

Before you start

Step 1: publish a plan

In the Mesub dashboard, open Plans, then New plan. Give it a name, a price, a token, a period, and the wallet that receives the money. An end date is optional.

Your organization's wallet signs the plan's creation, so the plan is yours on chain. To make sure only your wallets can ever receive the money, list them as the plan's destinations: they cannot be changed later. Mesub turns its name into a short slug: a plan named Pro becomes pro. Your code uses the slug everywhere.

Step 2: give your server the API key

In Developers, then Keys, create the project's API key. It starts with SUB_ and is shown once. Put it in your server's environment, then install the SDK:

MESUB_API_KEY=SUB_...
npm install @mesub/node

The key stays on your server. The browser never sees it.

Step 3: mount the routes and gate a route

Your server needs two things. The routes are what the button calls to subscribe. The gate is the question you ask before serving anything paid. With Express:

import { Mesub } from "@mesub/node";
import { mesubRoutes } from "@mesub/node/express";

const mesub = new Mesub(); // reads MESUB_API_KEY

// What the button calls. Mount it after your own login.
app.use(
  "/api/mesub",
  yourLogin,
  mesubRoutes({
    customer: (req) => (req.user ? { external_id: req.user.id } : null),
  }),
);

// A paid route.
app.get("/api/analytics", yourLogin, async (req, res) => {
  if (!(await mesub.hasAccess({ external_id: req.user.id }, "pro"))) {
    res.status(402).json({ error: "The Pro plan is needed." });
    return;
  }
  res.json(buildAnalytics(req.user));
});

customer must come from a session your server verified, never from what the browser sends. Next.js and NestJS have the same two pieces, as a route handler and a guard: see the quickstart.

Step 4: put the button on a page

npm install @mesub/react
import { MesubProvider, SubscribeButton } from "@mesub/react";
import "@mesub/react/styles.css";

<MesubProvider endpoint="/api/mesub">
  <SubscribeButton plan="pro">Subscribe to Pro</SubscribeButton>
</MesubProvider>;

No React, or your own design? Create the subscription from your server instead, with subscriptions.create in the @mesub/node README, and show the wallet prompt in your own UI.

Try it

Sign in to your site, click the button, and pick the customer's wallet. The wallet asks for two approvals: the terms, then the first payment. Once the payment lands:

That is the finish line. You have a paying subscriber.

What happens after the first payment?

Mesub charges every renewal on schedule and handles the ones that fail. Your code stays as it is.

WhenWhat Mesub does
Near the end of each periodReads the customer's wallet and tells you, with the subscription.renewal_upcoming webhook, whether the next payment can go through. If it cannot and the customer gave an email, Mesub emails them
The payment is dueSends it. Your wallet receives the price, and you get subscription.renewed
The payment failsThe subscription turns unpaid. On the Dev and Business tiers, Mesub retries 3 times by default, each retry a quarter of a period later and one day at most, and access stays on while retries run. On Free, you retry by hand and access is off meanwhile
Every retry failedThe subscription turns stopped and hasAccess answers false
The customer cancelsAccess stays on until the end of the period they paid for

Your gate stays the same through all of it: hasAccess answers from every attempt Mesub made, not only from the account on chain.

Who does what

WhereWhat it doesWhat it holds
Your pagesAsk the wallet for its two approvalsNothing secret
Your serverCreates subscriptions, checks access, receives webhooksThe API key
MesubSends each payment, retries a missed one, records every attemptNo funds

Mesub holds no funds: each payment moves from the customer's wallet to your receiving wallet.

What to know before you ship

Next