BARU: Helius mengakuisisi Light Protocol
Banner Langganan dan Pembayaran Berulang Solana
Blog/Dasar-Dasar

Menyiapkan Langganan dan Pembayaran Berulang di Solana

PenelitiLostin di X
Bacaan 19 menit

Pendahuluan

Penagihan berulang merupakan bagian mendasar dari perdagangan internet. Produk SaaS, platform API, sistem penggajian, dan banyak bisnis lainnya mengandalkan kemampuan untuk menagih pelanggan sesuai jadwal yang dapat diprediksi.

Hingga saat ini, menghadirkan pengalaman tersebut di Solana mengharuskan developer membangun banyak infrastruktur khusus. Solana Subscriptions Delegation Program yang baru (juga dikenal sebagai Solana Subscriptions & Allowances Program) mengatasi kendala ini dengan primitive onchain sumber terbuka yang telah diaudit untuk pembayaran berulang dan paket langganan.

Kini, pengguna dapat mengotorisasi transfer token mendatang satu kali (dengan batasan onchain yang eksplisit). Setelah itu, merchant atau kolektor yang disetujui dapat memulai pembayaran tanpa meminta tanda tangan pengguna. Program ini sudah aktif di mainnet dan devnet, serta mendukung mint dari SPL Token Program asli maupun Token-2022.

Developer kini dapat mengintegrasikan program terstandardisasi, alih-alih setiap aplikasi harus merancang dan mengamankan sistem delegasinya sendiri. Alur pembayaran yang sebelumnya memerlukan pengembangan khusus selama berminggu-minggu kini dapat diintegrasikan dalam hitungan hari.

Sebagai mitra peluncuran, Helius membantu menyempurnakan program ini dan kini menggunakannya untuk mendukung penagihan langganan paket API onchain. Dengan demikian, pelanggan Helius dapat mengotorisasi pembayaran berulang dalam USDC langsung dari dompet Solana mereka.

Dalam artikel ini, kita akan mempelajari cara kerja program tersebut dan menggunakannya untuk membangun alur pembayaran berulang yang skalabel. Kita akan membahas arsitektur Subscription Authority, PDA yang merepresentasikan setiap otorisasi, dan tiga model penagihan yang didukung program:

  • Batas pengeluaran tetap
  • Delegasi berulang
  • Paket langganan yang ditentukan merchant

Mengapa Langganan Onchain Sulit Dibangun

Program token Solana sudah mendukung pengeluaran yang didelegasikan. Pemilik akun token dapat menggunakan instruksi Approve atau ApproveChecked untuk mengotorisasi alamat lain agar dapat mentransfer atau membakar token atas nama mereka, hingga batas tertentu.

Masalahnya, akun token hanya menyimpan satu delegasi aktif dan satu jumlah yang didelegasikan. Menyetujui delegasi baru akan menggantikan delegasi sebelumnya beserta batasnya. Hal ini berlaku untuk akun yang dikelola oleh Token Program asli maupun Token-2022. Saat pengguna menyetujui layanan kedua, layanan tersebut menggantikan layanan pertama. Batas bawaan hanya berupa satu nilai; tidak ada konsep bawaan untuk periode penagihan, pengaturan ulang batas, atau status langganan.

Aplikasi dapat mengatasinya dengan membuat akun token terpisah untuk setiap hubungan pengeluaran, tetapi hal ini memecah saldo pengguna serta membuat pengalaman dompet dan aplikasi jauh lebih rumit. Sebagai alternatif, tim dapat membangun program escrow atau delegasi khusus, tetapi langkah tersebut kembali menghadirkan beban pengembangan dan keamanan yang semestinya dihilangkan oleh program bersama ini.

Yang belum tersedia adalah cara untuk mengubah satu slot delegasi milik Token Program menjadi gateway yang dapat diprogram bagi banyak otorisasi independen.

Mengenal Subscriptions Delegation Program yang baru

Subscriptions Delegation Program menambahkan lapisan terprogram yang sebelumnya belum tersedia tanpa mengubah program token dasar Solana. Untuk setiap pasangan (pengguna, mint token), program ini memperoleh Subscription Authority, yaitu program-derived address (PDA) yang menjadi delegasi bagi akun token pengguna untuk mint tertentu. Alamat ini diinisialisasi satu kali, lalu digunakan kembali oleh setiap langganan atau delegasi yang terkait dengan pengguna dan mint tersebut.

Selama inisialisasi, pengguna menandatangani transaksi yang menyetujui Subscription Authority dengan batas sebesar ~18,4 kuintiliun, atau u64::MAX. Hal ini aman karena Subscription Authority merupakan PDA yang hanya dapat menandatangani melalui Subscriptions Delegation Program. PDA tersebut tidak dapat memutuskan sendiri untuk mentransfer token. 

Sebelum menandatangani CPI ke Token Program, Subscriptions Delegation Program harus memuat akun otorisasi yang valid dan memverifikasi batasannya. Bergantung pada model otorisasi, pemeriksaan tersebut dapat mencakup:

  • Dompet atau layanan yang diizinkan memulai penarikan
  • Mint dan akun token sumber
  • Sisa batas total
  • Jumlah maksimum yang tersedia dalam periode penagihan saat ini
  • Waktu mulai dan kedaluwarsa otorisasi
  • Paket langganan yang diterima pengguna
  • Merchant atau kolektor yang disetujui yang memulai penagihan
  • Batasan tujuan yang dikonfigurasi oleh paket

Hanya setelah semua pemeriksaan tersebut lolos, program akan menandatangani sebagai Subscription Authority dan mengeksekusi transfer token. Jika tidak ada otorisasi aktif yang cocok dengan transfer yang diminta, transaksi akan gagal. Oleh karena itu, batas u64::MAX dimiliki oleh gateway yang dikendalikan program, bukan oleh merchant tertentu. Merchant hanya menerima kewenangan yang dijelaskan oleh akun delegasinya sendiri.

Akun token tetap memiliki tepat satu delegasi (yaitu PDA Subscription Authority), tetapi program dapat menempatkan banyak PDA otorisasi independen di belakangnya. Membuat otorisasi baru tidak akan menimpa otorisasi yang sudah ada. Setiap otorisasi memiliki status, batas, siklus hidup, dan jalur pencabutannya sendiri.

Tiga Model Otorisasi

Program ini mendukung tiga model berbeda: delegasi tetap, delegasi berulang, dan paket langganan.

Delegasi Tetap

Delegasi tetap mengotorisasi dompet atau layanan untuk menarik hingga jumlah total yang ditentukan. Setiap transfer mengurangi sisa batas, dan delegasi dapat diatur agar kedaluwarsa pada timestamp Unix tertentu.

Model ini berguna untuk anggaran agen yang dibatasi, batas satu kali, kewenangan pembelian berbatas waktu, dan kasus lain ketika pengguna ingin menentukan eksposur total maksimum.

Delegasi Berulang

Delegasi berulang menentukan jumlah yang dapat ditarik pada setiap periode. Ketika periode berikutnya dimulai, jumlah yang ditarik pada periode sebelumnya akan diatur ulang.

Pengguna mengendalikan ketentuannya, termasuk jumlah per periode, durasi periode, waktu mulai, dan waktu kedaluwarsa keseluruhan. Hal ini membuat delegasi berulang cocok untuk hubungan berkelanjutan seperti penggajian, pembayaran kontraktor, tunjangan berulang, atau perjanjian penagihan khusus yang batasnya ditentukan oleh pembayar.

Paket Langganan

Paket langganan membalik alur penyiapan. Alih-alih setiap pengguna menentukan ketentuan berulangnya sendiri, merchant menerbitkan paket yang dapat digunakan kembali dengan jumlah, periode penagihan, mint yang diterima, kolektor yang diizinkan, dan batasan tujuan opsional.

Pengguna meninjau dan menerima ketentuan tersebut, lalu membuat PDA Subscription Delegation yang terkait dengan paket. Ketentuan penagihan yang diterima disalin ke akun langganan pengguna sehingga merchant tidak dapat diam-diam mengubah harga utama atau periode penagihan bagi pelanggan yang sudah ada. Pemilik paket atau penarik yang disetujui kemudian dapat menagih hingga jumlah paket pada setiap periode penagihan.

Perbedaan ini penting:

  • Delegasi berulang adalah otorisasi yang ditentukan pembayar
  • Paket langganan adalah ketentuan yang diterbitkan merchant dan disetujui secara eksplisit oleh pembayar

Ketiga model menggunakan Subscription Authority yang sama dan pada akhirnya mengeksekusi transfer melalui arsitektur delegasi dasar yang sama.

Implementasi Referensi

Subscriptions Delegation Program dirancang dan dibangun oleh Moonsong Labs melalui kemitraan dengan Solana Foundation serta diaudit oleh Cantina. Kode sumber, dokumentasi, dan semua kliennya tersedia di repositori langganan Solana Foundation.

Program onchain ini ditulis dalam Rust no_std menggunakan Pinocchio. Pinocchio menyediakan model pengembangan tingkat rendah dengan sedikit dependensi. Dibandingkan dengan implementasi Anchor pada umumnya, pendekatan ini memungkinkan program mengelola penggunaan compute dan ukuran biner dengan lebih cermat.

Repositori ini juga menggunakan Codama untuk menghasilkan klien TypeScript dan Rust yang tersinkronisasi langsung dari antarmuka program. Untuk aplikasi TypeScript, paket utamanya adalah:

Kode
pnpm add @solana/subscriptions

Tersedia pula aplikasi web demo resmi yang menyediakan implementasi menyeluruh dan mudah digunakan di devnet. Program ini mendukung SPL Token dan Token-2022 serta menerbitkan peristiwa siklus hidup dan transfer onchain yang dapat didekode oleh aplikasi dan pengindeks menggunakan IDL yang telah dipublikasikan.

Kasus Penggunaan: Yang Dapat Dibangun Developer

Program ini berguna dalam situasi apa pun ketika pengguna dapat menentukan batas pembayaran mendatang sebelum mengetahui waktu pasti transfer akan dilakukan. Pengguna menandatangani satu kali untuk menetapkan otorisasi; merchant, layanan, penerima, atau agen kemudian dapat memulai transfer dalam batas tersebut.

Karena setiap pengaturan pengeluaran direpresentasikan oleh PDA tersendiri, berbagai kasus penggunaan ini dapat berjalan bersamaan di balik Subscription Authority yang sama. Pengguna dapat membayar paket API, menyediakan anggaran mingguan untuk agen AI, dan mengotorisasi retainer kontraktor dari akun token USDC yang sama tanpa satu otorisasi mengganggu otorisasi lainnya.

Penagihan API dan Infrastruktur Berulang

Paket langganan sangat cocok untuk produk SaaS, penyedia RPC, platform data, dan layanan infrastruktur lainnya. Penyedia dapat menerbitkan paket onchain terpisah untuk setiap tingkat produk dengan menentukan mint token yang diterima, harga, periode penagihan, kolektor yang disetujui, dan tujuan pembayaran yang diizinkan. Hal ini menghadirkan pengalaman berlangganan yang familier tanpa memerlukan pemroses kartu.

Pengeluaran Terbatas untuk Agen AI

Agen otonom memerlukan kemampuan untuk membayar API, compute, data, layanan perdagangan, dan sumber daya lainnya tanpa meminta persetujuan manusia. Namun, memberikan kendali tanpa batas atas dompet berisi dana kepada agen menimbulkan risiko keamanan yang jelas.

Delegasi tetap memberikan alternatif yang lebih aman. Pengguna dapat mengotorisasi agen untuk membelanjakan token hingga jumlah tertentu dan menetapkan masa berlaku tegas pada otorisasi tersebut. Pengguna juga dapat mencabut delegasi sebelum masa berlakunya berakhir.

Delegasi berulang memperluas model yang sama. Agen dapat menerima batas harian untuk permintaan API atau anggaran operasional mingguan, dengan jumlah yang tersedia diatur ulang pada awal setiap periode.

Penggajian Onchain dan Pembayaran Kontraktor

Delegasi berulang dapat mendukung penggajian berbasis penarikan, retainer, hibah, dan perjanjian kontraktor. Pembayar mengotorisasi karyawan atau kontraktor untuk menagih hingga jumlah tertentu per periode pembayaran. Otorisasi dapat menentukan jumlah per periode, durasi periode, waktu mulai, dan kedaluwarsa akhir. Saat pembayaran jatuh tempo, penerima atau layanan penggajian mengirimkan transaksi transfer.

Ini berbeda dari transaksi penggajian berbasis pengiriman tradisional. Pembayar tidak mengirimkan dana secara otomatis pada hari gajian. Sebaliknya, penerima mendapatkan hak terbatas untuk menarik jumlah yang disepakati selama setiap periode. Hasilnya adalah perjanjian pembayaran transparan yang dapat diperiksa secara onchain oleh kedua pihak.

Transfer dan aktivitas delegasi dapat dilacak melalui peristiwa yang diterbitkan program sehingga dashboard penggajian dan integrasi akuntansi dapat dibangun.

Penagihan Invoice Stablecoin

Gateway pembayaran dan platform penagihan B2B dapat menggunakan program ini untuk menggantikan permintaan pembayaran berulang dengan otorisasi persisten yang dibatasi. Pelanggan dapat mengotorisasi gateway untuk menagih:

  • Hingga jumlah total tetap untuk pesanan pembelian
  • Hingga jumlah tertentu dalam setiap periode invoice mingguan atau bulanan
  • Harga paket merchant terstandardisasi pada setiap siklus penagihan

Arsitektur yang sama dapat mendukung invoice berulang, akuisisi merchant, batas penggunaan, kebijakan pengeluaran perusahaan, dan alur kerja lain saat pembayar menginginkan otomatisasi tanpa menyerahkan kendali tanpa batas.

Pembayaran Mikro Konten dan Media

Delegasi berulang juga dapat mendukung model pembayaran berbasis penggunaan untuk penerbit, platform streaming, penyedia riset, dan layanan media lainnya.

Pengguna dapat mengotorisasi batas pengeluaran bulanan yang dikurangi sedikit demi sedikit setiap kali mereka mengakses konten berbayar. Misalnya, membuka artikel dapat menghabiskan 0,10 USDC dari batas bulanan 10 USDC, sedangkan laporan premium atau streaming video dapat memiliki harga lebih tinggi. Platform mengirimkan setiap pembayaran saat konten diakses.

Hal ini memungkinkan model “bayar sesuai yang Anda baca” tanpa memerlukan tanda tangan dompet untuk setiap artikel atau memaksa pengguna mengambil langganan tetap yang berlaku sepenuhnya atau tidak sama sekali. Penerbit memperoleh cara yang skalabel untuk memonetisasi setiap akses, sedangkan pengguna tetap memiliki batas pengeluaran yang dapat diprediksi dan dapat mencabut otorisasi kapan saja.

Bangun Alur Langganan di Devnet dengan Helius

Dalam bagian tutorial ini, kita akan membangun siklus hidup langganan merchant melalui langkah-langkah berikut:

  • Pelanggan menginisialisasi Subscription Authority untuk akun tokennya
  • Merchant menerbitkan paket Langganan
  • Pelanggan menerima paket
  • Merchant menagih pembayaran
  • Pelanggan membatalkan langganan

Contoh ini menggunakan @solana/subscriptions@0.4.0, klien TypeScript terbaru yang telah dipublikasikan pada saat artikel ini ditulis. Mengunci versi paket menjaga tutorial tetap stabil meskipun SDK berubah di kemudian hari.

Kita akan memodelkan dua pihak:

PeranTanggung Jawab
PelangganMemiliki token, menginisialisasi Subscription Authority, berlangganan, dan membatalkan paket
MerchantMenerbitkan paket dan mengirimkan transaksi penagihan

Untuk token, kita akan membuat mint devnet khusus dengan enam angka desimal dan menerbitkan 100 token uji kepada pelanggan. Hal ini menghindari ketergantungan pada faucet stablecoin terpisah sambil mempertahankan aritmetika unit dasar yang sama seperti token enam desimal, misalnya USDC.

Keypair JSON lokal membuat alur ini mudah dijalankan dari command line. Dalam aplikasi nyata, transaksi pelanggan biasanya ditandatangani melalui dompet browser atau seluler, sedangkan kolektor merchant menggunakan penanda tangan backend yang dikelola dengan aman.

Prasyarat

Tutorial ini mengasumsikan Anda memiliki:

  • Rilis Node.js terbaru
  • pnpm
  • Solana CLI
  • Kunci API Helius berbayar
  • SOL Devnet untuk kedua dompet uji

Siapkan Proyek

Buat proyek baru dengan direktori keys untuk menyimpan keypair pelanggan dan merchant:

Kode
mkdir helius-subscriptions-devnet
cd helius-subscriptions-devnet

pnpm init
mkdir -p src keys

Tambahkan "type": "module" ke package.json, lalu instal dependensi:

Kode
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

Buat tsconfig.json:

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

Buat Dompet Merchant dan Pelanggan

Buat satu keypair untuk setiap pihak:

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

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

Keypair ini hanya untuk tutorial devnet. Jangan melakukan commit kunci produksi ke repositori atau menyimpan penanda tangan merchant produksi sebagai file JSON yang tidak dienkripsi.

Buat .gitignore:

.gitignore
node_modules/
.env
keys/
state.json

Buat .env dan tambahkan kunci API Helius Anda:

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

Kedua dompet memerlukan SOL untuk membayar biaya transaksi dan membuat akun PDA masing-masing.

Kode
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 juga menyediakan faucet devnet melalui dashboard-nya. Permintaan SOL Devnet melalui faucet Helius atau RPC Helius memerlukan paket Helius berbayar.

Buat Klien Helius Bersama

Setiap skrip memerlukan koneksi Helius, plugin program, nilai konfigurasi, dan alamat yang sama. Kita akan memasukkan semuanya ke 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 menetapkan keypair yang dimuat sebagai identitas klien sekaligus pembayar biaya. Plugin RPC Helius menangani perencanaan, pengiriman, dan konfirmasi transaksi, sedangkan plugin token dan langganan menambahkan helper akun dan instruksinya masing-masing. Setiap skrip mencetak URL Orb (block explorer Helius) untuk transaksi yang dihasilkan.

Buat Mint Uji Devnet

Sebelum menginisialisasi Subscription Authority, pelanggan harus memiliki akun token untuk mint paket. Kita akan membuat mint uji dengan enam angka desimal, menerbitkan 100 token kepada pelanggan, dan membuat akun token tujuan kosong untuk merchant.

Plugin token Solana Kit menyediakan helper untuk membuat mint, akun token terkait, dan mencetak token. Buat 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,
);

Jalankan skrip. Skrip tersebut akan membuat state.json yang berisi informasi tentang mint yang dihasilkan dan ID paket unik. Pelanggan kini memiliki 100 token uji, sedangkan merchant memiliki akun token kosong yang siap menerima pembayaran langganan.

Inisialisasi Subscription Authority Pelanggan

Subscription Authority dibuat untuk pasangan (customer, mint) tertentu. Akun token pelanggan harus sudah ada sebelum inisialisasi.

Transaksi inisialisasi membuat PDA Subscription Authority dan menyetujuinya sebagai delegasi untuk akun token pelanggan. Authority yang sama kemudian dapat digunakan kembali untuk setiap delegasi tetap, delegasi berulang, dan Subscription Plan yang melibatkan pelanggan dan mint tersebut. Pelanggan menandatangani transaksi ini.

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

Jalankan skrip sebagai pelanggan. Skrip terlebih dahulu memeriksa apakah PDA sudah ada. Dengan demikian, perintah dapat dijalankan kembali dengan aman dan pengiriman transaksi inisialisasi duplikat dapat dihindari. Setelah dikonfirmasi, akun token pelanggan memiliki Subscription Authority sebagai delegasi Token Program-nya.

Buat Paket Langganan Merchant

Merchant kini menerbitkan ketentuan penagihan yang dapat diterima pelanggan. PDA Plan diperoleh dari alamat merchant dan ID paket.

Paket menentukan mint pembayaran, jumlah maksimum per periode, durasi periode, kolektor yang disetujui, tujuan yang diizinkan, dan metadata offchain opsional.

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

Jalankan skrip sebagai merchant. Beberapa kolom berikut sangat penting:

amount

Nilai token dinyatakan dalam unit dasar. Mint kita memiliki enam angka desimal, sehingga 5 token = 5.000.000 unit dasar. Paket ini mengizinkan merchant menagih maksimum kumulatif lima token selama setiap periode penagihan. Merchant dapat menagih seluruh jumlah tersebut dalam satu transaksi atau membaginya menjadi beberapa transaksi yang lebih kecil.

periodHours

Kita menggunakan durasi minimum, yaitu satu jam. Dengan demikian, kita dapat berlangganan, menagih pembayaran, menunggu satu jam, lalu menunjukkan bahwa batas tersebut diatur ulang tanpa harus menunggu satu bulan. Paket produksi akan menggunakan interval yang ditentukan oleh ketentuan penagihan produk sebenarnya.

destinations

Daftar tujuan yang diizinkan berisi pemilik dompet, bukan alamat akun token. Saat menagih pembayaran, program memeriksa pemilik akun token penerima. Karena dompet merchant kita tercantum dalam daftar tujuan, akun token terkaitnya merupakan penerima yang valid.

pullers

Merchant selalu diizinkan menagih dari paketnya sendiri. Dompet layanan penagihan tambahan dapat ditambahkan ke pullers. Kita membiarkan daftar ini kosong agar hanya keypair merchant yang dapat menagih pembayaran. Dengan konfigurasi kita, hanya merchant atau dompet yang disertakan dalam daftar penarik paket yang dapat mengirimkan transaksi penagihan yang valid. 

Minta Pelanggan Berlangganan

Pelanggan kini meninjau dan menerima ketentuan paket merchant saat ini. PDA Subscription Delegation yang dihasilkan diperoleh dari PDA paket + alamat pelanggan. 

Plugin TypeScript mengambil akun paket saat ini selama subscribe, sehingga kita tidak perlu memasukkan jumlah, periode, atau timestamp pembuatan yang diharapkan secara manual. Nilai-nilai tersebut disertakan dalam transaksi sebagai ketentuan. Buat 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,
);

Jalankan skrip sebagai pelanggan. Pelanggan menandatangani transaksi ini untuk menerima otorisasi pengeluaran baru. Akun langganan menyimpan ketentuan yang diterima. 

Setelah paket dibuat, planId, owner, mint, amount, periodHours, createdAt, dan destinations tidak dapat diubah, yang berarti ketentuannya tidak dapat diganti. Jika merchant kemudian memperbarui kolom yang dapat diubah pada paket, pelanggan lama tetap mempertahankan ketentuan yang awalnya mereka terima, sedangkan pelanggan baru menerima versi paket saat ini.

Pelanggan kini memiliki langganan aktif, tetapi belum ada pembayaran yang dilakukan.

Minta Merchant Menagih Pembayaran

Merchant kini dapat menagih hingga batas lima token paket selama periode penagihan saat ini. Subscriptions Program tidak mengeksekusi transaksi ini secara otomatis ketika timer berakhir. Backend merchant, worker penagihan, cron job, atau penarik yang disetujui tetap harus mengirimkan transaksi penagihan. Buat 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,
);

Jalankan skrip sebagai merchant. Selama eksekusi, program memverifikasi bahwa:

  • Pemanggil adalah merchant atau penarik yang disetujui
  • Langganan merupakan milik pelanggan dan paket tersebut
  • Langganan belum kedaluwarsa
  • Jumlah yang diminta tidak melebihi sisa batas periode saat ini
  • Dompet merchant merupakan tujuan yang disetujui
  • Akun token penerima dimiliki oleh tujuan yang disetujui
  • Mint dan Token Program cocok dengan paket yang diterima

Karena transaksi ini menagih seluruh batas lima token, menjalankannya kembali dalam periode satu jam yang sama semestinya akan gagal. Setelah periode berikutnya dimulai, batas periode akan diatur ulang dan merchant dapat menjalankan kembali skrip penagihan. Dalam sistem penagihan produksi, padanan skrip tersebut biasanya hanya dijalankan ketika invoice jatuh tempo.

Pelanggan Membatalkan Langganannya

Pelanggan dapat membatalkan langganan tanpa kerja sama merchant. Alur pembatalan standar tidak langsung menutup akun Subscription Delegation. Sebaliknya, alur ini menandai bahwa langganan akan berakhir dan menetapkan expiresAtTs. Setelah waktu kedaluwarsa tersebut berlalu, pelanggan dapat mencabut langganan dan menutup PDA. Buat 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,
);

Jalankan skrip sebagai pelanggan. Output mencakup timestamp saat pembatalan mulai berlaku. Instruksi standar cancelSubscription menerapkan masa tenggang hingga akhir periode penagihan aktif.

Secara operasional, pembatalan tidak boleh dianggap langsung meniadakan otorisasi periode saat ini. Sisa batas pada periode saat ini mungkin masih dapat ditagih hingga expiresAtTs. Merchant tidak dapat memulai periode penagihan baru setelah pembatalan berlaku.

Hal ini sesuai dengan perilaku langganan umum, yaitu pembatalan menghentikan perpanjangan berikutnya dan bukan mengakhiri secara retroaktif periode yang sudah dimulai pelanggan.

Studi Kasus Praktis: Paket Langganan Onchain Helius

Helius merupakan salah satu mitra peluncuran yang membantu membentuk Subscriptions Delegation Program sebelum dirilis di mainnet. Kami menggunakan program ini untuk mendukung perpanjangan otomatis paket API kami dengan USDC. Tujuan kami adalah memberikan kemudahan langganan SaaS konvensional kepada pelanggan yang membayar dengan kripto, sekaligus mempertahankan otorisasi dan penyelesaian pembayaran sepenuhnya di Solana.

Untuk mengaktifkan pembayaran otomatis, pelanggan menambahkan dompet Solana dari bagian Metode Pembayaran pada dashboard penagihan Helius. 

Selama penyiapan, pelanggan menandatangani persetujuan satu kali untuk Solana Subscriptions Program resmi dan mengotorisasi Helius untuk menagih pembayaran langganan dalam USDC dari dompet tersebut.

Saat invoice perpanjangan jatuh tempo, sistem penagihan Helius mengirimkan transaksi penagihan. Pelanggan tidak perlu membuka tautan pembayaran, menghubungkan kembali dompetnya, atau menandatangani transfer lain. Subscriptions Delegation Program menyediakan otorisasi onchain yang dapat digunakan kembali, sementara Helius tetap mengelola jadwal invoice, status akun, dan hak akses produk.

Pelanggan dapat menghubungkan hingga tiga dompet, tetapi hanya dompet yang ditandai sebagai metode pembayaran default yang digunakan untuk perpanjangan otomatis. Helius tidak mencoba membagi tagihan ke beberapa dompet atau beralih ke dompet lain yang terhubung jika dompet default tidak dapat membayar invoice.

Pelanggan dapat mengubah dompet default dari dashboard. Mereka juga dapat menghapus dompet, yang memerlukan tanda tangan dan mencabut otorisasinya untuk pembayaran otomatis. Menghapus satu-satunya dompet yang terhubung akan mengembalikan akun ke tautan pembayaran manual.

Tentu saja, otorisasi onchain tidak menjamin bahwa dompet akan memiliki cukup USDC saat invoice berikutnya jatuh tempo. Dalam skenario ini:

  • Helius tidak menagih dompet untuk perpanjangan tersebut.
  • Pelanggan menerima tautan pembayaran melalui email dan di dashboard.
  • Invoice tidak dicoba ulang secara otomatis pada dompet.
  • Setelah pelanggan menambah saldo, perpanjangan berikutnya dapat kembali ditagih secara otomatis.

Mekanisme fallback ini menjaga status penagihan tetap sederhana. Penagihan otomatis yang gagal berubah menjadi invoice terbuka biasa, bukan rangkaian transaksi percobaan ulang onchain tanpa batas.

Implementasi ini memberikan contoh praktis tentang cara Subscriptions Delegation Program melengkapi stack penagihan produksi. Program ini tidak menggantikan pembuatan invoice, pengelolaan akun, notifikasi, atau penegakan hak akses. Program ini menggantikan bagian yang sebelumnya mengharuskan pelanggan mengotorisasi setiap perpanjangan atau memerlukan pemroses pembayaran terpusat untuk menyimpan dan menggunakan otorisasi tersebut.

Kesimpulan

Program Solana Subscriptions menghadirkan cara terstandardisasi untuk membangun pembayaran berulang langsung secara onchain. Dengan menggabungkan subscription authority, paket merchant, dan transfer token yang didelegasikan, developer dapat menerapkan penagihan langganan tanpa mengandalkan infrastruktur pembayaran offchain atau logika pembayaran khusus.

Jika Anda membangun pembayaran berulang di Solana, Subscriptions program adalah pilihan awal yang tepat. Dengan SDK TypeScript serta RPC dan API Helius, integrasi penagihan langganan onchain menjadi mudah sehingga Anda dapat berfokus pada aplikasi, bukan mekanisme pembayaran yang mendasarinya.

Referensi Lebih Lanjut

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan

Gambar diperbesar