
Cómo configurar suscripciones y pagos recurrentes en Solana
Tabla de contenido
- Introducción
- Por qué las suscripciones en cadena han sido difíciles
- Presentamos el nuevo Subscriptions Delegation Program
- Tres modelos de autorización
- Delegaciones fijas
- Delegaciones recurrentes
- Planes de suscripción
- La implementación de referencia
- Casos de uso: lo que pueden crear los desarrolladores
- Facturación recurrente de API e infraestructura
- Gasto limitado para agentes de IA
- Nóminas y pagos a contratistas en cadena
- Cobro de facturas con stablecoins
- Micropagos de contenido y medios
- Crea un flujo de suscripción en Devnet con Helius
- Requisitos previos
- Configura el proyecto
- Crea las billeteras del comerciante y del cliente
- Crea un cliente compartido de Helius
- Crea el mint de prueba de Devnet
- Inicializa la Subscription Authority del cliente
- Crea un plan de suscripción del comerciante
- Haz que el cliente se suscriba
- Haz que el comerciante cobre un pago
- El cliente cancela su suscripción
- Caso práctico: planes de suscripción en cadena de Helius
- Conclusión
- Recursos adicionales
Introducción
La facturación recurrente es una parte fundamental del comercio por internet. Los productos SaaS, las plataformas de API, los sistemas de nómina y muchas otras empresas dependen de la capacidad de cobrar a sus clientes según un calendario predecible.
Hasta ahora, implementar esa experiencia en Solana implicaba crear mucha infraestructura personalizada. El nuevo Solana Subscriptions Delegation Program (también conocido como Solana Subscriptions & Allowances Program) resuelve este problema con una primitiva en cadena de código abierto y auditada para pagos recurrentes y planes de suscripción.
Ahora, un usuario puede autorizar una sola vez futuras transferencias de tokens (sujetas a restricciones explícitas en cadena). Después, un comerciante o cobrador aprobado puede iniciar pagos sin requerir la firma del usuario. Este programa ya está disponible tanto en mainnet como en devnet, y admite mints tanto del SPL Token Program original como de Token-2022.
En lugar de que cada aplicación diseñe y proteja su propio sistema de delegación, ahora los desarrolladores pueden integrar un programa estandarizado. Los flujos de pago que antes requerían semanas de desarrollo personalizado pueden integrarse en pocos días.
Como socio de lanzamiento, Helius ayudó a perfeccionar el programa y ahora lo usamos para impulsar la facturación en cadena de las suscripciones a nuestros planes de API. Esto permite a los clientes de Helius autorizar pagos recurrentes en USDC directamente desde sus billeteras de Solana.
En este artículo, examinaremos cómo funciona el programa y lo usaremos para crear flujos escalables de pagos recurrentes. Abordaremos la arquitectura de Subscription Authority, las PDA que representan autorizaciones individuales y los tres modelos de facturación que admite el programa:
- Límites de gasto fijos
- Delegaciones recurrentes
- Planes de suscripción definidos por el comerciante
Por qué las suscripciones en cadena han sido difíciles
Los programas de tokens de Solana ya admiten el gasto delegado. El propietario de una cuenta de tokens puede usar las instrucciones Approve o ApproveChecked para autorizar a otra dirección a transferir o quemar tokens en su nombre, hasta un límite especificado.
El problema es que una cuenta de tokens solo almacena un delegado actual y un importe delegado. Aprobar un nuevo delegado reemplaza al anterior y su límite. Esto se aplica a las cuentas administradas tanto por el Token Program original como por Token-2022. Cuando el usuario aprueba un segundo servicio, este reemplaza al primero. El límite nativo es un único valor; no incluye conceptos integrados de períodos de facturación, restablecimiento de límites ni estado de suscripción.
Las aplicaciones podrían evitar este problema creando una cuenta de tokens independiente para cada relación de gasto, pero esto fragmenta el saldo del usuario y complica mucho la experiencia de la billetera y la aplicación. Como alternativa, un equipo podría crear un programa personalizado de depósito en garantía o delegación, pero esto vuelve a introducir la carga de desarrollo y seguridad que el programa compartido pretende eliminar.
Faltaba una forma de convertir el único espacio de delegado del Token Program en una puerta de enlace programable para muchas autorizaciones independientes.
Presentamos el nuevo Subscriptions Delegation Program
El Subscriptions Delegation Program agrega esa capa programable sin modificar ninguno de los programas de tokens subyacentes de Solana. Para cada par (usuario, mint del token), el programa deriva una Subscription Authority, una dirección derivada del programa (PDA) que se convierte en el delegado de la cuenta de tokens del usuario para ese mint específico. Se inicializa una vez y luego se reutiliza en cada suscripción o delegación asociada con ese usuario y mint.
Durante la inicialización, el usuario firma una transacción que aprueba la Subscription Authority con un límite de ~18,4 trillones, o u64::MAX. Esto es seguro porque la Subscription Authority es una PDA que solo puede firmar mediante el Subscriptions Delegation Program. No puede decidir por sí sola transferir tokens.
Antes de firmar una CPI hacia el Token Program, el Subscriptions Delegation Program debe cargar una cuenta de autorización válida y verificar sus restricciones. Según el modelo de autorización, estas comprobaciones pueden incluir:
- La billetera o el servicio autorizado para iniciar el cobro
- El mint y la cuenta de tokens de origen
- El límite total restante
- El importe máximo disponible en el período de facturación actual
- La hora de inicio y el vencimiento de la autorización
- Los planes de suscripción aceptados por el usuario
- El comerciante o cobrador aprobado que inicia el cargo
- Cualquier restricción de destino configurada por el plan
Solo después de superar esas comprobaciones, el programa firma como la Subscription Authority y ejecuta la transferencia de tokens. Si ninguna autorización activa coincide con la transferencia solicitada, la transacción falla. Por lo tanto, el límite u64::MAX pertenece a la puerta de enlace controlada por el programa, no a un comerciante individual. Un comerciante solo recibe la autoridad descrita por su cuenta de delegación específica.
La cuenta de tokens sigue teniendo exactamente un delegado (es decir, la PDA de Subscription Authority), pero el programa puede colocar muchas PDA de autorización independientes detrás de ella. Crear una nueva autorización no sobrescribe las existentes. Cada autorización tiene su propio estado, límites, ciclo de vida y mecanismo de revocación.
Tres modelos de autorización
El programa admite tres modelos distintos: delegaciones fijas, delegaciones recurrentes y planes de suscripción.
Delegaciones fijas
Una delegación fija autoriza a una billetera o servicio a cobrar hasta un importe total definido. Cada transferencia reduce el límite restante, y la delegación puede vencer opcionalmente en una marca de tiempo Unix especificada.
Este modelo resulta útil para presupuestos limitados de agentes, asignaciones únicas, autorizaciones de compra con plazo definido y otros casos en los que un usuario quiere establecer una exposición total máxima.
Delegaciones recurrentes
Una delegación recurrente especifica el importe que puede cobrarse en cada período. Cuando comienza el siguiente período, se restablece el importe cobrado durante el período anterior.
El usuario controla las condiciones, incluido el importe por período, la duración del período, la hora de inicio y el vencimiento general. Esto hace que las delegaciones recurrentes sean adecuadas para relaciones continuas como nóminas, pagos a contratistas, asignaciones recurrentes o acuerdos de facturación personalizados en los que el pagador define los límites.
Planes de suscripción
Los planes de suscripción invierten el flujo de configuración. En lugar de que cada usuario defina sus propias condiciones recurrentes, un comerciante publica un plan reutilizable con un importe, período de facturación, mint aceptado, cobradores permitidos y restricciones de destino opcionales.
Un usuario revisa y acepta esas condiciones, lo que crea una PDA de Subscription Delegation vinculada al plan. Las condiciones de facturación aceptadas se copian en la cuenta de suscripción del usuario. Esto impide que el comerciante cambie de forma silenciosa el precio principal o el período de facturación de un suscriptor existente. Después, el propietario del plan o un cobrador aprobado puede cobrar hasta el importe del plan durante cada período de facturación.
Esta distinción es importante:
- Las delegaciones recurrentes son autorizaciones definidas por el pagador
- Los planes de suscripción son condiciones publicadas por el comerciante que el pagador acepta de forma explícita
Los tres modelos usan la misma Subscription Authority y, en última instancia, ejecutan las transferencias mediante la misma arquitectura de delegación subyacente.
La implementación de referencia
El Subscriptions Delegation Program fue diseñado y creado por Moonsong Labs en colaboración con Solana Foundation y auditado por Cantina. Su código fuente, documentación y clientes están disponibles en el repositorio de suscripciones de Solana Foundation.
El programa en cadena está escrito en Rust no_std mediante Pinocchio. Pinocchio proporciona un modelo de desarrollo de más bajo nivel y con pocas dependencias. En comparación con una implementación típica de Anchor, esto permite que el programa administre con mayor precisión el uso de cómputo y el tamaño del binario.
El repositorio también usa Codama para generar clientes sincronizados de TypeScript y Rust directamente desde la interfaz del programa. Para las aplicaciones de TypeScript, el paquete principal es:
pnpm add @solana/subscriptionsTambién hay una aplicación web de demostración oficial que proporciona una implementación integral fácil de usar en devnet. El programa admite SPL Token y Token-2022, y emite eventos en cadena de transferencias y del ciclo de vida que las aplicaciones y los indexadores pueden decodificar con la IDL publicada.
Casos de uso: lo que pueden crear los desarrolladores
El programa resulta útil en cualquier situación en la que un usuario pueda definir los límites de un pago futuro antes de saber exactamente cuándo ocurrirá la transferencia. El usuario firma una vez para establecer una autorización. Después, un comerciante, servicio, destinatario o agente puede iniciar transferencias dentro de esos límites.
Como cada acuerdo de gasto está representado por su propia PDA, estos casos de uso pueden coexistir detrás de la misma Subscription Authority. Un usuario podría pagar un plan de API, proporcionar un presupuesto semanal a un agente de IA y autorizar pagos periódicos a un contratista desde la misma cuenta de tokens USDC, sin que una autorización interfiera con otra.
Facturación recurrente de API e infraestructura
Los planes de suscripción encajan de forma natural con productos SaaS, proveedores de RPC, plataformas de datos y otros servicios de infraestructura. Un proveedor puede publicar un plan en cadena independiente para cada nivel de producto y definir el mint de token aceptado, el precio, el período de facturación, los cobradores aprobados y los destinos de pago permitidos. Esto crea una experiencia de suscripción conocida sin necesitar un procesador de tarjetas.
Gasto limitado para agentes de IA
Los agentes autónomos necesitan poder pagar por API, cómputo, datos, servicios de trading y otros recursos sin pedir aprobación humana. Sin embargo, dar a un agente control sin restricciones sobre una billetera con fondos crea un riesgo de seguridad evidente.
Las delegaciones fijas ofrecen una alternativa más segura. Un usuario puede autorizar a un agente a gastar hasta un importe específico de tokens y establecer un vencimiento estricto para esa autorización. El usuario también puede revocar la delegación antes de que venza.
Las delegaciones recurrentes amplían el mismo modelo. Un agente podría recibir un límite diario para solicitudes de API o un presupuesto operativo semanal, con un importe disponible que se restablece al inicio de cada período.
Nóminas y pagos a contratistas en cadena
Las delegaciones recurrentes pueden admitir nóminas basadas en cobros, pagos periódicos, subvenciones y acuerdos con contratistas. Un pagador autoriza a un empleado o contratista a cobrar hasta un importe específico por período de pago. La autorización puede especificar el importe por período, la duración del período, la hora de inicio y el vencimiento final. Cuando vence un pago, el destinatario o servicio de nómina envía la transacción de transferencia.
Esto no es lo mismo que una transacción de nómina tradicional basada en envíos. El pagador no envía los fondos automáticamente el día de pago. En su lugar, el destinatario recibe un derecho estrictamente limitado para cobrar el importe acordado durante cada período. El resultado es un acuerdo de pago transparente que ambas partes pueden consultar en cadena.
Las transferencias y la actividad de delegación pueden rastrearse mediante los eventos emitidos por el programa, lo que permite crear paneles de nómina e integraciones contables.
Cobro de facturas con stablecoins
Las pasarelas de pago y plataformas de facturación B2B pueden usar el programa para reemplazar las solicitudes de pago repetidas por autorizaciones persistentes y limitadas. Un cliente podría autorizar a una pasarela a cobrar:
- Hasta un importe total fijo para una orden de compra
- Hasta un importe específico durante cada período de facturación semanal o mensual
- El precio de un plan estandarizado del comerciante en cada ciclo de facturación
La misma arquitectura puede impulsar facturas recurrentes, servicios de adquirencia para comerciantes, límites de uso, políticas corporativas de gasto y otros flujos de trabajo en los que el pagador quiere automatización sin ceder un control ilimitado.
Micropagos de contenido y medios
Las delegaciones recurrentes también pueden admitir modelos de pago basados en el uso para editoriales, plataformas de streaming, proveedores de investigación y otros servicios de medios.
Un usuario puede autorizar un límite de gasto mensual que se reduce de forma incremental cada vez que accede a contenido de pago. Por ejemplo, abrir un artículo podría consumir 0,10 USDC de un límite mensual de 10 USDC, mientras que los informes premium o streams de video podrían tener precios más altos. La plataforma envía cada pago cuando se accede al contenido.
Esto permite modelos de “paga por lo que lees” sin exigir una firma de la billetera para cada artículo ni obligar a los usuarios a contratar una suscripción fija de todo o nada. Las editoriales obtienen una forma escalable de monetizar cada acceso, mientras que los usuarios conservan un límite de gasto predecible y pueden revocar la autorización en cualquier momento.
Crea un flujo de suscripción en Devnet con Helius
En esta sección del tutorial, crearemos el ciclo de vida de una suscripción de comerciante mediante los siguientes pasos:
- Un cliente inicializa una Subscription Authority para su cuenta de tokens
- Un comerciante publica un plan de suscripción
- El cliente acepta el plan
- El comerciante cobra un pago
- El cliente cancela la suscripción
Los ejemplos usan @solana/subscriptions@0.4.0, el cliente de TypeScript publicado más reciente al momento de escribir este artículo. Fijar las versiones de los paquetes mantiene estable el tutorial aunque el SDK cambie después.
Modelaremos dos participantes:
| Rol | Responsabilidad |
| Cliente | Posee los tokens, inicializa la Subscription Authority, se suscribe al plan y lo cancela |
| Comerciante | Publica el plan y envía las transacciones de cobro |
Para el token, crearemos un mint personalizado de devnet con seis decimales y emitiremos 100 tokens de prueba para el cliente. Esto evita depender de un faucet de stablecoins independiente y conserva la misma aritmética de unidades base que usan los tokens con seis decimales, como USDC.
Los pares de claves JSON locales permiten ejecutar el flujo fácilmente desde la línea de comandos. En una aplicación real, las transacciones del cliente normalmente se firmarían mediante una billetera web o móvil, mientras que el cobrador del comerciante usaría un firmante de backend administrado de forma segura.
Requisitos previos
Este tutorial supone que tienes:
- Una versión reciente de Node.js
pnpm- La CLI de Solana
- Una clave de API de pago de Helius
- SOL de Devnet para ambas billeteras de prueba
Configura el proyecto
Crea un proyecto nuevo con un directorio keys para almacenar los pares de claves del cliente y del comerciante:
mkdir helius-subscriptions-devnet
cd helius-subscriptions-devnet
pnpm init
mkdir -p src keysAgrega "type": "module" a package.json y luego instala las dependencias:
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/nodeCrea tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src"]
}Crea las billeteras del comerciante y del cliente
Genera un par de claves para cada participante:
solana-keygen new \
--no-bip39-passphrase \
--outfile keys/merchant.json
solana-keygen new \
--no-bip39-passphrase \
--outfile keys/customer.jsonEstos pares de claves son solo para el tutorial de devnet. No confirmes claves de producción en un repositorio ni almacenes el firmante de producción de un comerciante como un archivo JSON sin cifrar.
Crea .gitignore:
node_modules/
.env
keys/
state.jsonCrea .env y agrega tu clave de API de Helius:
HELIUS_API_KEY=YOUR_HELIUS_API_KEY
MERCHANT_KEYPAIR=./keys/merchant.json
CUSTOMER_KEYPAIR=./keys/customer.jsonAmbas billeteras necesitan SOL para pagar las comisiones de transacción y crear sus respectivas cuentas PDA.
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 también proporciona un faucet de devnet mediante su panel. Para solicitar SOL de Devnet mediante el faucet o RPC de Helius necesitas un plan de pago de Helius.
Crea un cliente compartido de Helius
Cada script necesita la misma conexión de Helius, los mismos plugins del programa, valores de configuración y direcciones. Colocaremos todo en src/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 establece el par de claves cargado como identidad del cliente y pagador de comisiones. El plugin de RPC de Helius gestiona la planificación, el envío y la confirmación de transacciones, mientras que los plugins de tokens y suscripciones agregan sus respectivos asistentes de cuentas e instrucciones. Cada script muestra una URL de Orb (el explorador de bloques de Helius) para la transacción resultante.
Crea el mint de prueba de Devnet
Antes de inicializar una Subscription Authority, el cliente debe tener una cuenta de tokens existente para el mint del plan. Crearemos un mint de prueba con seis decimales, emitiremos 100 tokens para el cliente y crearemos una cuenta de tokens de destino vacía para el comerciante.
El plugin de tokens de Solana Kit incluye asistentes para crear mints y cuentas de tokens asociadas, además de acuñar tokens. Crea src/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,
);Ejecuta el script. Creará state.json, que contiene información sobre el mint generado y un ID de plan único. El cliente ahora posee 100 tokens de prueba y el comerciante tiene una cuenta de tokens vacía lista para recibir pagos de suscripción.
Inicializa la Subscription Authority del cliente
Se crea una Subscription Authority para un par (customer, mint) específico. La cuenta de tokens del cliente debe existir antes de la inicialización.
La transacción de inicialización crea la PDA de Subscription Authority y la aprueba como delegada de la cuenta de tokens del cliente. Después, la misma autoridad puede reutilizarse para cada delegación fija, delegación recurrente y plan de suscripción que involucre a ese cliente y mint. El cliente firma esta transacción.
Crea src/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,
);
Ejecuta el script como cliente. Primero comprueba si la PDA ya existe. Esto permite volver a ejecutar el comando de forma segura y evita enviar una transacción de inicialización duplicada. Una vez confirmada, la cuenta de tokens del cliente tendrá la Subscription Authority como delegada del Token Program.
Crea un plan de suscripción del comerciante
Ahora el comerciante publica las condiciones de facturación que los clientes pueden aceptar. Una PDA de Plan se deriva de la dirección del comerciante y el ID del plan.
Un plan define el mint de pago, el importe máximo por período, la duración del período, los cobradores aprobados, los destinos permitidos y metadatos opcionales fuera de la cadena.
Crea src/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,
);
Ejecuta el script como comerciante. Algunos campos son especialmente importantes:
amount
Los valores de los tokens se expresan en unidades base. Nuestro mint tiene seis decimales, por lo que 5 tokens = 5.000.000 de unidades base. El plan permite al comerciante cobrar un máximo acumulado de cinco tokens durante cada período de facturación. El comerciante podría cobrar todo este importe en una sola transacción o dividirlo entre varias transacciones más pequeñas.
periodHours
Usamos el mínimo, que es una hora. Así podemos suscribirnos, cobrar un pago, esperar una hora y demostrar que el límite se restablece sin esperar un mes. Un plan de producción usaría el intervalo definido por las condiciones de facturación reales del producto.
destinations
La lista de destinos permitidos contiene propietarios de billeteras, no direcciones de cuentas de tokens. Al cobrar un pago, el programa comprueba al propietario de la cuenta de tokens receptora. Como la billetera de nuestro comerciante está entre los destinos, su cuenta de tokens asociada es un receptor válido.
pullers
Un comerciante siempre tiene permiso para cobrar de su propio plan. Se pueden agregar billeteras adicionales de servicios de facturación a pullers. Dejamos esta lista vacía para que solo el par de claves del comerciante pueda cobrar pagos. Con nuestra configuración, solo el comerciante o una billetera incluida en la lista de cobradores del plan puede enviar una transacción de cobro válida.
Haz que el cliente se suscriba
Ahora el cliente revisa y acepta las condiciones actuales del plan del comerciante. La PDA de Subscription Delegation resultante se deriva de la PDA del plan + la dirección del cliente.
El plugin de TypeScript obtiene la cuenta del plan actual durante subscribe, por lo que no necesitamos pasar manualmente el importe, el período ni la marca de tiempo de creación esperados. Esos valores se incluyen en la transacción como condiciones. Crea src/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,
);
Ejecuta el script como cliente. El cliente firma esta transacción para aceptar una nueva autorización de gasto. La cuenta de suscripción almacena las condiciones aceptadas.
Una vez creado un plan, planId, owner, mint, amount, periodHours, createdAt e destinations son inmutables, lo que significa que las condiciones no pueden modificarse. Si el comerciante actualiza posteriormente campos mutables del plan, los suscriptores existentes conservan las condiciones que aceptaron originalmente, mientras que los nuevos suscriptores reciben la versión actual del plan.
El cliente ahora tiene una suscripción activa, pero todavía no se ha realizado ningún pago.
Haz que el comerciante cobre un pago
Ahora el comerciante puede cobrar hasta el límite de cinco tokens del plan durante el período de facturación actual. El Subscriptions Program no ejecuta automáticamente esta transacción cuando vence un temporizador. Un backend del comerciante, un proceso de facturación, una tarea cron o un cobrador aprobado aún deberá enviar la transacción de cobro. Crea src/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,
);
Ejecuta el script como comerciante. Durante la ejecución, el programa verifica que:
- El emisor sea el comerciante o un cobrador aprobado
- La suscripción pertenezca al cliente y al plan
- La suscripción no haya vencido
- El importe solicitado se ajuste al límite restante del período actual
- La billetera del comerciante sea un destino aprobado
- La cuenta de tokens receptora pertenezca al destino aprobado
- El mint y el Token Program coincidan con el plan aceptado
Como esta transacción cobra el límite completo de cinco tokens, volver a ejecutarla durante el mismo período de una hora debería fallar. Cuando comience el siguiente período, el límite se restablecerá y el comerciante podrá volver a ejecutar el script de cobro. En un sistema de facturación de producción, el equivalente del script normalmente solo se ejecutaría cuando venciera una factura.
El cliente cancela su suscripción
El cliente puede cancelar la suscripción sin la cooperación del comerciante. El flujo de cancelación estándar no cierra de inmediato la cuenta de Subscription Delegation. En su lugar, marca la suscripción como próxima a finalizar y asigna un expiresAtTs. Una vez transcurrido ese vencimiento, el cliente puede revocar la suscripción y cerrar la PDA. Crea src/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,
);
Ejecuta el script como cliente. El resultado incluye la marca de tiempo en la que la cancelación entra en vigor. La instrucción estándar cancelSubscription implementa un período de gracia hasta el final del período de facturación activo.
En términos operativos, la cancelación no debe interpretarse como una reducción inmediata a cero de la autorización del período actual. Cualquier parte disponible del límite del período actual aún podrá cobrarse hasta expiresAtTs. El comerciante no puede iniciar otro período de facturación después de que la cancelación entre en vigor.
Esto coincide con el comportamiento habitual de las suscripciones, en el que la cancelación detiene la siguiente renovación en lugar de finalizar retroactivamente el período que el cliente ya inició.
Caso práctico: planes de suscripción en cadena de Helius
Helius fue uno de los socios de lanzamiento que ayudaron a definir el Subscriptions Delegation Program antes de su lanzamiento en mainnet. Usamos el programa para admitir renovaciones automáticas en USDC de nuestros planes de API. Nuestro objetivo es ofrecer a los clientes que pagan con criptomonedas la comodidad de una suscripción SaaS convencional, mientras mantenemos la autorización y liquidación de pagos íntegramente en Solana.
Para habilitar los pagos automáticos, el cliente agrega una billetera de Solana desde la sección Método de pago del panel de facturación de Helius.
Durante la configuración, el cliente firma una aprobación única para el Solana Subscriptions Program oficial y autoriza a Helius a cobrar pagos de suscripción en USDC desde esa billetera.
Cuando vence una factura de renovación, el sistema de facturación de Helius envía la transacción de cobro. El cliente no necesita abrir un enlace de pago, volver a conectar su billetera ni firmar otra transferencia. El Subscriptions Delegation Program proporciona la autorización reutilizable en cadena, mientras Helius continúa administrando el calendario de facturas, el estado de la cuenta y los derechos de acceso al producto.
Un cliente puede conectar hasta tres billeteras, pero solo la billetera marcada como método de pago predeterminado se usa para las renovaciones automáticas. Helius no intenta dividir un cargo entre varias billeteras ni usar otra billetera conectada si la predeterminada no puede cubrir la factura.
Los clientes pueden cambiar la billetera predeterminada desde el panel. También pueden eliminar una billetera, lo que requiere una firma y revoca su autorización para pagos automáticos. Si se elimina la única billetera conectada, la cuenta vuelve a usar enlaces de pago manuales.
Por supuesto, la autorización en cadena no garantiza que la billetera contenga suficiente USDC cuando venza la siguiente factura. En esta situación:
- Helius no cobra a la billetera por esa renovación.
- El cliente recibe un enlace de pago por correo electrónico y en el panel.
- La factura no vuelve a intentarse automáticamente en la billetera.
- Después de que el cliente agregue fondos, las renovaciones posteriores podrán cobrarse automáticamente de nuevo.
Este mecanismo alternativo mantiene sencillo el estado de facturación. Un cobro automático fallido se convierte en una factura abierta normal, en lugar de una serie indefinida de reintentos de transacciones en cadena.
La implementación ofrece un ejemplo práctico de cómo el Subscriptions Delegation Program encaja en una infraestructura de facturación de producción. El programa no reemplaza la facturación, la administración de cuentas, las notificaciones ni la aplicación de derechos de acceso. Reemplaza la parte que antes requería que el cliente autorizara cada renovación o que un procesador de pagos centralizado almacenara y ejerciera esa autorización.
Conclusión
El programa Solana Subscriptions presenta una forma estandarizada de crear pagos recurrentes directamente en cadena. Al combinar autoridades de suscripción, planes de comerciantes y transferencias delegadas de tokens, los desarrolladores pueden implementar la facturación de suscripciones sin depender de infraestructura de pago fuera de la cadena ni de lógica de pago personalizada.
Si estás creando pagos recurrentes en Solana, el programa Subscriptions es el punto de partida natural. Con el SDK de TypeScript y los RPC y API de Helius, integrar la facturación de suscripciones en cadena es sencillo. Así puedes concentrarte en tu aplicación en lugar de la mecánica de pago subyacente.
Recursos adicionales
Artículos relacionados
Suscríbete a Helius
Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos


