NOVO: Helius adquire a Light Protocol
Banner sobre assinaturas e pagamentos recorrentes na Solana
Blog/Fundamentos

Como configurar assinaturas e pagamentos recorrentes na Solana

PesquisadorLostin no X
19 min de leitura

Introdução

A cobrança recorrente é uma parte fundamental do comércio na internet. Produtos SaaS, plataformas de API, sistemas de folha de pagamento e inúmeras outras empresas dependem da capacidade de cobrar seus clientes em um cronograma previsível.

Até agora, implementar essa experiência na Solana exigia a criação de muita infraestrutura personalizada. O novo Solana Subscriptions Delegation Program (também conhecido como Solana Subscriptions & Allowances Program) resolve esse problema com uma primitiva onchain de código aberto e auditada para pagamentos recorrentes e planos de assinatura.

Agora, um usuário pode autorizar futuras transferências de tokens uma única vez (sujeitas a restrições onchain explícitas). Depois disso, um comerciante ou coletor aprovado pode iniciar pagamentos sem exigir a assinatura do usuário. Esse programa já está ativo tanto na mainnet quanto na devnet, com suporte a mints do SPL Token Program original e do Token-2022.

Em vez de cada aplicação projetar e proteger seu próprio sistema de delegação, os desenvolvedores agora podem integrar um programa padronizado. Fluxos de pagamento que antes exigiam semanas de desenvolvimento personalizado podem ser integrados em poucos dias.

Como parceira de lançamento, a Helius ajudou a aprimorar o programa e agora o utiliza para viabilizar a cobrança onchain das assinaturas de planos de API. Isso permite que clientes da Helius autorizem pagamentos recorrentes em USDC diretamente de suas carteiras Solana.

Neste artigo, examinaremos como o programa funciona e o usaremos para criar fluxos escaláveis de pagamentos recorrentes. Abordaremos a arquitetura da Subscription Authority, os PDAs que representam autorizações individuais e os três modelos de cobrança oferecidos pelo programa:

  • Limites fixos de gastos
  • Delegações recorrentes
  • Planos de assinatura definidos pelo comerciante

Por que as assinaturas onchain eram difíceis

Os programas de tokens da Solana já oferecem suporte a gastos delegados. O proprietário de uma conta de token pode usar as instruções Approve ou ApproveChecked para autorizar outro endereço a transferir ou queimar tokens em seu nome, até um limite especificado.

O problema é que uma conta de token armazena apenas um delegado atual e um valor delegado. Aprovar um novo delegado substitui o anterior e seu limite. Isso vale para contas gerenciadas tanto pelo Token Program original quanto pelo Token-2022. Quando o usuário aprova um segundo serviço, ele substitui o primeiro. O limite nativo é um valor único; ele não tem um conceito integrado de períodos de cobrança, redefinições de limite ou estado da assinatura.

As aplicações poderiam contornar isso criando uma conta de token separada para cada relação de gastos, mas isso fragmenta o saldo do usuário e torna a experiência na carteira e na aplicação muito mais complicada. Como alternativa, uma equipe poderia criar um programa personalizado de custódia ou delegação, mas isso reintroduziria a carga de desenvolvimento e segurança que o programa compartilhado pretende eliminar.

Faltava uma maneira de transformar o único slot de delegado do Token Program em um gateway programável para muitas autorizações independentes.

Conheça o novo Subscriptions Delegation Program

O Subscriptions Delegation Program adiciona essa camada programável ausente sem alterar nenhum dos programas de tokens subjacentes da Solana. Para cada par (usuário, mint do token), o programa deriva uma Subscription Authority, um endereço derivado de programa (PDA) que se torna o delegado da conta de token do usuário para esse mint específico. Ela é inicializada uma única vez e depois reutilizada por todas as assinaturas ou delegações associadas a esse usuário e mint.

Durante a inicialização, o usuário assina uma transação que aprova a Subscription Authority com um limite de ~18,4 quintilhões, ou u64::MAX. Isso é seguro porque a Subscription Authority é um PDA que só pode assinar por meio do Subscriptions Delegation Program. Ela não pode decidir transferir tokens de forma independente. 

Antes de assinar uma CPI para o Token Program, o Subscriptions Delegation Program precisa carregar uma conta de autorização válida e verificar suas restrições. Dependendo do modelo de autorização, essas verificações podem incluir:

  • A carteira ou o serviço autorizado a iniciar a cobrança
  • O mint e a conta de token de origem
  • O limite total restante
  • O valor máximo disponível no período de cobrança atual
  • O horário de início e a expiração da autorização
  • Os planos de assinatura aceitos pelo usuário
  • O comerciante ou coletor aprovado que inicia a cobrança
  • Quaisquer restrições de destino configuradas pelo plano

Somente depois que essas verificações são aprovadas, o programa assina como a Subscription Authority e executa a transferência de tokens. Se nenhuma autorização ativa corresponder à transferência solicitada, a transação falhará. Portanto, o limite u64::MAX pertence ao gateway controlado pelo programa, não a um comerciante individual. Um comerciante recebe apenas a autoridade descrita por sua conta de delegação específica.

A conta de token ainda tem exatamente um delegado (ou seja, o PDA da Subscription Authority), mas o programa pode colocar muitos PDAs de autorização independentes por trás dele. A criação de uma nova autorização não substitui nenhuma das existentes. Cada autorização tem seu próprio estado, limites, ciclo de vida e caminho de revogação.

Três modelos de autorização

O programa oferece três modelos distintos: delegações fixas, delegações recorrentes e planos de assinatura.

Delegações fixas

Uma delegação fixa autoriza uma carteira ou serviço a retirar até um valor total definido. Cada transferência reduz o limite restante, e a delegação pode, opcionalmente, expirar em um timestamp Unix especificado.

Esse modelo é útil para orçamentos limitados de agentes, limites únicos, autorizações de compra com prazo determinado e outros casos em que o usuário deseja definir uma exposição total máxima.

Delegações recorrentes

Uma delegação recorrente especifica o valor que pode ser retirado em cada período. Quando o próximo período começa, o valor retirado no período anterior é zerado.

O usuário controla os termos, incluindo o valor por período, a duração do período, o horário de início e a expiração geral. Isso torna as delegações recorrentes adequadas para relações contínuas, como folha de pagamento, pagamentos a prestadores, limites recorrentes ou acordos de cobrança personalizados nos quais o pagador define os limites.

Planos de assinatura

Os planos de assinatura invertem o fluxo de configuração. Em vez de cada usuário definir seus próprios termos recorrentes, um comerciante publica um plano reutilizável com valor, período de cobrança, mint aceito, coletores permitidos e restrições de destino opcionais.

O usuário analisa e aceita esses termos, criando um PDA de Subscription Delegation vinculado ao plano. Os termos de cobrança aceitos são copiados para a conta de assinatura do usuário, impedindo que o comerciante altere silenciosamente o preço principal ou o período de cobrança de um assinante existente. O proprietário do plano ou um cobrador aprovado pode então coletar até o valor do plano durante cada período de cobrança.

Essa distinção é importante:

  • Delegações recorrentes são autorizações definidas pelo pagador
  • Planos de assinatura são termos publicados pelo comerciante que o pagador aceita explicitamente

Os três modelos usam a mesma Subscription Authority e, por fim, executam transferências pela mesma arquitetura de delegação subjacente.

A implementação de referência

O Subscriptions Delegation Program foi projetado e desenvolvido pela Moonsong Labs em parceria com a Solana Foundation e auditado pela Cantina. Seu código-fonte, documentação e clientes estão disponíveis no repositório de assinaturas da Solana Foundation.

O programa onchain foi escrito em Rust no_std usando o Pinocchio. O Pinocchio oferece um modelo de desenvolvimento de nível mais baixo e com poucas dependências. Em comparação com uma implementação típica em Anchor, isso permite que o programa gerencie com mais cuidado o uso de computação e o tamanho do binário.

O repositório também usa o Codama para gerar clientes TypeScript e Rust sincronizados diretamente da interface do programa. Para aplicações TypeScript, o pacote principal é:

Código
pnpm add @solana/subscriptions

Há também uma aplicação web oficial de demonstração que fornece uma implementação completa e pode ser usada facilmente na devnet. O programa oferece suporte a SPL Token e Token-2022 e emite eventos onchain de ciclo de vida e transferência que aplicações e indexadores podem decodificar usando a IDL publicada.

Casos de uso: o que os desenvolvedores podem criar

O programa é útil sempre que um usuário pode definir os limites de um pagamento futuro antes de saber exatamente quando a transferência ocorrerá. O usuário assina uma única vez para estabelecer uma autorização; depois disso, um comerciante, serviço, destinatário ou agente pode iniciar transferências dentro desses limites.

Como cada acordo de gastos é representado por seu próprio PDA, esses casos de uso podem coexistir por trás da mesma Subscription Authority. Um usuário poderia pagar por um plano de API, fornecer um orçamento semanal a um agente de IA e autorizar o pagamento recorrente de um prestador a partir da mesma conta de token USDC, sem que uma autorização interfira na outra.

Cobrança recorrente de APIs e infraestrutura

Os planos de assinatura são ideais para produtos SaaS, provedores de RPC, plataformas de dados e outros serviços de infraestrutura. Um provedor pode publicar um plano onchain separado para cada nível de produto, definindo o mint do token aceito, o preço, o período de cobrança, os coletores aprovados e os destinos de pagamento permitidos. Isso cria uma experiência de assinatura familiar sem exigir um processador de cartões.

Gastos limitados para agentes de IA

Agentes autônomos precisam conseguir pagar por APIs, computação, dados, serviços de negociação e outros recursos sem solicitar aprovação humana. No entanto, conceder a um agente controle irrestrito sobre uma carteira com fundos cria um risco de segurança evidente.

As delegações fixas oferecem uma alternativa mais segura. Um usuário pode autorizar um agente a gastar até um valor específico de tokens e definir uma expiração rígida para essa autorização. O usuário também pode revogar a delegação antes que ela expire.

As delegações recorrentes ampliam o mesmo modelo. Um agente poderia receber um limite diário para solicitações de API ou um orçamento operacional semanal, com o valor disponível sendo redefinido no início de cada período.

Folha de pagamento onchain e pagamentos a prestadores

As delegações recorrentes podem viabilizar folhas de pagamento baseadas em cobrança, pagamentos recorrentes, subsídios e contratos com prestadores. Um pagador autoriza um funcionário ou prestador a cobrar até um valor especificado por período de pagamento. A autorização pode definir o valor por período, a duração do período, o horário de início e a expiração final. Quando um pagamento vence, o destinatário ou o serviço de folha de pagamento envia a transação de transferência.

Isso não é o mesmo que uma transação tradicional de folha de pagamento baseada em envio. O pagador não envia fundos automaticamente no dia do pagamento. Em vez disso, o destinatário recebe um direito estritamente limitado de retirar o valor acordado durante cada período. O resultado é um acordo de pagamento transparente que ambas as partes podem consultar onchain.

As transferências e a atividade de delegação podem ser acompanhadas pelos eventos emitidos pelo programa, possibilitando a criação de painéis de folha de pagamento e integrações contábeis.

Cobrança de faturas em stablecoins

Gateways de pagamento e plataformas de cobrança B2B podem usar o programa para substituir solicitações repetidas de pagamento por autorizações persistentes e limitadas. Um cliente poderia autorizar um gateway a cobrar:

  • Até um valor total fixo para uma ordem de compra
  • Até um valor especificado durante cada período semanal ou mensal de faturamento
  • O preço de um plano padronizado do comerciante em cada ciclo de cobrança

A mesma arquitetura pode viabilizar faturas recorrentes, adquirência para comerciantes, limites de uso, políticas de gastos corporativos e outros fluxos de trabalho nos quais o pagador deseja automação sem abrir mão de controle ilimitado.

Micropagamentos por conteúdo e mídia

As delegações recorrentes também podem viabilizar modelos de pagamento baseados em uso para editoras, plataformas de streaming, provedores de pesquisas e outros serviços de mídia.

Um usuário pode autorizar um limite de gastos mensal que é consumido gradualmente sempre que ele acessa conteúdo pago. Por exemplo, abrir um artigo pode consumir 0,10 USDC de um limite mensal de 10 USDC, enquanto relatórios premium ou transmissões de vídeo podem ter preços mais altos. A plataforma envia cada pagamento conforme o conteúdo é acessado.

Isso viabiliza modelos de “pague pelo que você lê” sem exigir uma assinatura da carteira para cada artigo nem obrigar os usuários a aderir a uma assinatura fixa e inflexível. As editoras ganham uma forma escalável de monetizar eventos individuais de acesso, enquanto os usuários mantêm um limite de gastos previsível e podem revogar a autorização a qualquer momento.

Crie um fluxo de assinatura na devnet com a Helius

Nesta seção do tutorial, criaremos o ciclo de vida de uma assinatura de comerciante com as seguintes etapas:

  • Um cliente inicializa uma Subscription Authority para sua conta de token
  • Um comerciante publica um plano de assinatura
  • O cliente aceita o plano
  • O comerciante cobra um pagamento
  • O cliente cancela a assinatura

Os exemplos usam @solana/subscriptions@0.4.0, o cliente TypeScript publicado mais recente no momento da redação. Fixar as versões dos pacotes mantém o tutorial estável, mesmo que o SDK seja alterado posteriormente.

Usaremos dois participantes:

FunçãoResponsabilidade
ClientePossui os tokens, inicializa a Subscription Authority, assina e cancela o plano
ComerciantePublica o plano e envia transações de cobrança

Para o token, criaremos um mint personalizado de seis casas decimais na devnet e emitiremos 100 tokens de teste para o cliente. Isso evita a dependência de uma faucet de stablecoin separada, preservando a mesma aritmética de unidades básicas usada por tokens com seis casas decimais, como o USDC.

Keypairs JSON locais facilitam a execução do fluxo pela linha de comando. Em uma aplicação real, as transações do cliente normalmente seriam assinadas por uma carteira móvel ou de navegador, enquanto o coletor do comerciante usaria um signatário de backend gerenciado com segurança.

Pré-requisitos

Este tutorial pressupõe que você tenha:

  • Uma versão recente do Node.js
  • pnpm
  • A Solana CLI
  • Uma chave de API paga da Helius
  • SOL da devnet para ambas as carteiras de teste

Configure o projeto

Crie um novo projeto com um diretório keys para armazenar os keypairs do cliente e do comerciante:

Código
mkdir helius-subscriptions-devnet
cd helius-subscriptions-devnet

pnpm init
mkdir -p src keys

Adicione "type": "module" a package.json e instale as dependências:

Código
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

Crie tsconfig.json:

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

Crie as carteiras do comerciante e do cliente

Gere um keypair para cada participante:

Código
solana-keygen new \
  --no-bip39-passphrase \
  --outfile keys/merchant.json

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

Esses keypairs destinam-se apenas ao tutorial na devnet. Não envie chaves de produção para um repositório nem armazene o signatário de produção de um comerciante como um arquivo JSON não criptografado.

Crie .gitignore:

.gitignore
node_modules/
.env
keys/
state.json

Crie .env e adicione sua chave de API da Helius:

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

As duas carteiras precisam de SOL para pagar as taxas de transação e criar suas respectivas contas PDA.

Código
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"

A Helius também oferece uma faucet da devnet em seu painel. A solicitação de SOL da devnet pela faucet da Helius ou pelo RPC da Helius exige um plano pago da Helius.

Crie um cliente compartilhado da Helius

Cada script precisa da mesma conexão da Helius, dos mesmos plugins de programa, valores de configuração e endereços. Colocaremos tudo isso em 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 define o keypair carregado como a identidade do cliente e o pagador de taxas. O plugin de RPC da Helius cuida do planejamento, envio e confirmação das transações, enquanto os plugins de token e assinaturas adicionam seus respectivos auxiliares de conta e instrução. Cada script exibe uma URL do Orb (o explorador de blocos da Helius) para a transação resultante.

Crie o mint de teste na devnet

Antes de inicializar uma Subscription Authority, o cliente precisa ter uma conta de token existente para o mint do plano. Criaremos um mint de teste com seis casas decimais, emitiremos 100 tokens para o cliente e criaremos uma conta de token de destino vazia para o comerciante.

O plugin de tokens do Solana Kit fornece auxiliares para criar mints, contas de token associadas e emitir tokens. Crie 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,
);

Execute o script. Ele criará state.json, que contém informações sobre o mint gerado e um ID de plano exclusivo. Agora, o cliente possui 100 tokens de teste, e o comerciante tem uma conta de token vazia pronta para receber pagamentos de assinaturas.

Inicialize a Subscription Authority do cliente

Uma Subscription Authority é criada para um par (customer, mint) específico. A conta de token do cliente já deve existir antes da inicialização.

A transação de inicialização cria o PDA da Subscription Authority e o aprova como delegado da conta de token do cliente. A mesma autoridade pode então ser reutilizada em todas as delegações fixas, delegações recorrentes e planos de assinatura que envolvam esse cliente e mint. O cliente assina essa transação.

Crie 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,
);

Execute o script como cliente. Primeiro, ele verifica se o PDA já existe. Isso torna seguro executar o comando novamente e evita o envio de uma transação de inicialização duplicada. Após a confirmação, a conta de token do cliente terá a Subscription Authority como delegada no Token Program.

Crie um plano de assinatura do comerciante

Agora, o comerciante publica os termos de cobrança que os clientes podem aceitar. Um PDA de plano é derivado do endereço do comerciante e do ID do plano.

Um plano define o mint de pagamento, o valor máximo por período, a duração do período, os coletores aprovados, os destinos permitidos e metadados offchain opcionais.

Crie 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,
);

Execute o script como comerciante. Alguns campos são especialmente importantes:

amount

Os valores dos tokens são expressos em unidades básicas. Nosso mint tem seis casas decimais, portanto, 5 tokens = 5.000.000 de unidades básicas. O plano permite que o comerciante cobre um máximo acumulado de cinco tokens durante cada período de cobrança. O comerciante pode cobrar todo esse valor em uma única transação ou dividi-lo entre várias transações menores.

periodHours

Usamos o mínimo, que é uma hora. Assim, podemos assinar, cobrar um pagamento, aguardar uma hora e demonstrar que o limite é redefinido sem precisar esperar um mês. Um plano de produção usaria o intervalo definido pelos termos reais de cobrança do produto.

destinations

A lista de destinos permitidos contém proprietários de carteiras, não endereços de contas de token. Ao cobrar um pagamento, o programa verifica o proprietário da conta de token receptora. Como a carteira do nosso comerciante está entre os destinos, sua conta de token associada é uma destinatária válida.

pullers

Um comerciante sempre tem permissão para fazer cobranças em seu próprio plano. Outras carteiras de serviços de cobrança podem ser adicionadas a pullers. Deixamos essa lista vazia para que somente o keypair do comerciante possa cobrar pagamentos. Com nossa configuração, apenas o comerciante ou uma carteira incluída na lista de cobradores do plano pode enviar uma transação de cobrança válida. 

Faça o cliente assinar o plano

Agora, o cliente analisa e aceita os termos atuais do plano do comerciante. O PDA de Subscription Delegation resultante é derivado do PDA do plano + endereço do cliente. 

O plugin TypeScript busca a conta atual do plano durante subscribe, portanto, não precisamos informar manualmente o valor, o período ou o timestamp de criação esperados. Esses valores são incluídos na transação como termos. Crie 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,
);

Execute o script como cliente. O cliente assina essa transação para aceitar uma nova autorização de gastos. A conta de assinatura armazena os termos aceitos. 

Depois que um plano é criado, planId, owner, mint, amount, periodHours, createdAt e destinations são imutáveis, o que significa que os termos não podem ser alterados. Se o comerciante atualizar posteriormente campos mutáveis no plano, os assinantes existentes manterão os termos originalmente aceitos, enquanto novos assinantes receberão a versão atual do plano.

Agora, o cliente tem uma assinatura ativa, mas nenhum pagamento foi feito ainda.

Faça o comerciante cobrar um pagamento

Agora, o comerciante pode cobrar até o limite de cinco tokens do plano durante o período de cobrança atual. O Subscriptions Program não executa essa transação automaticamente quando um temporizador expira. Um backend do comerciante, worker de cobrança, cron job ou cobrador aprovado ainda precisará enviar a transação de cobrança. Crie 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,
);

Execute o script como comerciante. Durante a execução, o programa verifica se:

  • O chamador é o comerciante ou um cobrador aprovado
  • A assinatura pertence ao cliente e ao plano
  • A assinatura não expirou
  • O valor solicitado cabe no limite restante do período atual
  • A carteira do comerciante é um destino aprovado
  • A conta de token receptora pertence ao destino aprovado
  • O mint e o Token Program correspondem ao plano aceito

Como essa transação cobra o limite total de cinco tokens, executá-la novamente durante o mesmo período de uma hora deve falhar. Quando o próximo período começar, o limite do período será redefinido e o comerciante poderá executar o script de cobrança novamente. Em um sistema de cobrança em produção, o equivalente desse script normalmente seria executado apenas quando uma fatura vencesse.

O cliente cancela sua assinatura

O cliente pode cancelar a assinatura sem a cooperação do comerciante. O fluxo padrão de cancelamento não fecha imediatamente a conta de Subscription Delegation. Em vez disso, ele marca a assinatura como próxima do fim e atribui um expiresAtTs. Depois que essa expiração passar, o cliente poderá revogar a assinatura e fechar o PDA. Crie 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,
);

Execute o script como cliente. A saída inclui o timestamp em que o cancelamento entra em vigor. A instrução padrão cancelSubscription implementa um período de carência até o fim do período de cobrança ativo.

Em termos operacionais, o cancelamento não deve ser tratado como uma zeragem imediata da autorização do período atual. Qualquer limite ainda disponível no período atual poderá ser cobrado até expiresAtTs. O comerciante não pode iniciar outro período de cobrança depois que o cancelamento entra em vigor.

Isso corresponde ao comportamento comum de assinaturas, no qual o cancelamento interrompe a próxima renovação, em vez de encerrar retroativamente o período que o cliente já iniciou.

Estudo de caso prático: planos de assinatura onchain da Helius

A Helius foi uma das parceiras de lançamento que ajudaram a moldar o Subscriptions Delegation Program antes de seu lançamento na mainnet. Usamos o programa para oferecer renovações automáticas em USDC dos nossos planos de API. Nosso objetivo é proporcionar aos clientes que pagam com criptomoedas a conveniência de uma assinatura SaaS convencional, mantendo a autorização e a liquidação do pagamento inteiramente na Solana.

Para ativar pagamentos automáticos, o cliente adiciona uma carteira Solana na seção Forma de pagamento do painel de cobrança da Helius. 

Durante a configuração, o cliente assina uma aprovação única para o Solana Subscriptions Program oficial e autoriza a Helius a cobrar dessa carteira os pagamentos da assinatura em USDC.

Quando uma fatura de renovação vence, o sistema de cobrança da Helius envia a transação de cobrança. O cliente não precisa abrir um link de pagamento, reconectar sua carteira nem assinar outra transferência. O Subscriptions Delegation Program fornece a autorização onchain reutilizável, enquanto a Helius continua gerenciando o cronograma de faturas, o estado da conta e os direitos de acesso ao produto.

Um cliente pode conectar até três carteiras, mas apenas a carteira marcada como forma de pagamento padrão é usada para renovações automáticas. A Helius não tenta dividir uma cobrança entre carteiras nem usar outra carteira conectada se a carteira padrão não puder cobrir a fatura.

Os clientes podem alterar a carteira padrão no painel. Eles também podem remover uma carteira, o que exige uma assinatura e revoga sua autorização para pagamentos automáticos. Remover a única carteira conectada faz a conta voltar a usar links de pagamento manuais.

Naturalmente, a autorização onchain não garante que a carteira terá USDC suficiente quando a próxima fatura vencer. Nesse cenário:

  • A Helius não cobra essa renovação da carteira.
  • O cliente recebe um link de pagamento por e-mail e no painel.
  • A cobrança da fatura não é repetida automaticamente na carteira.
  • Depois que o cliente adiciona saldo, as renovações posteriores podem voltar a ser cobradas automaticamente.

Essa alternativa mantém o estado da cobrança simples. Uma cobrança automática malsucedida se torna uma fatura aberta comum, em vez de uma série indefinida de novas tentativas de transação onchain.

A implementação oferece um exemplo prático de como o Subscriptions Delegation Program se encaixa em uma stack de cobrança em produção. O programa não substitui o faturamento, o gerenciamento de contas, as notificações nem a aplicação dos direitos de acesso. Ele substitui a parte que antes exigia que o cliente autorizasse cada renovação ou que um processador de pagamentos centralizado armazenasse e exercesse essa autorização.

Conclusão

O programa Solana Subscriptions apresenta uma forma padronizada de criar pagamentos recorrentes diretamente onchain. Ao combinar autoridades de assinatura, planos de comerciantes e transferências delegadas de tokens, os desenvolvedores podem implementar cobranças de assinaturas sem depender de infraestrutura de pagamento offchain ou lógica de pagamento personalizada.

Se você está criando pagamentos recorrentes na Solana, o Subscriptions Program é o ponto de partida natural. Com o SDK TypeScript e os RPCs e APIs da Helius, integrar cobranças de assinaturas onchain é simples, permitindo que você se concentre em sua aplicação, e não nos mecanismos de pagamento subjacentes.

Outros recursos

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos

Imagem ampliada