Solana Subscriptions program: technical how-to
How to use the Solana Foundation Subscriptions program (De1eg…) with @solana/subscriptions: Subscription Authority, fixed and recurring delegations, merchant plans, pull payments, Token-2022 hooks, rent, and security gotchas. Pinocchio program + Kit SDK.
devrels.xyz/a/225short linkClassic SPL Token accounts allow one approved delegate at a time. That breaks real products: a user might want a monthly SaaS pull, a daily bot allowance, and a merchant agreement on the same USDC ATA without juggling revokes.
The solana-foundation/subscriptions program (docs: solana.com/docs/payments/subscriptions) puts a program-owned Subscription Authority (SA) PDA as that single delegate (u64::MAX approval), then gates every spend behind a separate delegation or subscription record. The SA cannot move funds alone.
Program identity and stack
| Item | Value |
|---|---|
| Program ID | De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 |
| Repo | github.com/solana-foundation/subscriptions |
| Implementation | Rust / Pinocchio + Codama IDL |
| TypeScript | @solana/subscriptions (Kit plugin) |
| Rust client | subscriptions crate |
| Demo | solana-subscriptions-program.vercel.app · local webapp/ |
| Audit | Cantina — see repo audits/ |
Core model
User ATA (mint M)
│ Approve once: delegate = SubscriptionAuthority PDA
▼
SA (user, mint) ──checks──► FixedDelegation
──checks──► RecurringDelegation
──checks──► SubscriptionDelegation (+ Plan)
Puller (delegatee / merchant puller) calls transfer*
→ program CPI TransferChecked only if PDA terms allow| Model | Use when | Pull instruction |
|---|---|---|
| Fixed | One-time cap (escrow-like allowance), optional expiry | transferFixed |
| Recurring | Budget that resets each period (day/week/month…) | transferRecurring |
| Subscription + Plan | Merchant publishes terms; user subscribes; pullers charge each period | transferSubscription |
Token-2022 mints work, including TransferHook: hook accounts are forwarded into TransferChecked. Destinations that require incoming memo (MemoTransfer) are not supported — the program does not emit a Memo CPI.
Install and client setup
pnpm add @solana/subscriptions
# peers: @solana/kit (+ rpc/signer plugins as in Kit docs)import { address, createClient } from "@solana/kit"
import { solanaLocalRpc } from "@solana/kit-plugin-rpc" // example plugin names may track Kit releases
import { signer } from "@solana/kit-plugin-signer"
import { subscriptionsProgram } from "@solana/subscriptions"
const client = createClient()
.use(signer(walletSigner))
.use(solanaLocalRpc({ rpcUrl: "https://api.devnet.solana.com" }))
.use(subscriptionsProgram())
// Overlay builders also export get*OverlayInstructionAsync for custom tx assemblyLocal full stack from the repo: just setup && just build && just test; demo UI just webapp-run (RPC :8899, API :3001, UI :5173).
Flow A — Fixed or recurring delegation (technical)
1. Init Subscription Authority (once per user + mint)
const TOKEN_PROGRAM = address(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
) // or Token-2022 program id
await client.subscriptions.instructions
.initSubscriptionAuthority({
tokenMint: mint,
userAta: userAta,
tokenProgram: TOKEN_PROGRAM,
})
.sendTransaction()
// Rent (approx from README): SA ≈ 0.00162864 SOL2. Create fixed delegation
await client.subscriptions.instructions
.createFixedDelegation({
tokenMint: mint,
delegatee: pullerWallet,
nonce: 0n, // unique per (user, mint, delegatee) space — see IDL/PDA seeds
amount: 1_000_000n, // raw token units
expiryTs: BigInt(Math.floor(Date.now() / 1000) + 86_400), // optional window
})
.sendTransaction()
// Rent ≈ 0.00219240 SOL for FixedDelegation PDA3. Create recurring delegation
await client.subscriptions.instructions
.createRecurringDelegation({
tokenMint: mint,
delegatee: pullerWallet,
nonce: 1n,
// period length + per-period amount + optional overall expiry — fields per IDL
amount: 100_000n,
// periodSeconds / period config: use current SDK types from package
})
.sendTransaction()
// Rent ≈ 0.00235944 SOL4. Pull (delegatee)
// Fixed
await client.subscriptions.instructions
.transferFixed({
/* mint, user, delegatee, amount, destination ATA, token program, … */
})
.sendTransaction()
// Recurring — enforces remaining budget for current period
await client.subscriptions.instructions
.transferRecurring({
/* … */
})
.sendTransaction()
// Token-2022 + transfer hook: plugin appends hook accounts automatically.
// Manual overlay: resolveTransferHookAccounts → transferHookAccounts5. Revoke / reclaim rent
revokeDelegation— close fixed/recurring PDA, rent to payerrevokeSubscriptionAuthority— revoke SPL delegate + close SA- Abandoned PDAs after SA close/reinit:
RevokeAbandonedDelegation/RevokeAbandonedSubscription(recorded payer)
Do not treat closeSubscriptionAuthority alone as a full kill switch without reading security notes: same-slot reinit can reuse init_id; use explicit revoke paths.
Flow B — Merchant subscription plan
- Merchant
createPlan— pricing, period, puller whitelist, optionalend_ts(rent ≈ 0.00430824 SOL) - User has SA initialized for that mint
- User
subscribe— createsSubscriptionDelegation(rent ≈ 0.00196968 SOL) - Approved puller
transferSubscriptioneach billing period - User
cancelSubscription(end of period) or dual-signedcancelSubscriptionNow; optionalresumeSubscriptionbefore terminal revoke - Merchant
updatePlan/deletePlanwhen allowed
// Shape only — field names track the published package / IDL
await client.subscriptions.instructions
.createPlan({
/* owner, mint, amount, period, pullers[], endTs?, metadata? */
})
.sendTransaction()
await client.subscriptions.instructions
.subscribe({
/* plan, userAta, mint, … */
})
.sendTransaction()
await client.subscriptions.instructions
.transferSubscription({
/* plan, subscription, amount, destination, puller, … */
})
.sendTransaction()Queries: fetchPlansForOwner, fetchSubscriptionsForUser, fetchDelegationsByDelegator / fetchDelegationsByDelegatee, isSubscriptionAuthorityInitialized.
Events (indexers)
Self-CPI events in the Codama IDL, including transfer events (FixedTransferEvent, RecurringTransferEvent, SubscriptionTransferEvent) and lifecycle (SubscriptionCreatedEvent, cancelled/resumed, PlanUpdatedEvent). Decode via generated clients.
Sponsored rent / gasless
Kit payer() distinct from identity() can sponsor rent on create paths. Sponsored SA/open perpetual subscriptions may lock rent until the user cancels or closes authority — prefer finite expiryTs / end_ts when a relayer pays rent, and enforce quotas off-chain.
Security checklist (must-read for production)
- Active ≠ collectable. User can revoke ATA approval, freeze, empty, or close the token account while subscription still looks active on-chain. Check balance + delegate before delivering service.
- Held signatures. Durable nonce / multisig proposals can execute long after intent changed; authority-control and init+subscribe bundles have subtle revive paths — see README Security Considerations.
- init_id is slot-granular; same-slot close+reinit is not reliable revocation.
- Native SPL Multisig account owners unsupported; use Squads / Swig-style smart wallets (covered in their integration tests).
- Track audit commits in
audits/AUDIT_STATUS.mdbefore mainnet amounts matter.
Rent table (approx, from upstream README)
| Step | ~SOL |
|---|---|
| Enable SA | 0.00162864 |
| Create plan | 0.00430824 |
| Subscribe | 0.00196968 |
| Fixed delegation | 0.00219240 |
| Recurring delegation | 0.00235944 |
PDAs (Codama-generated helpers)
From the package root: findSubscriptionAuthorityPda, findFixedDelegationPda, findRecurringDelegationPda, findPlanPda, findSubscriptionDelegationPda, findEventAuthorityPda. Always use generated finders — do not hand-roll seeds out of date with the program.
Related on DevRels
- Streamflow — vesting/locks/airdrops (different problem: time-unlock streams, not pull delegations)
- SPL Token Wrap
- pay.sh / agentic HTTP pay
Summary
Use Subscriptions when you need multiple limited, revocable spend permissions on one ATA: init the SA, create fixed/recurring delegations or a merchant plan + subscribe, pull with the matching transfer* instruction via @solana/subscriptions, and index self-CPI events. Treat collectability and signature freshness as product requirements, not footnotes — the program enforces amounts and periods; it does not guarantee the user still has tokens or an approval when your backend delivers a month of service.
Keep reading
Stablecoin rails are useless at the last mile if users only have cash. MoneyGram Ramps is the Foundation’s bid to wire that last mile into SDP payments.
Want Token-2022 extensions on a classic mint (or the reverse) without convincing every venue to migrate? Token Wrap is the permissionless 1:1 wrapper.
Tempo optimizes for commercial stablecoin payments with Stripe-class distribution. Solana optimizes for open markets and apps on one fast L1. Same USDC — different jobs. Here is a practical chooser for Solana builders.
Get new articles in your inbox
Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.
