NOUVEAU : Helius acquiert Light Protocol
Bannière des abonnements et paiements récurrents sur Solana
Blog/Fondamentaux

Configurer des abonnements et des paiements récurrents sur Solana

ChercheurLostin sur X
19 min de lecture

Introduction

La facturation récurrente est un pilier du commerce en ligne. Les produits SaaS, les plateformes API, les systèmes de paie et d’innombrables autres entreprises doivent pouvoir facturer leurs clients selon un calendrier prévisible.

Jusqu’à présent, mettre en œuvre cette expérience sur Solana nécessitait de créer une infrastructure personnalisée conséquente. Le nouveau Solana Subscriptions Delegation Program, également appelé Solana Subscriptions & Allowances Program, résout ce problème grâce à une primitive onchain open source et auditée pour les paiements récurrents et les offres d’abonnement.

Un utilisateur peut désormais autoriser une seule fois de futurs transferts de tokens, sous réserve de contraintes onchain explicites. Un marchand ou collecteur approuvé peut ensuite initier les paiements sans exiger la signature de l’utilisateur. Ce programme est déjà disponible sur le mainnet et le devnet. Il prend en charge les mints du SPL Token Program d’origine et de Token-2022.

Au lieu que chaque application conçoive et sécurise son propre système de délégation, les développeurs peuvent désormais intégrer un programme standardisé. Les flux de paiement qui exigeaient auparavant plusieurs semaines de développement personnalisé peuvent être intégrés en quelques jours.

En tant que partenaire de lancement, Helius a contribué à améliorer le programme et l’utilise désormais pour assurer la facturation onchain des abonnements à ses offres API. Les clients Helius peuvent ainsi autoriser des paiements récurrents en USDC directement depuis leurs portefeuilles Solana.

Dans cet article, nous examinerons le fonctionnement du programme et l’utiliserons pour créer des flux de paiement récurrent évolutifs. Nous aborderons l’architecture Subscription Authority, les PDA représentant chaque autorisation et les trois modèles de facturation pris en charge par le programme :

  • Plafonds de dépenses fixes
  • Délégations récurrentes
  • Offres d’abonnement définies par les marchands

Pourquoi les abonnements onchain étaient difficiles à mettre en œuvre

Les programmes de tokens de Solana prennent déjà en charge la délégation de dépenses. Le propriétaire d’un compte de tokens peut utiliser les instructions Approve ou ApproveChecked pour autoriser une autre adresse à transférer ou brûler des tokens en son nom, dans la limite d’un plafond défini.

Le problème est qu’un compte de tokens ne stocke qu’un seul délégué actif et un seul montant délégué. L’approbation d’un nouveau délégué remplace le précédent et son plafond. Cela vaut pour les comptes gérés par le Token Program d’origine comme par Token-2022. Lorsque l’utilisateur approuve un second service, celui-ci remplace le premier. Le plafond natif est une valeur unique, sans notion intégrée de période de facturation, de réinitialisation du plafond ou d’état d’abonnement.

Les applications pourraient contourner ce problème en créant un compte de tokens distinct pour chaque relation de dépense, mais cela fragmente le solde de l’utilisateur et complique considérablement l’expérience du portefeuille et de l’application. Une équipe pourrait aussi développer un programme personnalisé de séquestre ou de délégation, mais elle retrouverait alors les contraintes de développement et de sécurité que le programme partagé vise précisément à supprimer.

Il manquait un moyen de transformer l’unique emplacement de délégué du Token Program en une passerelle programmable pour plusieurs autorisations indépendantes.

Découvrez le nouveau Subscriptions Delegation Program

Le Subscriptions Delegation Program ajoute cette couche programmable manquante sans modifier les programmes de tokens sous-jacents de Solana. Pour chaque paire (utilisateur, mint de token), le programme dérive une Subscription Authority, une adresse dérivée du programme (PDA) qui devient le délégué du compte de tokens de l’utilisateur pour ce mint précis. Elle est initialisée une fois, puis réutilisée par chaque abonnement ou délégation associé à cet utilisateur et à ce mint.

Lors de l’initialisation, l’utilisateur signe une transaction qui approuve la Subscription Authority avec un plafond de ~18,4 trillions, soit u64::MAX. Cette opération est sûre, car la Subscription Authority est une PDA qui ne peut signer que par l’intermédiaire du Subscriptions Delegation Program. Elle ne peut pas décider seule de transférer des tokens. 

Avant de signer un CPI vers le Token Program, le Subscriptions Delegation Program doit charger un compte d’autorisation valide et vérifier ses contraintes. Selon le modèle d’autorisation, ces contrôles peuvent inclure :

  • Le portefeuille ou service autorisé à initier le prélèvement
  • Le mint et le compte de tokens source
  • Le plafond total restant
  • Le montant maximal disponible pendant la période de facturation en cours
  • La date de début et d’expiration de l’autorisation
  • Les offres d’abonnement acceptées par l’utilisateur
  • Le marchand ou collecteur approuvé qui initie le prélèvement
  • Toute restriction de destination configurée par l’offre

Ce n’est qu’après la réussite de ces vérifications que le programme signe en tant que Subscription Authority et exécute le transfert de tokens. Si aucune autorisation active ne correspond au transfert demandé, la transaction échoue. Le plafond u64::MAX appartient donc à la passerelle contrôlée par le programme, et non à un marchand particulier. Un marchand ne reçoit que l’autorité décrite par son compte de délégation spécifique.

Le compte de tokens conserve exactement un délégué, à savoir la PDA Subscription Authority, mais le programme peut placer derrière elle de nombreuses PDA d’autorisation indépendantes. La création d’une nouvelle autorisation n’écrase aucune autorisation existante. Chacune possède son propre état, ses propres limites, son propre cycle de vie et son propre mécanisme de révocation.

Trois modèles d’autorisation

Le programme prend en charge trois modèles distincts : les délégations fixes, les délégations récurrentes et les offres d’abonnement.

Délégations fixes

Une délégation fixe autorise un portefeuille ou un service à prélever jusqu’à un montant total défini. Chaque transfert réduit le plafond restant, et la délégation peut éventuellement expirer à un horodatage Unix donné.

Ce modèle convient aux budgets plafonnés des agents, aux autorisations ponctuelles, aux droits d’achat limités dans le temps et aux autres cas où un utilisateur souhaite définir une exposition totale maximale.

Délégations récurrentes

Une délégation récurrente précise le montant qui peut être prélevé pendant chaque période. Au début de la période suivante, le montant prélevé pendant la période précédente est réinitialisé.

L’utilisateur contrôle les conditions, notamment le montant par période, la durée de la période, la date de début et l’expiration globale. Les délégations récurrentes conviennent donc aux relations continues, comme la paie, les paiements de prestataires, les allocations récurrentes ou les accords de facturation personnalisés dans lesquels le payeur définit les limites.

Offres d’abonnement

Les offres d’abonnement inversent le flux de configuration. Au lieu que chaque utilisateur définisse ses propres conditions récurrentes, un marchand publie une offre réutilisable avec un montant, une période de facturation, un mint accepté, des collecteurs autorisés et d’éventuelles restrictions de destination.

L’utilisateur examine et accepte ces conditions, ce qui crée une PDA Subscription Delegation liée à l’offre. Les conditions de facturation acceptées sont copiées dans le compte d’abonnement de l’utilisateur. Le marchand ne peut donc pas modifier discrètement le prix principal ou la période de facturation d’un abonné existant. Le propriétaire de l’offre ou un préleveur approuvé peut ensuite collecter jusqu’au montant de l’offre pendant chaque période de facturation.

Cette distinction est importante :

  • Les délégations récurrentes sont des autorisations définies par le payeur
  • Les offres d’abonnement sont des conditions publiées par le marchand auxquelles le payeur adhère explicitement

Les trois modèles utilisent la même Subscription Authority et exécutent finalement les transferts via la même architecture de délégation sous-jacente.

L’implémentation de référence

Le Subscriptions Delegation Program a été conçu et développé par Moonsong Labs en partenariat avec la Solana Foundation, puis audité par Cantina. Son code source, sa documentation et ses clients sont disponibles dans le dépôt des abonnements de la Solana Foundation.

Le programme onchain est écrit en Rust no_std avec Pinocchio. Pinocchio fournit un modèle de développement de plus bas niveau avec peu de dépendances. Par rapport à une implémentation Anchor classique, il permet au programme de mieux maîtriser l’utilisation des ressources de calcul et la taille du binaire.

Le dépôt utilise également Codama pour générer des clients TypeScript et Rust synchronisés directement depuis l’interface du programme. Pour les applications TypeScript, le package principal est :

Code
pnpm add @solana/subscriptions

Il existe aussi une application web de démonstration officielle qui fournit une implémentation de bout en bout facile à utiliser sur le devnet. Le programme prend en charge SPL Token et Token-2022. Il émet également des événements onchain de cycle de vie et de transfert que les applications et les indexeurs peuvent décoder avec l’IDL publié.

Cas d’usage : ce que les développeurs peuvent créer

Le programme est utile dès qu’un utilisateur peut définir les limites d’un futur paiement avant de savoir exactement quand le transfert aura lieu. L’utilisateur signe une fois pour établir une autorisation. Un marchand, un service, un bénéficiaire ou un agent peut ensuite initier des transferts dans ces limites.

Comme chaque accord de dépense est représenté par sa propre PDA, ces cas d’usage peuvent coexister derrière la même Subscription Authority. Un utilisateur peut payer une offre API, allouer un budget hebdomadaire à un agent d’IA et autoriser le paiement régulier d’un prestataire depuis le même compte de tokens USDC, sans qu’une autorisation n’interfère avec une autre.

Facturation récurrente des API et de l’infrastructure

Les offres d’abonnement conviennent naturellement aux produits SaaS, aux fournisseurs RPC, aux plateformes de données et aux autres services d’infrastructure. Un fournisseur peut publier une offre onchain distincte pour chaque niveau de produit, en définissant le mint de token accepté, le prix, la période de facturation, les collecteurs approuvés et les destinations de paiement autorisées. Il offre ainsi une expérience d’abonnement familière sans recourir à un prestataire de paiement par carte.

Dépenses plafonnées pour les agents d’IA

Les agents autonomes doivent pouvoir payer des API, des ressources de calcul, des données, des services de trading et d’autres ressources sans demander d’approbation humaine. Cependant, donner à un agent le contrôle illimité d’un portefeuille approvisionné crée un risque de sécurité évident.

Les délégations fixes offrent une solution plus sûre. Un utilisateur peut autoriser un agent à dépenser jusqu’à un montant précis de tokens et définir une date d’expiration stricte pour cette autorisation. Il peut également révoquer la délégation avant son expiration.

Les délégations récurrentes étendent ce même modèle. Un agent peut recevoir une allocation quotidienne pour les requêtes API ou un budget de fonctionnement hebdomadaire, le montant disponible étant réinitialisé au début de chaque période.

Paie onchain et paiements des prestataires

Les délégations récurrentes peuvent prendre en charge la paie par prélèvement, les honoraires récurrents, les subventions et les contrats de prestation. Un payeur autorise un salarié ou un prestataire à collecter jusqu’à un montant défini par période de paie. L’autorisation peut préciser le montant par période, la durée de la période, la date de début et l’expiration finale. Lorsqu’un paiement arrive à échéance, le bénéficiaire ou le service de paie soumet la transaction de transfert.

Ce fonctionnement diffère d’une transaction de paie traditionnelle par envoi. Le payeur n’envoie pas automatiquement les fonds le jour de la paie. Le bénéficiaire reçoit plutôt un droit strictement limité de prélever le montant convenu pendant chaque période. Il en résulte un accord de paiement transparent que les deux parties peuvent consulter onchain.

Les transferts et les activités de délégation peuvent être suivis grâce aux événements émis par le programme, ce qui permet de créer des tableaux de bord de paie et des intégrations comptables.

Encaissement de factures en stablecoins

Les passerelles de paiement et les plateformes de facturation B2B peuvent utiliser le programme pour remplacer les demandes de paiement répétées par des autorisations persistantes et plafonnées. Un client peut autoriser une passerelle à collecter :

  • Jusqu’à un montant total fixe pour un bon de commande
  • Jusqu’à un montant défini pendant chaque période de facturation hebdomadaire ou mensuelle
  • Le prix d’une offre standardisée du marchand à chaque cycle de facturation

La même architecture peut prendre en charge les factures récurrentes, l’acquisition commerçant, les plafonds d’utilisation, les politiques de dépenses d’entreprise et d’autres flux dans lesquels le payeur souhaite automatiser les paiements sans céder un contrôle illimité.

Micropaiements pour les contenus et les médias

Les délégations récurrentes peuvent également prendre en charge des modèles de paiement à l’usage pour les éditeurs, les plateformes de streaming, les fournisseurs de recherche et d’autres services médias.

Un utilisateur peut autoriser une enveloppe mensuelle de dépenses, débitée progressivement à chaque accès à un contenu payant. Par exemple, l’ouverture d’un article peut consommer 0,10 USDC sur une enveloppe mensuelle de 10 USDC, tandis que les rapports premium ou les flux vidéo peuvent coûter plus cher. La plateforme soumet chaque paiement lors de l’accès au contenu.

Cela permet de proposer des modèles où l’utilisateur « paie ce qu’il lit », sans exiger une signature du portefeuille pour chaque article ni imposer un abonnement fixe et indivisible. Les éditeurs disposent d’un moyen évolutif de monétiser chaque accès, tandis que les utilisateurs conservent un plafond de dépenses prévisible et peuvent révoquer l’autorisation à tout moment.

Créer un flux d’abonnement sur le devnet avec Helius

Dans cette partie du tutoriel, nous allons créer le cycle de vie d’un abonnement marchand en suivant ces étapes :

  • Un client initialise une Subscription Authority pour son compte de tokens
  • Un marchand publie une offre d’abonnement
  • Le client accepte l’offre
  • Le marchand encaisse un paiement
  • Le client annule l’abonnement

Les exemples utilisent @solana/subscriptions@0.4.0, le dernier client TypeScript publié au moment de la rédaction. Le verrouillage des versions des packages garantit la stabilité du tutoriel, même si le SDK évolue par la suite.

Nous modéliserons deux acteurs :

RôleResponsabilité
ClientPossède les tokens, initialise la Subscription Authority, souscrit à l’offre et l’annule
MarchandPublie l’offre et soumet les transactions d’encaissement

Pour le token, nous créerons un mint devnet personnalisé à six décimales et émettrons 100 tokens de test pour le client. Nous évitons ainsi de dépendre d’un faucet de stablecoins distinct tout en conservant les mêmes calculs en unités de base que les tokens à six décimales comme l’USDC.

Les paires de clés JSON locales facilitent l’exécution du flux en ligne de commande. Dans une application réelle, les transactions du client seraient normalement signées depuis un portefeuille web ou mobile, tandis que le collecteur du marchand utiliserait un signataire backend géré de manière sécurisée.

Prérequis

Ce tutoriel suppose que vous disposez des éléments suivants :

  • Une version récente de Node.js
  • pnpm
  • La CLI Solana
  • Une clé API Helius payante
  • Des SOL de devnet pour les deux portefeuilles de test

Configurer le projet

Créez un projet avec un répertoire keys pour stocker les paires de clés du client et du marchand :

Code
mkdir helius-subscriptions-devnet
cd helius-subscriptions-devnet

pnpm init
mkdir -p src keys

Ajoutez "type": "module" à package.json, puis installez les dépendances :

Code
pnpm add \
  @solana/subscriptions@0.4.0 \
  @solana/kit@6.10.0 \
  @solana/kit-plugin-rpc@0.12.1 \
  @solana/kit-plugin-signer@0.12.1 \
  @solana-program/token@0.13.0 \
  dotenv

pnpm add -D typescript tsx @types/node

Créez tsconfig.json :

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src"]
}

Créer les portefeuilles du marchand et du client

Générez une paire de clés pour chaque acteur :

Code
solana-keygen new \
  --no-bip39-passphrase \
  --outfile keys/merchant.json

solana-keygen new \
  --no-bip39-passphrase \
  --outfile keys/customer.json

Ces paires de clés sont réservées au tutoriel sur le devnet. Ne validez pas de clés de production dans un dépôt et ne stockez pas le signataire d’un marchand en production dans un fichier JSON non chiffré.

Créez .gitignore :

.gitignore
node_modules/
.env
keys/
state.json

Créez .env et ajoutez votre clé API Helius :

.env
HELIUS_API_KEY=YOUR_HELIUS_API_KEY
MERCHANT_KEYPAIR=./keys/merchant.json
CUSTOMER_KEYPAIR=./keys/customer.json

Les deux portefeuilles ont besoin de SOL pour payer les frais de transaction et créer leurs comptes PDA respectifs.

Code
export HELIUS_DEVNET_URL="https://devnet.helius-rpc.com/?api-key=YOUR_HELIUS_API_KEY"

solana airdrop 1 \
  "$(solana-keygen pubkey keys/merchant.json)" \
  --url "$HELIUS_DEVNET_URL"

solana airdrop 1 \
  "$(solana-keygen pubkey keys/customer.json)" \
  --url "$HELIUS_DEVNET_URL"

Helius fournit également un faucet devnet depuis son tableau de bord. Pour demander des SOL de devnet via le faucet Helius ou le RPC Helius, une offre Helius payante est nécessaire.

Créer un client Helius partagé

Chaque script nécessite la même connexion Helius, les mêmes plugins de programme, les mêmes valeurs de configuration et les mêmes adresses. Nous les placerons tous dans src/config.ts :

config.ts

import "dotenv/config";

import { readFileSync, writeFileSync } from "node:fs";

import {
  address,
  createClient,
  type Address,
} from "@solana/kit";
import { solanaDevnetRpc } from "@solana/kit-plugin-rpc";
import { signerFromFile } from "@solana/kit-plugin-signer";
import {
  associatedTokenProgram,
  tokenProgram,
} from "@solana-program/token";
import { subscriptionsProgram } from "@solana/subscriptions";

function requiredEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(`Missing ${name} in .env`);
  }

  return value;
}

const apiKey = requiredEnv("HELIUS_API_KEY");

export const HELIUS_RPC_URL =
  `https://devnet.helius-rpc.com/?api-key=${encodeURIComponent(apiKey)}` as const;

export const HELIUS_WS_URL =
  `wss://devnet.helius-rpc.com/?api-key=${encodeURIComponent(apiKey)}` as const;

export const MERCHANT_KEYPAIR =
  process.env.MERCHANT_KEYPAIR ?? "./keys/merchant.json";

export const CUSTOMER_KEYPAIR =
  process.env.CUSTOMER_KEYPAIR ?? "./keys/customer.json";

export const TOKEN_DECIMALS = 6;

// Five tokens when the mint has six decimals.
export const PLAN_AMOUNT = 5_000_000n;

// Keep the devnet period short so we can test multiple billing cycles.
export const PLAN_PERIOD_HOURS = 1n;

const STATE_FILE = "./state.json";

type StateJson = {
  tokenMint: string;
  planId: string;
};

export async function createAppClient(keypairPath: string) {
  return await createClient()
    .use(signerFromFile(keypairPath))
    .use(
      solanaDevnetRpc({
        rpcUrl: HELIUS_RPC_URL,
        rpcSubscriptionsUrl: HELIUS_WS_URL,
      }),
    )
    .use(tokenProgram())
    .use(associatedTokenProgram())
    .use(subscriptionsProgram());
}

export function saveState(
  tokenMint: Address,
  planId: bigint,
): void {
  writeFileSync(
    STATE_FILE,
    JSON.stringify(
      {
        tokenMint,
        planId: planId.toString(),
      },
      null,
      2,
    ),
  );
}

export function loadState(): {
  tokenMint: Address;
  planId: bigint;
} {
  const parsed = JSON.parse(
    readFileSync(STATE_FILE, "utf8"),
  ) as StateJson;

  if (!parsed.tokenMint || !parsed.planId) {
    throw new Error(
      "state.json is missing tokenMint or planId",
    );
  }

  return {
    tokenMint: address(parsed.tokenMint),
    planId: BigInt(parsed.planId),
  };
}

export function printSignature(
  label: string,
  signature: unknown,
): void {
  const value = String(signature);

  console.log(`${label}: ${value}`);
  console.log(
    `Orb: https://orb.helius.dev/tx/${value}?cluster=devnet`,
  );
}

signerFromFile définit la paire de clés chargée à la fois comme identité du client et comme payeur des frais. Le plugin RPC Helius gère la planification, la soumission et la confirmation des transactions, tandis que les plugins de tokens et d’abonnements ajoutent leurs outils respectifs pour les comptes et les instructions. Chaque script affiche une URL Orb, l’explorateur de blocs de Helius, pour la transaction obtenue.

Créer le mint de test sur le devnet

Avant d’initialiser une Subscription Authority, le client doit disposer d’un compte de tokens existant pour le mint de l’offre. Nous créerons un mint de test à six décimales, émettrons 100 tokens pour le client et créerons un compte de tokens de destination vide pour le marchand.

Le plugin de tokens de Solana Kit fournit des outils pour créer des mints et des comptes de tokens associés, ainsi que pour émettre des tokens. Créez src/00-bootstrap.ts :

00-bootstrap.ts
import { generateKeyPairSigner } from "@solana/kit";
import {
  findAssociatedTokenPda,
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";

import {
  createAppClient,
  CUSTOMER_KEYPAIR,
  MERCHANT_KEYPAIR,
  printSignature,
  saveState,
  TOKEN_DECIMALS,
} from "./config.js";

const [merchantClient, customerClient] =
  await Promise.all([
    createAppClient(MERCHANT_KEYPAIR),
    createAppClient(CUSTOMER_KEYPAIR),
  ]);

const mint = await generateKeyPairSigner();

const createMintResult =
  await merchantClient.token.instructions
    .createMint({
      newMint: mint,
      decimals: TOKEN_DECIMALS,
      mintAuthority: merchantClient.identity.address,
      freezeAuthority: null,
    })
    .sendTransaction();

const fundCustomerResult =
  await merchantClient.token.instructions
    .mintToATA({
      mint: mint.address,
      owner: customerClient.identity.address,
      mintAuthority: merchantClient.identity,
      amount: 100_000_000n,
      decimals: TOKEN_DECIMALS,
    })
    .sendTransaction();

const createMerchantAtaResult =
  await merchantClient.associatedToken.instructions
    .createAssociatedTokenIdempotent({
      mint: mint.address,
      owner: merchantClient.identity.address,
      tokenProgram: TOKEN_PROGRAM_ADDRESS,
    })
    .sendTransaction();

const [customerAta] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: customerClient.identity.address,
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

const [merchantAta] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: merchantClient.identity.address,
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

// Plan PDAs are derived from the merchant and plan ID.
// Using the current timestamp gives each test run a fresh ID.
const planId = BigInt(Date.now());

saveState(mint.address, planId);

console.log("Mint:", mint.address);
console.log("Plan ID:", planId.toString());

console.log("Customer:", customerClient.identity.address);
console.log("Customer ATA:", customerAta);

console.log("Merchant:", merchantClient.identity.address);
console.log("Merchant ATA:", merchantAta);

printSignature(
  "Create mint",
  createMintResult.context.signature,
);

printSignature(
  "Fund customer",
  fundCustomerResult.context.signature,
);

printSignature(
  "Create merchant ATA",
  createMerchantAtaResult.context.signature,
);

Exécutez le script. Il créera state.json, qui contient des informations sur le mint généré et un identifiant d’offre unique. Le client possède désormais 100 tokens de test, et le marchand dispose d’un compte de tokens vide prêt à recevoir les paiements d’abonnement.

Initialiser la Subscription Authority du client

Une Subscription Authority est créée pour une paire (customer, mint) donnée. Le compte de tokens du client doit exister avant l’initialisation.

La transaction d’initialisation crée la PDA Subscription Authority et l’approuve comme délégué du compte de tokens du client. La même autorité peut ensuite être réutilisée pour chaque délégation fixe, délégation récurrente et offre d’abonnement impliquant ce client et ce mint. Le client signe cette transaction.

Créez src/01-init-authority.ts :

01-init-authority.ts

import {
  findAssociatedTokenPda,
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
  fetchMaybeSubscriptionAuthority,
  findSubscriptionAuthorityPda,
} from "@solana/subscriptions";

import {
  createAppClient,
  CUSTOMER_KEYPAIR,
  loadState,
  printSignature,
} from "./config.js";

const customerClient =
  await createAppClient(CUSTOMER_KEYPAIR);

const { tokenMint } = loadState();

const [customerAta] = await findAssociatedTokenPda({
  mint: tokenMint,
  owner: customerClient.identity.address,
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

const [subscriptionAuthorityPda] =
  await findSubscriptionAuthorityPda({
    user: customerClient.identity.address,
    tokenMint,
  });

const existing =
  await fetchMaybeSubscriptionAuthority(
    customerClient.rpc,
    subscriptionAuthorityPda,
  );

if (existing.exists) {
  console.log(
    "Subscription Authority already exists:",
    subscriptionAuthorityPda,
  );

  process.exit(0);
}

const result =
  await customerClient.subscriptions.instructions
    .initSubscriptionAuthority({
      tokenMint,
      tokenProgram: TOKEN_PROGRAM_ADDRESS,
      userAta: customerAta,
    })
    .sendTransaction();

console.log(
  "Subscription Authority:",
  subscriptionAuthorityPda,
);

printSignature(
  "Initialize authority",
  result.context.signature,
);

Exécutez le script en tant que client. Il vérifie d’abord si la PDA existe déjà. La commande peut ainsi être relancée sans risque et évite de soumettre une transaction d’initialisation en double. Après confirmation, la Subscription Authority devient le délégué Token Program du compte de tokens du client.

Créer une offre d’abonnement marchand

Le marchand publie maintenant les conditions de facturation que les clients peuvent accepter. Une PDA Plan est dérivée de l’adresse du marchand et de l’identifiant de l’offre.

Une offre définit le mint de paiement, le montant maximal par période, la durée de la période, les collecteurs approuvés, les destinations autorisées et d’éventuelles métadonnées offchain.

Créez src/02-create-plan.ts :

02-create-plan.ts

import {
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
  fetchMaybePlan,
  findPlanPda,
} from "@solana/subscriptions";

import {
  createAppClient,
  loadState,
  MERCHANT_KEYPAIR,
  PLAN_AMOUNT,
  PLAN_PERIOD_HOURS,
  printSignature,
} from "./config.js";

const merchantClient =
  await createAppClient(MERCHANT_KEYPAIR);

const { tokenMint, planId } = loadState();

const [planPda] = await findPlanPda({
  owner: merchantClient.identity.address,
  planId,
});

const existing = await fetchMaybePlan(
  merchantClient.rpc,
  planPda,
);

if (existing.exists) {
  console.log("Plan already exists:", planPda);
  process.exit(0);
}

const result =
  await merchantClient.subscriptions.instructions
    .createPlan({
      planId,
      mint: tokenMint,

      // Five tokens per billing period.
      amount: PLAN_AMOUNT,

      // One-hour periods for this devnet test.
      periodHours: PLAN_PERIOD_HOURS,

      // No scheduled plan-wide end.
      endTs: 0n,

      // The owner of the receiving token account
      // must be included in this list.
      destinations: [
        merchantClient.identity.address,
      ],

      // An empty pullers list means only the merchant
      // can initiate collections.
      pullers: [],

      metadataUri:
        "https://example.com/helius-devnet-plan.json",

      tokenProgram: TOKEN_PROGRAM_ADDRESS,
    })
    .sendTransaction();

console.log("Plan PDA:", planPda);

printSignature(
  "Create plan",
  result.context.signature,
);

Exécutez le script en tant que marchand. Certains champs sont particulièrement importants :

amount

Les valeurs des tokens sont exprimées en unités de base. Notre mint comporte six décimales : 5 tokens correspondent donc à 5 000 000 d’unités de base. L’offre autorise le marchand à collecter un maximum cumulé de cinq tokens pendant chaque période de facturation. Le marchand peut collecter la totalité en une seule transaction ou la répartir entre plusieurs transactions plus petites.

periodHours

Nous utilisons la valeur minimale, soit une heure. Nous pouvons ainsi souscrire, collecter un paiement, attendre une heure et démontrer la réinitialisation du plafond sans attendre un mois. Une offre de production utiliserait l’intervalle défini par les conditions de facturation réelles du produit.

destinations

La liste des destinations autorisées contient les propriétaires de portefeuilles, et non les adresses de comptes de tokens. Lors de l’encaissement d’un paiement, le programme vérifie le propriétaire du compte de tokens destinataire. Comme le portefeuille de notre marchand figure parmi les destinations, son compte de tokens associé est un destinataire valide.

pullers

Un marchand est toujours autorisé à collecter les paiements de sa propre offre. Des portefeuilles supplémentaires de services de facturation peuvent être ajoutés à pullers. Nous laissons cette liste vide afin que seule la paire de clés du marchand puisse collecter les paiements. Avec notre configuration, seul le marchand ou un portefeuille figurant dans la liste des préleveurs de l’offre peut soumettre une transaction d’encaissement valide. 

Faire souscrire le client

Le client examine et accepte maintenant les conditions actuelles de l’offre du marchand. La PDA Subscription Delegation obtenue est dérivée de la PDA de l’offre + l’adresse du client. 

Le plugin TypeScript récupère le compte actuel de l’offre pendant subscribe. Nous n’avons donc pas besoin de transmettre manuellement le montant, la période ou l’horodatage de création attendus. Ces valeurs sont incluses dans la transaction en tant que conditions. Créez src/03-subscribe.ts :

03-subscribe.ts

import {
  fetchMaybeSubscriptionDelegation,
  findPlanPda,
  findSubscriptionDelegationPda,
} from "@solana/subscriptions";

import {
  createAppClient,
  CUSTOMER_KEYPAIR,
  loadState,
  MERCHANT_KEYPAIR,
  printSignature,
} from "./config.js";

const [customerClient, merchantClient] =
  await Promise.all([
    createAppClient(CUSTOMER_KEYPAIR),
    createAppClient(MERCHANT_KEYPAIR),
  ]);

const { tokenMint, planId } = loadState();

const [planPda] = await findPlanPda({
  owner: merchantClient.identity.address,
  planId,
});

const [subscriptionPda] =
  await findSubscriptionDelegationPda({
    planPda,
    subscriber: customerClient.identity.address,
  });

const existing =
  await fetchMaybeSubscriptionDelegation(
    customerClient.rpc,
    subscriptionPda,
  );

if (existing.exists) {
  console.log(
    "Customer is already subscribed:",
    subscriptionPda,
  );

  process.exit(0);
}

const result =
  await customerClient.subscriptions.instructions
    .subscribe({
      merchant: merchantClient.identity.address,
      planId,
      tokenMint,
    })
    .sendTransaction();

console.log("Subscription PDA:", subscriptionPda);

printSignature(
  "Subscribe",
  result.context.signature,
);

Exécutez le script en tant que client. Le client signe cette transaction pour accepter une nouvelle autorisation de dépense. Le compte d’abonnement stocke les conditions acceptées. 

Une fois l’offre créée, planId, owner, mint, amount, periodHours, createdAt et destinations sont immuables. Les conditions ne peuvent donc pas être modifiées. Si le marchand met ensuite à jour des champs modifiables de l’offre, les abonnés existants conservent les conditions initialement acceptées, tandis que les nouveaux abonnés reçoivent la version actuelle de l’offre.

Le client dispose désormais d’un abonnement actif, mais aucun paiement n’a encore été effectué.

Faire encaisser un paiement par le marchand

Le marchand peut désormais collecter jusqu’au plafond de cinq tokens de l’offre pendant la période de facturation en cours. Le Subscriptions Program n’exécute pas automatiquement cette transaction à l’expiration d’un minuteur. Un backend du marchand, un worker de facturation, une tâche cron ou un préleveur approuvé doit toujours soumettre la transaction d’encaissement. Créez src/04-collect.ts :

04-collect.ts

import {
  findAssociatedTokenPda,
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
  findPlanPda,
  findSubscriptionDelegationPda,
} from "@solana/subscriptions";

import {
  createAppClient,
  CUSTOMER_KEYPAIR,
  loadState,
  MERCHANT_KEYPAIR,
  PLAN_AMOUNT,
  printSignature,
} from "./config.js";

const [merchantClient, customerClient] =
  await Promise.all([
    createAppClient(MERCHANT_KEYPAIR),
    createAppClient(CUSTOMER_KEYPAIR),
  ]);

const { tokenMint, planId } = loadState();

const [merchantAta] =
  await findAssociatedTokenPda({
    mint: tokenMint,
    owner: merchantClient.identity.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

const [customerAta] =
  await findAssociatedTokenPda({
    mint: tokenMint,
    owner: customerClient.identity.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

const [planPda] = await findPlanPda({
  owner: merchantClient.identity.address,
  planId,
});

const [subscriptionPda] =
  await findSubscriptionDelegationPda({
    planPda,
    subscriber: customerClient.identity.address,
  });

const beforeCustomer =
  await merchantClient.rpc
    .getTokenAccountBalance(customerAta)
    .send();

const beforeMerchant =
  await merchantClient.rpc
    .getTokenAccountBalance(merchantAta)
    .send();

const result =
  await merchantClient.subscriptions.instructions
    .transferSubscription({
      caller: merchantClient.identity,

      // The customer whose balance is being charged.
      delegator: customerClient.identity.address,

      tokenMint,
      subscriptionPda,
      planPda,

      // Collect the full five-token period allowance.
      amount: PLAN_AMOUNT,

      receiverAta: merchantAta,
      tokenProgram: TOKEN_PROGRAM_ADDRESS,
    })
    .sendTransaction();

const afterCustomer =
  await merchantClient.rpc
    .getTokenAccountBalance(customerAta)
    .send();

const afterMerchant =
  await merchantClient.rpc
    .getTokenAccountBalance(merchantAta)
    .send();

console.log(
  "Customer:",
  beforeCustomer.value.uiAmountString,
  "->",
  afterCustomer.value.uiAmountString,
);

console.log(
  "Merchant:",
  beforeMerchant.value.uiAmountString,
  "->",
  afterMerchant.value.uiAmountString,
);

printSignature(
  "Collect payment",
  result.context.signature,
);

Exécutez le script en tant que marchand. Pendant l’exécution, le programme vérifie les éléments suivants :

  • L’appelant est le marchand ou un préleveur approuvé
  • L’abonnement appartient au client et à l’offre
  • L’abonnement n’a pas expiré
  • Le montant demandé respecte le plafond restant de la période en cours
  • Le portefeuille du marchand est une destination approuvée
  • Le compte de tokens destinataire appartient à la destination approuvée
  • Le mint et le Token Program correspondent à l’offre acceptée

Comme cette transaction collecte la totalité du plafond de cinq tokens, une nouvelle exécution pendant la même période d’une heure doit échouer. Au début de la période suivante, le plafond est réinitialisé et le marchand peut réexécuter le script d’encaissement. Dans un système de facturation en production, l’équivalent du script ne s’exécuterait normalement qu’à l’échéance d’une facture.

Le client annule son abonnement

Le client peut annuler l’abonnement sans la coopération du marchand. Le flux d’annulation standard ne ferme pas immédiatement le compte Subscription Delegation. Il marque plutôt l’abonnement comme arrivant à son terme et lui attribue un expiresAtTs. Une fois cette expiration passée, le client peut révoquer l’abonnement et fermer la PDA. Créez src/05-cancel.ts :

05-cancel.ts

import {
  fetchSubscriptionDelegation,
  findPlanPda,
  findSubscriptionDelegationPda,
} from "@solana/subscriptions";

import {
  createAppClient,
  CUSTOMER_KEYPAIR,
  loadState,
  MERCHANT_KEYPAIR,
  printSignature,
} from "./config.js";

const [customerClient, merchantClient] =
  await Promise.all([
    createAppClient(CUSTOMER_KEYPAIR),
    createAppClient(MERCHANT_KEYPAIR),
  ]);

const { planId } = loadState();

const [planPda] = await findPlanPda({
  owner: merchantClient.identity.address,
  planId,
});

const [subscriptionPda] =
  await findSubscriptionDelegationPda({
    planPda,
    subscriber: customerClient.identity.address,
  });

const result =
  await customerClient.subscriptions.instructions
    .cancelSubscription({
      planPda,
      subscriptionPda,
    })
    .sendTransaction();

const subscription =
  await fetchSubscriptionDelegation(
    customerClient.rpc,
    subscriptionPda,
  );

const expiresAt = new Date(
  Number(subscription.data.expiresAtTs) * 1_000,
);

console.log("Subscription PDA:", subscriptionPda);

console.log(
  "Cancellation effective at:",
  expiresAt.toISOString(),
);

printSignature(
  "Cancel subscription",
  result.context.signature,
);

Exécutez le script en tant que client. La sortie indique l’horodatage auquel l’annulation prend effet. L’instruction standard cancelSubscription applique un délai de grâce jusqu’à la fin de la période de facturation active.

Sur le plan opérationnel, l’annulation ne doit pas être considérée comme une remise à zéro immédiate de l’autorisation de la période en cours. Tout plafond encore disponible pour cette période peut rester collectable jusqu’à expiresAtTs. Le marchand ne peut pas commencer une nouvelle période de facturation après la prise d’effet de l’annulation.

Cela correspond au fonctionnement habituel des abonnements, où l’annulation empêche le prochain renouvellement au lieu de mettre fin rétroactivement à la période déjà commencée par le client.

Étude de cas pratique : les offres d’abonnement onchain de Helius

Helius a fait partie des partenaires de lancement qui ont contribué à façonner le Subscriptions Delegation Program avant son déploiement sur le mainnet. Nous utilisons ce programme pour assurer le renouvellement automatique en USDC de nos offres API. Notre objectif est d’offrir aux clients qui paient en cryptomonnaies la simplicité d’un abonnement SaaS traditionnel, tout en conservant l’autorisation et le règlement du paiement entièrement sur Solana.

Pour activer les paiements automatiques, le client ajoute un portefeuille Solana depuis la section Payment Method du tableau de bord de facturation Helius. 

Pendant la configuration, le client signe une approbation unique pour le Solana Subscriptions Program officiel et autorise Helius à collecter les paiements d’abonnement en USDC depuis ce portefeuille.

Lorsqu’une facture de renouvellement arrive à échéance, le système de facturation de Helius soumet la transaction d’encaissement. Le client n’a pas besoin d’ouvrir un lien de paiement, de reconnecter son portefeuille ou de signer un autre transfert. Le Subscriptions Delegation Program fournit l’autorisation onchain réutilisable, tandis que Helius continue de gérer le calendrier de facturation, l’état du compte et les droits d’accès au produit.

Un client peut connecter jusqu’à trois portefeuilles, mais seul celui défini comme moyen de paiement par défaut est utilisé pour les renouvellements automatiques. Helius ne tente pas de répartir un prélèvement entre plusieurs portefeuilles ni de passer à un autre portefeuille connecté si le portefeuille par défaut ne peut pas couvrir la facture.

Les clients peuvent changer de portefeuille par défaut depuis le tableau de bord. Ils peuvent également supprimer un portefeuille, ce qui nécessite une signature et révoque son autorisation de paiement automatique. La suppression du seul portefeuille connecté rétablit les liens de paiement manuel pour le compte.

L’autorisation onchain ne garantit évidemment pas que le portefeuille contiendra suffisamment d’USDC à l’échéance de la prochaine facture. Dans ce cas :

  • Helius ne débite pas le portefeuille pour ce renouvellement.
  • Le client reçoit un lien de paiement par e-mail et dans le tableau de bord.
  • La facture ne fait pas l’objet d’une nouvelle tentative automatique sur le portefeuille.
  • Une fois le portefeuille réapprovisionné, les renouvellements suivants peuvent de nouveau être encaissés automatiquement.

Ce mécanisme de secours simplifie l’état de la facturation. Un encaissement automatique échoué devient une facture ouverte ordinaire plutôt qu’une série indéfinie de nouvelles tentatives de transactions onchain.

Cette implémentation illustre concrètement l’intégration du Subscriptions Delegation Program dans une infrastructure de facturation en production. Le programme ne remplace pas la facturation, la gestion des comptes, les notifications ou l’application des droits d’accès. Il remplace la partie qui exigeait auparavant que le client autorise chaque renouvellement ou qu’un prestataire de paiement centralisé stocke et exerce cette autorisation.

Conclusion

Le Solana Subscriptions Program propose une méthode standardisée pour créer des paiements récurrents directement onchain. En combinant les Subscription Authorities, les offres des marchands et les transferts de tokens délégués, les développeurs peuvent mettre en œuvre la facturation d’abonnements sans dépendre d’une infrastructure de paiement offchain ni d’une logique de paiement personnalisée.

Si vous développez des paiements récurrents sur Solana, le Subscriptions Program constitue un excellent point de départ. Avec le SDK TypeScript ainsi que les RPC et API de Helius, l’intégration d’une facturation d’abonnements onchain est simple. Vous pouvez ainsi vous concentrer sur votre application plutôt que sur les mécanismes de paiement sous-jacents.

Ressources complémentaires

Abonnez-vous à Helius

Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication

Image agrandie