Subscription Plan

A subscription plan lets a merchant publish billing terms that users can accept. After a user subscribes, the merchant or an approved puller can collect up to the plan amount each billing period.

This guide shows the full flow as building blocks. The merchant creates a plan, the subscriber accepts it, and the merchant or puller collects payments from the resulting subscription PDA.

Install

pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token

Create A Plan

The merchant owns the plan. The plan PDA is derived from the merchant address and planId.

import { address, createClient } from '@solana/kit';
import { solanaLocalRpc } from '@solana/kit-plugin-rpc';
import { signer } from '@solana/kit-plugin-signer';
import { findPlanPda, subscriptionsProgram } from '@solana/subscriptions';
const merchantClient = createClient()
.use(signer(merchantSigner))
.use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' }))
.use(subscriptionsProgram());
const planId = 1n;
const tokenMint = address('TOKEN_MINT_ADDRESS_HERE');
const amount = 5_000_000n;
const periodHours = 720n;
const metadataUri = 'https://example.com/plan.json';
const destinations = [merchantSigner.address];
const pullers = [address('PULLER_WALLET_ADDRESS_HERE')];
await merchantClient.subscriptions.instructions
.createPlan({
planId,
mint: tokenMint,
amount,
periodHours,
endTs: 0n,
destinations,
pullers,
metadataUri,
})
.sendTransaction();
const [planPda] = await findPlanPda({
owner: merchantSigner.address,
planId,
});

Update A Plan

The merchant can update mutable plan fields after creation. Existing subscribers keep the terms they accepted, while new subscribers accept the current plan terms.

A few rules apply:

  • A finite endTs can only be shortened, never extended or cleared (PlanEndTsCannotExtend).
  • On a Sunset plan you can remove pullers (the new set must be a subset of the current one) to revoke a compromised puller; status, endTs, and metadata stay frozen.
  • Edits work during a plan's final billing period as long as endTs is unchanged.
import { PlanStatus } from '@solana/subscriptions';
const updatedMetadataUri = 'https://example.com/updated-plan.json';
const updatedPullers = [address('NEW_PULLER_WALLET_ADDRESS_HERE')];
await merchantClient.subscriptions.instructions
.updatePlan({
owner: merchantSigner,
planPda,
status: PlanStatus.Active,
endTs: 0n,
pullers: updatedPullers,
metadataUri: updatedMetadataUri,
})
.sendTransaction();

Subscribe

The subscriber accepts the current plan terms. The subscription PDA is derived from the plan PDA and subscriber address.

import { createClient } from '@solana/kit';
import { solanaLocalRpc } from '@solana/kit-plugin-rpc';
import { signer } from '@solana/kit-plugin-signer';
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import {
fetchMaybeSubscriptionAuthority,
findSubscriptionAuthorityPda,
findSubscriptionDelegationPda,
subscriptionsProgram,
} from '@solana/subscriptions';
const subscriberClient = createClient()
.use(signer(subscriberSigner))
.use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' }))
.use(subscriptionsProgram());
const [subscriberAta] = await findAssociatedTokenPda({
mint: tokenMint,
owner: subscriberSigner.address,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [subscriptionAuthorityPda] = await findSubscriptionAuthorityPda({
user: subscriberSigner.address,
tokenMint,
});
const subscriptionAuthority = await fetchMaybeSubscriptionAuthority(
subscriberClient.rpc,
subscriptionAuthorityPda,
);
if (!subscriptionAuthority.exists) {
await subscriberClient.subscriptions.instructions
.initSubscriptionAuthority({
tokenMint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
userAta: subscriberAta,
})
.sendTransaction();
}
await subscriberClient.subscriptions.instructions
.subscribe({
merchant: merchantSigner.address,
planId,
tokenMint,
})
.sendTransaction();
const [subscriptionPda] = await findSubscriptionDelegationPda({
planPda,
subscriber: subscriberSigner.address,
});

Collect A Payment

The merchant or a whitelisted puller signs collection. When the plan uses a destination allowlist, the owner of the receiver token account must be listed in destinations.

const receiverAta = address('MERCHANT_TOKEN_ACCOUNT_ADDRESS_HERE');
await merchantClient.subscriptions.instructions
.transferSubscription({
caller: merchantOrPullerSigner,
delegator: subscriberSigner.address,
tokenMint,
subscriptionPda,
planPda,
amount: 200_000n,
receiverAta,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
})
.sendTransaction();

Cancel And Revoke

Cancelling marks the subscription as ending. Revoking closes the subscription PDA after the cancellation expiry has elapsed. The subscriber signs both transactions.

await subscriberClient.subscriptions.instructions
.cancelSubscription({
subscriber: subscriberSigner,
planPda,
subscriptionPda,
})
.sendTransaction();
// Run this after the cancelled subscription's expiresAtTs has elapsed.
await subscriberClient.subscriptions.instructions
.revokeSubscription({
authority: subscriberSigner,
planPda,
subscriptionPda,
})
.sendTransaction();

Resume A Subscription

A cancelled subscription can be reactivated before it is revoked. Once revoked, the subscription account is closed and the subscriber must subscribe again. Resume requires the subscriber's SubscriptionAuthority for the plan's mint, which the program validates (owner, mint, and init_id) and rejects if it is stale or re-initialized.

await subscriberClient.subscriptions.instructions
.resumeSubscription({
subscriber: subscriberSigner,
planPda,
tokenMint,
})
.sendTransaction();

Notes

  • amount is in base units. For a 6-decimal token, 5_000_000 means 5 tokens.
  • The TypeScript SDK fetches live plan terms during subscribe when you omit them.
  • The Rust SubscribeBuilder needs the expected plan terms. Fetch and decode the plan account first, then pass those fields through SubscribeData.
  • Only the merchant or a wallet listed in pullers can collect payments.
  • The subscriber signs setup, cancel, and revoke transactions. The merchant or approved puller signs collection transactions.

Is this page helpful?

Table of Contents

Edit Page
© 2026 Solana Foundation. All rights reserved.