All articles
solanasubscriptionsdelegationspaymentstoken-2022pinocchiokitbuilders

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 link

Classic 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

Pointers
ItemValue
Program IDDe1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44
Repogithub.com/solana-foundation/subscriptions
ImplementationRust / Pinocchio + Codama IDL
TypeScript@solana/subscriptions (Kit plugin)
Rust clientsubscriptions crate
Demosolana-subscriptions-program.vercel.app · local webapp/
AuditCantina — see repo audits/

Core model

text
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
Three authorization models
ModelUse whenPull instruction
FixedOne-time cap (escrow-like allowance), optional expirytransferFixed
RecurringBudget that resets each period (day/week/month…)transferRecurring
Subscription + PlanMerchant publishes terms; user subscribes; pullers charge each periodtransferSubscription

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

bash
pnpm add @solana/subscriptions
# peers: @solana/kit (+ rpc/signer plugins as in Kit docs)
typescript
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 assembly

Local 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)

typescript
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 SOL

2. Create fixed delegation

typescript
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 PDA

3. Create recurring delegation

typescript
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 SOL

4. Pull (delegatee)

typescript
// 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 → transferHookAccounts

5. Revoke / reclaim rent

  • revokeDelegation — close fixed/recurring PDA, rent to payer
  • revokeSubscriptionAuthority — 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

  1. Merchant createPlan — pricing, period, puller whitelist, optional end_ts (rent ≈ 0.00430824 SOL)
  2. User has SA initialized for that mint
  3. User subscribe — creates SubscriptionDelegation (rent ≈ 0.00196968 SOL)
  4. Approved puller transferSubscription each billing period
  5. User cancelSubscription (end of period) or dual-signed cancelSubscriptionNow; optional resumeSubscription before terminal revoke
  6. Merchant updatePlan / deletePlan when allowed
typescript
// 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.md before mainnet amounts matter.

Rent table (approx, from upstream README)

Recoverable rent on close
Step~SOL
Enable SA0.00162864
Create plan0.00430824
Subscribe0.00196968
Fixed delegation0.00219240
Recurring delegation0.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

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

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

Solana Subscriptions program: technical how-to | devrels.xyz